From 44a99d1736facffbd6944b817230b8ca8965ba39 Mon Sep 17 00:00:00 2001 From: Miguel de la Cruz Date: Wed, 14 Jun 2023 23:33:26 +0200 Subject: [PATCH] Move playbooks to plugin (#23732) * Remove build references * Remove playbooks webapp and server, and add the prepackaged plugin * Remove translations * Add ProductSettings to the playwright type * Restore playbooks as a prepackaged plugin for cypress e2e tests --- e2e-tests/cypress/tests/support/api/plugin.js | 1 + .../support/server/default_config.ts | 4 +- server/Makefile | 9 +- server/channels/app/plugin_install_test.go | 2 +- server/channels/app/product.go | 10 - server/cmd/mattermost/main.go | 1 - server/config/client.go | 2 - server/config/client_test.go | 14 - server/config/diff_test.go | 9 + server/go.mod | 8 +- server/go.sum | 10 - server/i18n/en.json | 208 - server/playbooks/client/LICENSE | 202 - server/playbooks/client/action.go | 63 - server/playbooks/client/actions.go | 78 - server/playbooks/client/client.go | 276 - server/playbooks/client/doc.go | 5 - server/playbooks/client/doc_test.go | 36 - server/playbooks/client/error_response.go | 52 - server/playbooks/client/playbook.go | 192 - server/playbooks/client/playbook_run.go | 292 - server/playbooks/client/playbook_runs.go | 375 - server/playbooks/client/playbook_runs_test.go | 77 - server/playbooks/client/playbooks.go | 269 - server/playbooks/client/playbooks_test.go | 76 - server/playbooks/client/reminder.go | 8 - server/playbooks/client/reminders.go | 36 - server/playbooks/client/settings.go | 55 - server/playbooks/client/stats.go | 38 - server/playbooks/client/telemetry.go | 49 - server/playbooks/client/unexport_test.go | 7 - server/playbooks/product/api_adapter.go | 497 - .../product/imports/playbooks_imports.go | 10 - server/playbooks/product/logrus.go | 86 - server/playbooks/product/playbooks_product.go | 818 -- .../product/pluginapi/cluster/job.go | 232 - .../product/pluginapi/cluster/job_once.go | 212 - .../pluginapi/cluster/job_once_scheduler.go | 231 - .../product/pluginapi/cluster/mutex.go | 188 - .../product/pluginapi/cluster/wait.go | 46 - server/playbooks/product/pluginapi/license.go | 110 - server/playbooks/server/.gitignore | 3 - server/playbooks/server/api/actions.go | 214 - server/playbooks/server/api/api.go | 114 - server/playbooks/server/api/api.yaml | 2053 ---- server/playbooks/server/api/bot.go | 226 - server/playbooks/server/api/categories.go | 355 - server/playbooks/server/api/context.go | 41 - server/playbooks/server/api/error_handler.go | 36 - .../playbooks/server/api/graph_dataloader.go | 17 - server/playbooks/server/api/graphql.go | 191 - .../server/api/graphql_loader_favorite.go | 56 - .../server/api/graphql_loader_playbook.go | 79 - .../playbooks/server/api/graphql_playbook.go | 239 - server/playbooks/server/api/graphql_root.go | 25 - .../server/api/graphql_root_playbook.go | 549 - .../playbooks/server/api/graphql_root_run.go | 327 - server/playbooks/server/api/graphql_run.go | 266 - server/playbooks/server/api/graphqli.html | 39 - server/playbooks/server/api/logger.go | 62 - server/playbooks/server/api/playbook_runs.go | 1888 ---- server/playbooks/server/api/playbooks.go | 774 -- server/playbooks/server/api/schema.graphqls | 324 - server/playbooks/server/api/settings.go | 43 - server/playbooks/server/api/signal.go | 134 - server/playbooks/server/api/stats.go | 168 - server/playbooks/server/api/telemetry.go | 256 - server/playbooks/server/api/urls.go | 41 - server/playbooks/server/api_actions_test.go | 457 - server/playbooks/server/api_bot_test.go | 54 - server/playbooks/server/api_general_test.go | 23 - .../server/api_graphql_playbooks_test.go | 691 -- .../playbooks/server/api_graphql_runs_test.go | 1345 --- server/playbooks/server/api_playbooks_test.go | 1543 --- server/playbooks/server/api_runs_test.go | 1767 ---- server/playbooks/server/api_settings_test.go | 37 - server/playbooks/server/api_stats_test.go | 234 - server/playbooks/server/api_telemetry_test.go | 40 - server/playbooks/server/app/action.go | 128 - .../playbooks/server/app/actions_service.go | 578 -- server/playbooks/server/app/category.go | 153 - .../playbooks/server/app/category_service.go | 167 - server/playbooks/server/app/errors.go | 24 - server/playbooks/server/app/export.go | 75 - server/playbooks/server/app/export_test.go | 82 - .../playbooks/server/app/keywords_ignore.go | 44 - .../app/mocks/mock_job_once_scheduler.go | 109 - .../server/app/permissions_service.go | 545 - server/playbooks/server/app/playbook.go | 741 -- server/playbooks/server/app/playbook_run.go | 1209 --- .../server/app/playbook_run_service.go | 3674 ------- .../playbooks/server/app/playbook_run_test.go | 219 - .../playbooks/server/app/playbook_service.go | 258 - server/playbooks/server/app/playbook_test.go | 186 - .../playbooks/server/app/plugin_api_tools.go | 37 - .../server/app/regular_digest_service.go | 39 - .../server/app/regular_digest_service_test.go | 168 - server/playbooks/server/app/reminder.go | 253 - server/playbooks/server/app/sort.go | 72 - server/playbooks/server/app/task_actions.go | 153 - .../playbooks/server/app/task_actions_test.go | 107 - server/playbooks/server/app/telemetry.go | 114 - server/playbooks/server/app/urls.go | 49 - server/playbooks/server/app/urls_test.go | 59 - server/playbooks/server/app/user_info.go | 32 - server/playbooks/server/app/variables.go | 37 - server/playbooks/server/app/variables_test.go | 64 - server/playbooks/server/bot/bot.go | 82 - .../playbooks/server/bot/mocks/mock_poster.go | 245 - server/playbooks/server/bot/poster.go | 296 - server/playbooks/server/command/command.go | 2044 ---- server/playbooks/server/config/config.go | 37 - .../playbooks/server/config/configuration.go | 50 - server/playbooks/server/config/service.go | 199 - .../server/e2etest.config.json.sample | 14 - server/playbooks/server/enterprise/LICENSE | 39 - server/playbooks/server/enterprise/license.go | 61 - server/playbooks/server/httptools/client.go | 73 - server/playbooks/server/main_test.go | 507 - server/playbooks/server/metrics/metrics.go | 254 - server/playbooks/server/metrics/service.go | 48 - .../playbooks/server/playbooks/service_api.go | 135 - .../playbooks/server/scheduler/scheduler.go | 76 - server/playbooks/server/sqlstore/actions.go | 321 - .../playbooks/server/sqlstore/actions_test.go | 95 - server/playbooks/server/sqlstore/category.go | 271 - .../server/sqlstore/category_test.go | 123 - server/playbooks/server/sqlstore/migrate.go | 160 - .../playbooks/server/sqlstore/migrations.go | 2477 ----- .../migrations/future/mysql/0.59.down.sql | 1 - .../migrations/future/mysql/0.59.up.sql | 1 - .../migrations/future/mysql/0.60.down.sql | 62 - .../migrations/future/mysql/0.60.up.sql | 63 - .../migrations/future/mysql/0.61.down.sql | 45 - .../migrations/future/mysql/0.61.up.sql | 50 - .../migrations/future/mysql/0.62.down.sql | 2 - .../migrations/future/mysql/0.62.up.sql | 4 - .../migrations/future/mysql/0.63.down.sql | 14 - .../migrations/future/mysql/0.63.up.sql | 14 - .../migrations/future/postgres/0.59.down.sql | 96 - .../migrations/future/postgres/0.59.up.sql | 95 - .../migrations/future/postgres/0.60.down.sql | 4 - .../migrations/future/postgres/0.60.up.sql | 4 - .../migrations/future/postgres/0.61.down.sql | 18 - .../migrations/future/postgres/0.61.up.sql | 11 - .../migrations/future/postgres/0.62.down.sql | 2 - .../migrations/future/postgres/0.62.up.sql | 3 - .../migrations/future/postgres/0.63.down.sql | 1 - .../migrations/future/postgres/0.63.up.sql | 1 - .../mysql/000001_create_IR_system.down.sql | 1 - .../mysql/000001_create_IR_system.up.sql | 4 - .../mysql/000002_create_IR_incident.down.sql | 1 - .../mysql/000002_create_IR_incident.up.sql | 19 - .../mysql/000003_create_ir_playbook.down.sql | 1 - .../mysql/000003_create_ir_playbook.up.sql | 14 - .../000004_create_ir_playbook_member.down.sql | 1 - .../000004_create_ir_playbook_member.up.sql | 6 - ...active_stage_title_to_ir_incident.down.sql | 14 - ...d_active_stage_title_to_ir_incident.up.sql | 22 - .../000006_create_ir_status_posts.down.sql | 1 - .../000006_create_ir_status_posts.up.sql | 7 - ...d_reminder_post_id_to_ir_incident.down.sql | 14 - ...add_reminder_post_id_to_ir_incident.up.sql | 14 - ...oadcast_channel_id_to_ir_incident.down.sql | 14 - ...broadcast_channel_id_to_ir_incident.up.sql | 14 - ...oadcast_channel_id_to_ir_playbook.down.sql | 14 - ...broadcast_channel_id_to_ir_playbook.up.sql | 14 - ..._previous_reminder_to_ir_incident.down.sql | 14 - ...dd_previous_reminder_to_ir_incident.up.sql | 14 - ...r_message_template_to_ir_playbook.down.sql | 14 - ...der_message_template_to_ir_playbook.up.sql | 18 - ...r_message_template_to_ir_incident.down.sql | 14 - ...der_message_template_to_ir_incident.up.sql | 18 - ...er_default_seconds_to_ir_playbook.down.sql | 14 - ...imer_default_seconds_to_ir_playbook.up.sql | 14 - ...add_current_status_to_ir_incident.down.sql | 14 - ...4_add_current_status_to_ir_incident.up.sql | 18 - ...015_add_status_to_ir_status_posts.down.sql | 14 - ...00015_add_status_to_ir_status_posts.up.sql | 14 - .../000016_create_ir_timeline_event.down.sql | 1 - .../000016_create_ir_timeline_event.up.sql | 15 - ...d_reporter_user_id_to_ir_incident.down.sql | 14 - ...add_reporter_user_id_to_ir_incident.up.sql | 18 - ...d_invited_user_ids_to_ir_incident.down.sql | 14 - ...ted_invited_user_ids_to_ir_incident.up.sql | 18 - ...d_invited_useri_ds_to_ir_playbook.down.sql | 14 - ...ted_invited_useri_ds_to_ir_playbook.up.sql | 18 - ...vite_users_enabled_to_ir_playbook.down.sql | 14 - ...invite_users_enabled_to_ir_playbook.up.sql | 14 - ...fault_commander_id_to_ir_incident.down.sql | 14 - ...default_commander_id_to_ir_incident.up.sql | 14 - ...fault_commander_id_to_ir_playbook.down.sql | 14 - ...default_commander_id_to_ir_playbook.up.sql | 14 - ..._commander_enabled_to_ir_playbook.down.sql | 14 - ...lt_commander_enabled_to_ir_playbook.up.sql | 14 - ...ated_at_deleted_at_in_ir_incident.down.sql | 1 - ...reated_at_deleted_at_in_ir_incident.up.sql | 8 - ...ncement_channel_id_to_ir_incident.down.sql | 14 - ...ouncement_channel_id_to_ir_incident.up.sql | 14 - ...ncement_channel_id_to_ir_playbook.down.sql | 14 - ...ouncement_channel_id_to_ir_playbook.up.sql | 14 - ...nt_channel_enabled_to_ir_playbook.down.sql | 14 - ...ment_channel_enabled_to_ir_playbook.up.sql | 14 - ...ok_on_creation_url_to_ir_incident.down.sql | 14 - ...hook_on_creation_url_to_ir_incident.up.sql | 18 - ...ok_on_creation_url_to_ir_playbook.down.sql | 14 - ...hook_on_creation_url_to_ir_playbook.up.sql | 18 - ...n_creation_enabled_to_ir_playbook.down.sql | 14 - ..._on_creation_enabled_to_ir_playbook.up.sql | 14 - ..._invited_group_ids_to_ir_incident.down.sql | 14 - ...ed_invited_group_ids_to_ir_incident.up.sql | 18 - ..._invited_group_ids_to_ir_playbook.down.sql | 14 - ...ed_invited_group_ids_to_ir_playbook.up.sql | 18 - ..._add_retrospective_to_ir_incident.down.sql | 14 - ...33_add_retrospective_to_ir_incident.up.sql | 18 - ...dd_message_on_join_to_ir_playbook.down.sql | 14 - ..._add_message_on_join_to_ir_playbook.up.sql | 18 - ...ge_on_join_enabled_to_ir_playbook.down.sql | 14 - ...sage_on_join_enabled_to_ir_playbook.up.sql | 14 - ...dd_message_on_join_to_ir_incident.down.sql | 14 - ..._add_message_on_join_to_ir_incident.up.sql | 18 - .../000037_create_ir_viewed_channel.down.sql | 1 - .../000037_create_ir_viewed_channel.up.sql | 5 - ...ctive_published_at_to_ir_incident.down.sql | 14 - ...pective_published_at_to_ir_incident.up.sql | 14 - ...r_interval_seconds_to_ir_incident.down.sql | 14 - ...der_interval_seconds_to_ir_incident.up.sql | 14 - ...r_interval_seconds_to_ir_playbook.down.sql | 14 - ...der_interval_seconds_to_ir_playbook.up.sql | 14 - ...ctive_was_canceled_to_ir_incident.down.sql | 14 - ...pective_was_canceled_to_ir_incident.up.sql | 14 - ...ospective_template_to_ir_playbook.down.sql | 14 - ...trospective_template_to_ir_playbook.up.sql | 18 - ..._status_update_url_to_ir_playbook.down.sql | 14 - ...on_status_update_url_to_ir_playbook.up.sql | 18 - ...tus_update_enabled_to_ir_playbook.down.sql | 14 - ...tatus_update_enabled_to_ir_playbook.up.sql | 14 - ..._status_update_url_to_ir_incident.down.sql | 14 - ...on_status_update_url_to_ir_incident.up.sql | 18 - ...ignal_any_keywords_to_ir_playbook.down.sql | 14 - ..._signal_any_keywords_to_ir_playbook.up.sql | 18 - ...y_keywords_enabled_to_ir_playbook.down.sql | 14 - ...any_keywords_enabled_to_ir_playbook.up.sql | 14 - ...0048_add_update_at_to_ir_playbook.down.sql | 14 - ...000048_add_update_at_to_ir_playbook.up.sql | 32 - ...t_status_update_at_to_ir_incident.down.sql | 14 - ...ast_status_update_at_to_ir_incident.up.sql | 25 - ...on_archive_enabled_to_ir_playbook.down.sql | 14 - ...l_on_archive_enabled_to_ir_playbook.up.sql | 14 - ...on_archive_enabled_to_ir_incident.down.sql | 14 - ...l_on_archive_enabled_to_ir_incident.up.sql | 14 - ...z_echannel_enabled_to_ir_playbook.down.sql | 14 - ...riz_echannel_enabled_to_ir_playbook.up.sql | 14 - ...z_echannel_enabled_to_ir_incident.down.sql | 14 - ...riz_echannel_enabled_to_ir_incident.up.sql | 14 - ...el_on_archive_enabled_ir_playbook.down.sql | 14 - ...nnel_on_archive_enabled_ir_playbook.up.sql | 14 - ...el_on_archive_enabled_ir_incident.down.sql | 14 - ...nnel_on_archive_enabled_ir_incident.up.sql | 14 - ..._drop_status_from_ir_status_posts.down.sql | 14 - ...56_drop_status_from_ir_status_posts.up.sql | 14 - ...ate_current_status_in_ir_incident.down.sql | 7 - ...pdate_current_status_in_ir_incident.up.sql | 7 - ..._add_category_name_to_ir_playbook.down.sql | 14 - ...58_add_category_name_to_ir_playbook.up.sql | 18 - ..._add_category_name_to_ir_incident.down.sql | 14 - ...59_add_category_name_to_ir_incident.up.sql | 18 - ...adcast_channel_ids_to_ir_incident.down.sql | 14 - ...roadcast_channel_ids_to_ir_incident.up.sql | 25 - ...adcast_channel_ids_to_ir_playbook.down.sql | 29 - ...roadcast_channel_ids_to_ir_playbook.up.sql | 46 - ...nnel_id_to_root_id_to_ir_incident.down.sql | 14 - ...hannel_id_to_root_id_to_ir_incident.up.sql | 14 - ...0063_convert_charset_of_ir_system.down.sql | 1 - ...000063_convert_charset_of_ir_system.up.sql | 13 - ...064_add_pr_key_ir_playbook_member.down.sql | 14 - ...00064_add_pr_key_ir_playbook_member.up.sql | 14 - .../000065_drop_index_posts_unique.down.sql | 14 - .../000065_drop_index_posts_unique.up.sql | 14 - ...000066_add_pr_key_ir_status_posts.down.sql | 14 - .../000066_add_pr_key_ir_status_posts.up.sql | 14 - ...0067_add_pr_key_ir_timeline_event.down.sql | 14 - ...000067_add_pr_key_ir_timeline_event.up.sql | 14 - ...r_viewed_channel_channelid_userid.down.sql | 14 - ..._ir_viewed_channel_channelid_userid.up.sql | 14 - ...0069_add_pr_key_ir_viewed_channel.down.sql | 14 - ...000069_add_pr_key_ir_viewed_channel.up.sql | 14 - ...ugin_id_in_plugin_key_value_store.down.sql | 3 - ...plugin_id_in_plugin_key_value_store.up.sql | 3 - ...er_default_seconds_to_ir_incident.down.sql | 14 - ...imer_default_seconds_to_ir_incident.up.sql | 14 - ...bhook_on_creation_url_ir_playbook.down.sql | 14 - ...webhook_on_creation_url_ir_playbook.up.sql | 14 - ..._on_status_update_url_ir_playbook.down.sql | 14 - ...ok_on_status_update_url_ir_playbook.up.sql | 14 - ...bhook_on_creation_url_ir_incident.down.sql | 14 - ...webhook_on_creation_url_ir_incident.up.sql | 14 - ..._on_status_update_url_ir_incident.down.sql | 14 - ...ok_on_status_update_url_ir_incident.up.sql | 14 - .../mysql/000076_create_ir_userinfo.down.sql | 1 - .../mysql/000076_create_ir_userinfo.up.sql | 4 - ...tion_settings_json_to_ir_userinfo.down.sql | 14 - ...cation_settings_json_to_ir_userinfo.up.sql | 14 - ...ndex_ir_statusposts_posts_unique2.down.sql | 20 - ..._index_ir_statusposts_posts_unique2.up.sql | 14 - ..._viewed_channel_channelid_userid2.down.sql | 20 - ...ir_viewed_channel_channelid_userid2.up.sql | 14 - .../postgres/000001_create_IR_system.down.sql | 1 - .../postgres/000001_create_IR_system.up.sql | 4 - .../000002_create_IR_incident.down.sql | 1 - .../postgres/000002_create_IR_incident.up.sql | 20 - .../000003_create_ir_playbook.down.sql | 1 - .../postgres/000003_create_ir_playbook.up.sql | 14 - .../000004_create_ir_playbookmember.down.sql | 1 - .../000004_create_ir_playbookmember.up.sql | 8 - ...active_stage_title_to_ir_incident.down.sql | 1 - ...d_active_stage_title_to_ir_incident.up.sql | 22 - .../000006_create_ir_status_posts.down.sql | 1 - .../000006_create_ir_status_posts.up.sql | 8 - ...d_reminder_post_id_to_ir_incident.down.sql | 1 - ...add_reminder_post_id_to_ir_incident.up.sql | 1 - ...oadcast_channel_id_to_ir_incident.down.sql | 1 - ...broadcast_channel_id_to_ir_incident.up.sql | 1 - ...oadcast_channel_id_to_ir_playbook.down.sql | 1 - ...broadcast_channel_id_to_ir_playbook.up.sql | 1 - ..._previous_reminder_to_ir_incident.down.sql | 1 - ...dd_previous_reminder_to_ir_incident.up.sql | 1 - ...r_message_template_to_ir_playbook.down.sql | 1 - ...der_message_template_to_ir_playbook.up.sql | 1 - ...r_message_template_to_ir_incident.down.sql | 1 - ...der_message_template_to_ir_incident.up.sql | 1 - ...er_default_seconds_to_ir_playbook.down.sql | 1 - ...imer_default_seconds_to_ir_playbook.up.sql | 1 - ...add_current_status_to_ir_incident.down.sql | 1 - ...4_add_current_status_to_ir_incident.up.sql | 5 - ...015_add_status_to_ir_status_posts.down.sql | 1 - ...00015_add_status_to_ir_status_posts.up.sql | 1 - .../000016_create_ir_timeline_event.down.sql | 1 - .../000016_create_ir_timeline_event.up.sql | 16 - ...d_reporter_user_id_to_ir_incident.down.sql | 1 - ...add_reporter_user_id_to_ir_incident.up.sql | 5 - ...d_invited_user_ids_to_ir_incident.down.sql | 1 - ...ted_invited_user_ids_to_ir_incident.up.sql | 1 - ...d_invited_useri_ds_to_ir_playbook.down.sql | 1 - ...ted_invited_useri_ds_to_ir_playbook.up.sql | 1 - ...vite_users_enabled_to_ir_playbook.down.sql | 1 - ...invite_users_enabled_to_ir_playbook.up.sql | 1 - ...fault_commander_id_to_ir_incident.down.sql | 1 - ...default_commander_id_to_ir_incident.up.sql | 1 - ...fault_commander_id_to_ir_playbook.down.sql | 1 - ...default_commander_id_to_ir_playbook.up.sql | 1 - ..._commander_enabled_to_ir_playbook.down.sql | 1 - ...lt_commander_enabled_to_ir_playbook.up.sql | 1 - ...ated_at_deleted_at_in_ir_incident.down.sql | 1 - ...reated_at_deleted_at_in_ir_incident.up.sql | 7 - ...ncement_channel_id_to_ir_incident.down.sql | 1 - ...ouncement_channel_id_to_ir_incident.up.sql | 1 - ...ncement_channel_id_to_ir_playbook.down.sql | 1 - ...ouncement_channel_id_to_ir_playbook.up.sql | 1 - ...nt_channel_enabled_to_ir_playbook.down.sql | 1 - ...ment_channel_enabled_to_ir_playbook.up.sql | 1 - ...ok_on_creation_url_to_ir_incident.down.sql | 1 - ...hook_on_creation_url_to_ir_incident.up.sql | 1 - ...ok_on_creation_url_to_ir_playbook.down.sql | 1 - ...hook_on_creation_url_to_ir_playbook.up.sql | 1 - ...n_creation_enabled_to_ir_playbook.down.sql | 1 - ..._on_creation_enabled_to_ir_playbook.up.sql | 1 - ..._invited_group_ids_to_ir_incident.down.sql | 1 - ...ed_invited_group_ids_to_ir_incident.up.sql | 1 - ..._invited_group_ids_to_ir_playbook.down.sql | 1 - ...ed_invited_group_ids_to_ir_playbook.up.sql | 1 - ..._add_retrospective_to_ir_incident.down.sql | 1 - ...33_add_retrospective_to_ir_incident.up.sql | 1 - ...dd_message_on_join_to_ir_playbook.down.sql | 1 - ..._add_message_on_join_to_ir_playbook.up.sql | 1 - ...ge_on_join_enabled_to_ir_playbook.down.sql | 1 - ...sage_on_join_enabled_to_ir_playbook.up.sql | 1 - ...dd_message_on_join_to_ir_incident.down.sql | 1 - ..._add_message_on_join_to_ir_incident.up.sql | 1 - .../000037_create_ir_viewed_channel.down.sql | 1 - .../000037_create_ir_viewed_channel.up.sql | 6 - ...ctive_published_at_to_ir_incident.down.sql | 1 - ...pective_published_at_to_ir_incident.up.sql | 1 - ...r_interval_seconds_to_ir_incident.down.sql | 1 - ...der_interval_seconds_to_ir_incident.up.sql | 1 - ...r_interval_seconds_to_ir_playbook.down.sql | 1 - ...der_interval_seconds_to_ir_playbook.up.sql | 1 - ...ctive_was_canceled_to_ir_incident.down.sql | 1 - ...pective_was_canceled_to_ir_incident.up.sql | 1 - ...ospective_template_to_ir_playbook.down.sql | 1 - ...trospective_template_to_ir_playbook.up.sql | 5 - ..._status_update_url_to_ir_playbook.down.sql | 1 - ...on_status_update_url_to_ir_playbook.up.sql | 1 - ...tus_update_enabled_to_ir_playbook.down.sql | 1 - ...tatus_update_enabled_to_ir_playbook.up.sql | 1 - ..._status_update_url_to_ir_incident.down.sql | 1 - ...on_status_update_url_to_ir_incident.up.sql | 1 - ...ignal_any_keywords_to_ir_playbook.down.sql | 1 - ..._signal_any_keywords_to_ir_playbook.up.sql | 1 - ...y_keywords_enabled_to_ir_playbook.down.sql | 1 - ...any_keywords_enabled_to_ir_playbook.up.sql | 1 - ...0048_add_update_at_to_ir_playbook.down.sql | 1 - ...000048_add_update_at_to_ir_playbook.up.sql | 6 - ...t_status_update_at_to_ir_incident.down.sql | 1 - ...ast_status_update_at_to_ir_incident.up.sql | 12 - ...on_archive_enabled_to_ir_playbook.down.sql | 1 - ...l_on_archive_enabled_to_ir_playbook.up.sql | 1 - ...on_archive_enabled_to_ir_incident.down.sql | 1 - ...l_on_archive_enabled_to_ir_incident.up.sql | 1 - ...z_echannel_enabled_to_ir_playbook.down.sql | 1 - ...riz_echannel_enabled_to_ir_playbook.up.sql | 1 - ...z_echannel_enabled_to_ir_incident.down.sql | 1 - ...riz_echannel_enabled_to_ir_incident.up.sql | 1 - ...el_on_archive_enabled_ir_playbook.down.sql | 1 - ...nnel_on_archive_enabled_ir_playbook.up.sql | 1 - ...el_on_archive_enabled_ir_incident.down.sql | 1 - ...nnel_on_archive_enabled_ir_incident.up.sql | 1 - ..._drop_status_from_ir_status_posts.down.sql | 1 - ...56_drop_status_from_ir_status_posts.up.sql | 1 - ...ate_current_status_in_ir_incident.down.sql | 7 - ...pdate_current_status_in_ir_incident.up.sql | 7 - ..._add_category_name_to_ir_playbook.down.sql | 1 - ...58_add_category_name_to_ir_playbook.up.sql | 5 - ..._add_category_name_to_ir_incident.down.sql | 1 - ...59_add_category_name_to_ir_incident.up.sql | 5 - ...adcast_channel_ids_to_ir_incident.down.sql | 1 - ...roadcast_channel_ids_to_ir_incident.up.sql | 12 - ...adcast_channel_ids_to_ir_playbook.down.sql | 2 - ...roadcast_channel_ids_to_ir_playbook.up.sql | 18 - ...nnel_id_to_root_id_to_ir_incident.down.sql | 1 - ...hannel_id_to_root_id_to_ir_incident.up.sql | 1 - ...0063_convert_charset_of_ir_system.down.sql | 1 - ...000063_convert_charset_of_ir_system.up.sql | 1 - ...064_add_pr_key_ir_playbook_member.down.sql | 13 - ...00064_add_pr_key_ir_playbook_member.up.sql | 13 - .../000065_drop_index_posts_unique.down.sql | 0 .../000065_drop_index_posts_unique.up.sql | 0 ...000066_add_pr_key_ir_status_posts.down.sql | 13 - .../000066_add_pr_key_ir_status_posts.up.sql | 13 - ...0067_add_pr_key_ir_timeline_event.down.sql | 13 - ...000067_add_pr_key_ir_timeline_event.up.sql | 13 - ...r_viewed_channel_channelid_userid.down.sql | 0 ..._ir_viewed_channel_channelid_userid.up.sql | 0 ...0069_add_pr_key_ir_viewed_channel.down.sql | 13 - ...000069_add_pr_key_ir_viewed_channel.up.sql | 13 - ...ugin_id_in_plugin_key_value_store.down.sql | 4 - ...plugin_id_in_plugin_key_value_store.up.sql | 4 - ...er_default_seconds_to_ir_incident.down.sql | 1 - ...imer_default_seconds_to_ir_incident.up.sql | 1 - ...bhook_on_creation_url_ir_playbook.down.sql | 1 - ...webhook_on_creation_url_ir_playbook.up.sql | 1 - ..._on_status_update_url_ir_playbook.down.sql | 1 - ...ok_on_status_update_url_ir_playbook.up.sql | 1 - ...bhook_on_creation_url_ir_incident.down.sql | 1 - ...webhook_on_creation_url_ir_incident.up.sql | 1 - ..._on_status_update_url_ir_incident.down.sql | 1 - ...ok_on_status_update_url_ir_incident.up.sql | 1 - .../000076_create_ir_userinfo.down.sql | 1 - .../postgres/000076_create_ir_userinfo.up.sql | 4 - ...tion_settings_json_to_ir_userinfo.down.sql | 1 - ...cation_settings_json_to_ir_userinfo.up.sql | 1 - ...ndex_ir_statusposts_posts_unique2.down.sql | 0 ..._index_ir_statusposts_posts_unique2.up.sql | 0 ..._viewed_channel_channelid_userid2.down.sql | 8 - ...ir_viewed_channel_channelid_userid2.up.sql | 1 - .../server/sqlstore/migrations_test.go | 827 -- .../server/sqlstore/migrations_tests_utils.go | 44 - .../server/sqlstore/migrations_utils.go | 357 - .../sqlstore/mockmocks/mock_storeapi.go | 67 - .../sqlstore/mocks/mock_configurationapi.go | 52 - .../server/sqlstore/mocks/mock_kvapi.go | 51 - .../server/sqlstore/mocks/mock_storeapi.go | 67 - server/playbooks/server/sqlstore/playbook.go | 1232 --- .../playbooks/server/sqlstore/playbook_run.go | 1751 ---- .../server/sqlstore/playbook_run_test.go | 1682 ---- .../server/sqlstore/playbook_test.go | 2041 ---- .../server/sqlstore/pluginapi_client.go | 47 - server/playbooks/server/sqlstore/stats.go | 614 -- .../server/sqlstore/stats_for_test.go | 49 - .../playbooks/server/sqlstore/stats_test.go | 729 -- server/playbooks/server/sqlstore/store.go | 134 - .../playbooks/server/sqlstore/store_test.go | 879 -- .../server/sqlstore/support_for_test.go | 841 -- server/playbooks/server/sqlstore/system.go | 73 - .../server/sqlstore/timeline_event_test.go | 199 - server/playbooks/server/sqlstore/user_info.go | 117 - .../server/sqlstore/user_info_test.go | 192 - server/playbooks/server/sqlstore/versions.go | 38 - server/playbooks/server/telemetry/noop.go | 208 - server/playbooks/server/telemetry/rudder.go | 728 -- .../playbooks/server/telemetry/rudder_test.go | 559 -- .../playbooks/server/timeutils/timeutils.go | 74 - .../server/timeutils/timeutils_test.go | 132 - server/public/model/config.go | 9 +- server/public/plugin/environment.go | 1 - webapp/Makefile | 11 +- webapp/channels/src/plugins/products.ts | 17 +- webapp/channels/webpack.config.js | 21 +- webapp/package-lock.json | 8925 +---------------- webapp/package.json | 3 +- webapp/platform/types/src/config.ts | 2 - webapp/playbooks/.eslintrc.json | 688 -- webapp/playbooks/.npmrc | 1 - webapp/playbooks/babel.config.js | 56 - webapp/playbooks/graphql_gen.ts | 22 - webapp/playbooks/i18n/cs.json | 33 - webapp/playbooks/i18n/de.json | 853 -- webapp/playbooks/i18n/en.json | 608 -- webapp/playbooks/i18n/en_AU.json | 838 -- webapp/playbooks/i18n/es.json | 453 - webapp/playbooks/i18n/fa.json | 459 - webapp/playbooks/i18n/fr.json | 513 - webapp/playbooks/i18n/hr.json | 841 -- webapp/playbooks/i18n/hu.json | 729 -- webapp/playbooks/i18n/ja.json | 836 -- webapp/playbooks/i18n/kk.json | 453 - webapp/playbooks/i18n/ko.json | 768 -- webapp/playbooks/i18n/ml.json | 459 - webapp/playbooks/i18n/nl.json | 845 -- webapp/playbooks/i18n/pl.json | 840 -- webapp/playbooks/i18n/ru.json | 523 - webapp/playbooks/i18n/sl.json | 444 - webapp/playbooks/i18n/sv.json | 797 -- webapp/playbooks/i18n/tr.json | 765 -- webapp/playbooks/i18n/zh_Hans.json | 1 - webapp/playbooks/i18n/zh_Hant.json | 1 - webapp/playbooks/jest.config.js | 18 - webapp/playbooks/package.json | 108 - webapp/playbooks/rudder_transform.js | 57 - webapp/playbooks/scripts/deploy.js | 16 - webapp/playbooks/src/actions.ts | 371 - webapp/playbooks/src/browser_routing.ts | 45 - webapp/playbooks/src/client.ts | 813 -- .../formatted_duration.test.tsx.snap | 33 - .../src/components/actions_modal.tsx | 156 - .../src/components/actions_modal_action.tsx | 70 - .../actions_modal_action_children.tsx | 106 - .../src/components/actions_modal_trigger.tsx | 109 - .../src/components/assets/app-bar-icon.png | Bin 5005 -> 0 bytes .../src/components/assets/buttons.tsx | 246 - .../src/components/assets/error_svg.tsx | 130 - .../src/components/assets/files_overlay.png | Bin 4059 -> 0 bytes .../components/assets/icons/clear_icon.tsx | 8 - .../assets/icons/clipboards_checkmark.tsx | 30 - .../assets/icons/clipboards_play.tsx | 30 - .../src/components/assets/icons/clock.tsx | 31 - .../components/assets/icons/exclamation.tsx | 32 - .../components/assets/icons/external_link.tsx | 32 - .../components/assets/icons/left_chevron.tsx | 22 - .../assets/icons/playbooks_product_icon.tsx | 23 - .../assets/icons/post_menu_icon.tsx | 14 - .../src/components/assets/icons/profiles.tsx | 30 - .../assets/icons/three_dots_icon.tsx | 16 - .../components/assets/icons/warning_icon.tsx | 13 - .../assets/illustrations/bug_search_svg.tsx | 46 - .../illustrations/clipboard_checklist_svg.tsx | 68 - .../illustrations/dumpster_fire_svg.tsx | 76 - .../assets/illustrations/gears_svg.tsx | 107 - .../assets/illustrations/handshake_svg.tsx | 67 - .../assets/illustrations/light_bulb_svg.tsx | 63 - .../assets/illustrations/rocket_man_svg.tsx | 440 - .../assets/illustrations/rocket_svg.tsx | 133 - .../assets/illustrations/search_svg.tsx | 42 - .../illustrations/smiley_sunglasses_svg.tsx | 91 - .../src/components/assets/inputs.tsx | 49 - .../src/components/assets/loading_spinner.tsx | 79 - .../components/assets/mattermost_logo_svg.tsx | 40 - .../assets/no_content_playbook_runs_svg.tsx | 261 - .../src/components/assets/no_metrics_svg.tsx | 309 - .../assets/page_run_collaboration_svg.tsx | 321 - .../src/components/assets/success_svg.tsx | 135 - .../playbooks/src/components/assets/svg.tsx | 13 - .../assets/upgrade_error_illustration_svg.tsx | 129 - .../assets/upgrade_illustration_svg.tsx | 345 - .../upgrade_key_metrics_background_svg.tsx | 1240 --- .../upgrade_playbook_background_svg.tsx | 862 -- .../upgrade_success_illustration_svg.tsx | 130 - .../backstage/archive_playbook_modal.tsx | 69 - .../src/components/backstage/backstage.tsx | 81 - .../backstage/backstage_list_header.tsx | 19 - .../src/components/backstage/bar_graph.tsx | 137 - .../backstage/category_selector.tsx | 68 - .../components/backstage/channel_selector.tsx | 194 - .../backstage/convert_enterprise_notice.tsx | 40 - .../convert_private_playbook_modal.tsx | 40 - .../src/components/backstage/css_utils.tsx | 221 - .../backstage/file_drag_detection.tsx | 44 - .../backstage/file_upload_overlay.tsx | 53 - .../components/backstage/follow_button.tsx | 103 - .../components/backstage/import_playbook.tsx | 91 - .../components/backstage/lhs_navigation.tsx | 35 - .../backstage/lhs_playbook_dot_menu.tsx | 41 - .../components/backstage/lhs_run_dot_menu.tsx | 111 - .../src/components/backstage/line_graph.tsx | 128 - .../src/components/backstage/main_body.tsx | 136 - .../backstage/metrics/metrics_card.tsx | 224 - .../backstage/metrics/metrics_row.tsx | 122 - .../backstage/metrics/metrics_run_list.tsx | 133 - .../metrics/metrics_run_list_header.tsx | 83 - .../backstage/metrics/metrics_stats_view.tsx | 87 - .../metrics/no_metrics_placeholder.tsx | 68 - .../metrics/playbook_key_metrics.tsx | 119 - .../upgrade_key_metrics_placeholder.tsx | 27 - .../backstage/playbook_access_modal.tsx | 192 - .../automation/assign_owner_selector.tsx | 185 - .../automation/auto_assign_owner.tsx | 47 - .../automation/channel_access.tsx | 237 - .../automation/clear_indicator.tsx | 11 - .../playbook_edit/automation/invite_users.tsx | 110 - .../automation/invite_users_selector.tsx | 283 - .../playbook_edit/automation/menu_list.tsx | 47 - .../automation/patterned_input.tsx | 74 - .../playbook_edit/automation/styles.tsx | 35 - .../playbook_edit/automation/toggle.tsx | 90 - .../automation/webhook_setting.tsx | 49 - .../playbook_edit/metrics/metric_edit.tsx | 229 - .../playbook_edit/metrics/metric_view.tsx | 151 - .../playbook_edit/metrics/metrics.tsx | 360 - .../backstage/playbook_edit/metrics/shared.ts | 60 - .../backstage/playbook_edit/styles.tsx | 44 - .../backstage/playbook_editor/controls.tsx | 592 -- .../inputs/broadcast_channels_selector.tsx | 191 - .../retrospective_interval_selector.tsx | 48 - .../outline/inputs/update_timer_selector.tsx | 62 - .../outline/inputs/webhooks_input.tsx | 187 - .../playbook_editor/outline/outline.tsx | 223 - .../playbook_editor/outline/scroll_nav.tsx | 207 - .../playbook_editor/outline/section.tsx | 115 - .../outline/section_actions.tsx | 292 - .../outline/section_retrospective.tsx | 126 - .../outline/section_status_updates.tsx | 204 - .../playbook_editor/playbook_editor.tsx | 599 -- .../components/backstage/playbook_list.tsx | 430 - .../playbook_list_getting_started.tsx | 97 - .../backstage/playbook_list_row.tsx | 353 - .../playbook_run/add_participant_modal.tsx | 232 - .../playbook_run/become_participant_modal.tsx | 191 - .../playbook_runs/playbook_run/checklists.tsx | 39 - .../playbook_run/context_menu.tsx | 184 - .../playbook_runs/playbook_run/controls.tsx | 228 - .../enable_disable_run_status_update.tsx | 33 - .../playbook_runs/playbook_run/finish_run.tsx | 151 - .../playbook_runs/playbook_run/following.tsx | 58 - .../playbook_runs/playbook_run/header.tsx | 205 - .../playbook_run/header_button.tsx | 97 - .../playbook_run/metrics/metric_input.tsx | 150 - .../playbook_run/metrics/metrics_data.tsx | 144 - .../playbook_run/playbook_run.tsx | 318 - .../playbook_run/restore_run.tsx | 36 - .../playbook_run/retrospective.tsx | 226 - .../playbook_run/retrospective/report.tsx | 57 - .../retrospective/report_text_area.tsx | 92 - .../retrospective/timeline_event_item.tsx | 425 - .../playbook_runs/playbook_run/rhs.tsx | 153 - .../playbook_runs/playbook_run/rhs_info.tsx | 70 - .../playbook_run/rhs_info_activity.tsx | 72 - .../playbook_run/rhs_info_metrics.tsx | 148 - .../playbook_run/rhs_info_overview.tsx | 328 - .../playbook_run/rhs_info_styles.tsx | 91 - .../playbook_run/rhs_participants.tsx | 371 - .../playbook_run/rhs_status_updates.tsx | 43 - .../playbook_run/rhs_timeline.tsx | 180 - .../playbook_run/send_message_button.tsx | 42 - .../playbook_run/status_update.tsx | 474 - .../playbook_runs/playbook_run/summary.tsx | 94 - .../playbook_run/timeline_utils.ts | 153 - .../playbook_run/update_card.tsx | 114 - .../backstage/playbook_runs/shared.tsx | 121 - .../components/backstage/playbook_usage.tsx | 104 - .../backstage/profile_autocomplete.tsx | 202 - .../backstage/public_private_selector.tsx | 168 - .../backstage/restore_playbook_modal.tsx | 71 - .../src/components/backstage/rhs/rhs.tsx | 118 - .../backstage/rhs/task_inbox/task.tsx | 161 - .../backstage/rhs/task_inbox/task_inbox.tsx | 336 - .../backstage/route_leaving_guard.tsx | 64 - .../backstage/runs_list/checkbox_input.tsx | 83 - .../backstage/runs_list/filters.tsx | 195 - .../backstage/runs_list/playbook_selector.tsx | 195 - .../components/backstage/runs_list/row.tsx | 255 - .../backstage/runs_list/run_list_header.tsx | 89 - .../backstage/runs_list/runs_list.tsx | 133 - .../src/components/backstage/runs_page.tsx | 112 - .../backstage/runs_page_no_content.tsx | 123 - .../src/components/backstage/search_input.tsx | 112 - .../backstage/select_users_below.tsx | 204 - .../src/components/backstage/shared.tsx | 41 - .../backstage/start_trial_notice.tsx | 30 - .../src/components/backstage/stats_view.tsx | 296 - .../src/components/backstage/status_badge.tsx | 88 - .../src/components/backstage/styles.tsx | 254 - .../src/components/backstage/toast.tsx | 126 - .../src/components/backstage/toast_banner.tsx | 156 - .../components/backstage/upgrade_modal.tsx | 143 - .../backstage/upgrade_modal_data.tsx | 215 - .../backstage/upgrade_modal_footer.tsx | 57 - .../backstage/upgrade_modal_header.tsx | 50 - .../backstage/upgrade_modal_illustration.tsx | 41 - .../upgrade_playbook_placeholder.tsx | 27 - .../components/broadcast_channel_selector.tsx | 88 - .../src/components/channel_actions_modal.tsx | 223 - .../src/components/channel_header.tsx | 32 - .../components/checklist/checklist_list.tsx | 447 - .../checklist/collapsible_checklist.tsx | 338 - .../collapsible_checklist_hover_menu.tsx | 156 - .../checklist/generic_checklist.tsx | 202 - .../components/checklist_item/assign_to.tsx | 240 - .../checklist_item/checklist_item.tsx | 607 -- .../checklist_item_draggable.tsx | 62 - .../src/components/checklist_item/command.tsx | 276 - .../components/checklist_item/description.tsx | 121 - .../src/components/checklist_item/duedate.tsx | 461 - .../components/checklist_item/hover_menu.tsx | 174 - .../src/components/checklist_item/inputs.tsx | 154 - .../checklist_item/task_actions.tsx | 92 - .../checklist_item/task_actions_modal.tsx | 232 - .../src/components/checklist_item/title.tsx | 137 - .../playbooks/src/components/cloud_modal.tsx | 26 - .../src/components/cloud_upgrade_post.tsx | 91 - .../src/components/command_input.tsx | 115 - .../src/components/create_playbook_modal.tsx | 101 - .../src/components/custom_post_styles.tsx | 30 - .../src/components/datetime_input.tsx | 187 - .../src/components/datetime_parsing.test.ts | 87 - .../src/components/datetime_parsing.ts | 153 - .../src/components/datetime_selector.tsx | 233 - webapp/playbooks/src/components/dot_menu.tsx | 234 - webapp/playbooks/src/components/dropdown.tsx | 132 - .../playbooks/src/components/error_page.tsx | 67 - .../components/formatted_duration.test.tsx | 92 - .../src/components/formatted_duration.tsx | 65 - .../src/components/formatted_markdown.tsx | 51 - .../src/components/give_feedback_button.tsx | 58 - .../src/components/global_header_right.tsx | 99 - .../src/components/keywords_selector.tsx | 144 - .../playbooks/src/components/login_hook.tsx | 35 - .../src/components/markdown_edit.tsx | 206 - .../src/components/markdown_textbox.tsx | 262 - .../components/modals/run_playbook_modal.tsx | 530 - .../components/modals/run_update_channel.tsx | 139 - .../src/components/modals/run_update_name.tsx | 107 - .../modals/update_run_status_modal.tsx | 520 - .../src/components/multi_checkbox.tsx | 112 - .../src/components/pagination_row.tsx | 87 - .../src/components/patterned_text_area.tsx | 132 - .../src/components/playbook_actions_modal.tsx | 105 - .../src/components/playbooks_selector.tsx | 332 - webapp/playbooks/src/components/post_menu.tsx | 109 - .../src/components/post_menu_modal.tsx | 28 - webapp/playbooks/src/components/post_text.tsx | 47 - .../src/components/profile/profile.tsx | 114 - .../src/components/profile/profile_button.tsx | 123 - .../components/profile/profile_selector.tsx | 352 - .../src/components/retrospective_post.tsx | 177 - .../retrospective_reminder_posts.tsx | 452 - .../src/components/rhs/rhs_about.tsx | 245 - .../src/components/rhs/rhs_about_buttons.tsx | 138 - .../components/rhs/rhs_about_description.tsx | 159 - .../src/components/rhs/rhs_about_title.tsx | 198 - .../src/components/rhs/rhs_checklist_list.tsx | 466 - .../playbooks/src/components/rhs/rhs_home.tsx | 200 - .../src/components/rhs/rhs_home_item.tsx | 199 - .../playbooks/src/components/rhs/rhs_main.tsx | 280 - .../src/components/rhs/rhs_participant.tsx | 107 - .../src/components/rhs/rhs_participants.tsx | 223 - .../src/components/rhs/rhs_post_update.tsx | 234 - .../components/rhs/rhs_post_update_button.tsx | 64 - .../src/components/rhs/rhs_run_details.tsx | 205 - .../components/rhs/rhs_run_details_title.tsx | 100 - .../rhs/rhs_run_details_tour_dialog.tsx | 146 - .../src/components/rhs/rhs_run_list.tsx | 922 -- .../components/rhs/rhs_run_participants.tsx | 44 - .../rhs/rhs_run_participants_title.tsx | 49 - .../src/components/rhs/rhs_shared.tsx | 87 - .../src/components/rhs/rhs_title_common.tsx | 79 - .../src/components/run_actions_modal.tsx | 190 - .../sidebar/create_playbook_dropdown.tsx | 180 - .../src/components/sidebar/group.tsx | 134 - .../playbooks/src/components/sidebar/item.tsx | 151 - .../components/sidebar/playbooks_sidebar.tsx | 252 - .../src/components/sidebar/sidebar.tsx | 128 - .../src/components/sortable_col_header.tsx | 51 - .../src/components/team/team_selector.tsx | 89 - .../components/templates/template_data.tsx | 787 -- .../components/templates/template_item.tsx | 164 - .../templates/template_selector.tsx | 109 - webapp/playbooks/src/components/text_edit.tsx | 166 - webapp/playbooks/src/components/time_spec.tsx | 21 - .../src/components/tutorial/tours.ts | 58 - .../tutorial/tutorial_tour_tip/backdrop.tsx | 53 - .../tutorial/tutorial_tour_tip/dot.scss | 37 - .../tutorial/tutorial_tour_tip/dot.tsx | 43 - .../tutorial/tutorial_tour_tip/hooks.ts | 118 - .../tutorial/tutorial_tour_tip/index.ts | 5 - .../tutorial/tutorial_tour_tip/manager.ts | 220 - .../tutorial_tour_tip/tutorial_tour_tip.scss | 365 - .../tutorial_tour_tip/tutorial_tour_tip.tsx | 269 - .../playbooks/src/components/update_post.tsx | 182 - .../src/components/update_request_post.tsx | 248 - .../src/components/upgrade_banner.tsx | 284 - .../widgets/conditional_tooltip.tsx | 53 - .../components/widgets/confirmation_modal.tsx | 271 - .../widgets/confirmation_modal_light.tsx | 84 - .../src/components/widgets/copy_link.tsx | 96 - .../src/components/widgets/generic_modal.tsx | 274 - .../src/components/widgets/header.tsx | 73 - .../src/components/widgets/menu/menu.tsx | 83 - .../components/widgets/menu/menu_group.tsx | 33 - .../src/components/widgets/menu/menu_item.tsx | 85 - .../components/widgets/menu/menu_wrapper.tsx | 122 - .../playbooks/src/components/widgets/pill.tsx | 47 - .../src/components/widgets/show_more.tsx | 215 - .../components/widgets/text_with_tooltip.tsx | 75 - .../text_with_tooltip_when_ellipsis.tsx | 77 - .../src/components/widgets/tooltip.tsx | 48 - .../widgets/unsaved_changes_modal.tsx | 38 - webapp/playbooks/src/constants.ts | 36 - webapp/playbooks/src/graphql/apollo.tsx | 51 - .../src/graphql/generated/fragment-masking.ts | 50 - webapp/playbooks/src/graphql/generated/gql.ts | 92 - .../src/graphql/generated/graphql.ts | 619 -- .../playbooks/src/graphql/generated/index.ts | 2 - webapp/playbooks/src/graphql/hooks.ts | 196 - webapp/playbooks/src/graphql/playbook.graphql | 93 - webapp/playbooks/src/graphql/runs.graphql | 24 - webapp/playbooks/src/graphql_client.ts | 13 - webapp/playbooks/src/hooks/crud.ts | 218 - webapp/playbooks/src/hooks/general.test.ts | 400 - webapp/playbooks/src/hooks/general.ts | 683 -- webapp/playbooks/src/hooks/index.ts | 7 - webapp/playbooks/src/hooks/license.ts | 56 - webapp/playbooks/src/hooks/permissions.ts | 83 - webapp/playbooks/src/hooks/routing.ts | 123 - webapp/playbooks/src/hooks/run.tsx | 65 - webapp/playbooks/src/hooks/telemetry.ts | 28 - webapp/playbooks/src/index.tsx | 340 - webapp/playbooks/src/license.test.ts | 161 - webapp/playbooks/src/license.ts | 84 - webapp/playbooks/src/manifest.js | 2 - webapp/playbooks/src/mobile.ts | 5 - webapp/playbooks/src/reducer.test.ts | 64 - webapp/playbooks/src/reducer.ts | 425 - webapp/playbooks/src/remote_entry.ts | 5 - webapp/playbooks/src/rhs_opener.ts | 112 - .../playbooks/src/rhs_title_remote_render.tsx | 30 - webapp/playbooks/src/selectors.ts | 324 - webapp/playbooks/src/slash_command.test.ts | 27 - webapp/playbooks/src/slash_command.ts | 79 - webapp/playbooks/src/styles/headings.tsx | 11 - webapp/playbooks/src/types/actions.ts | 203 - webapp/playbooks/src/types/assets.d.ts | 14 - webapp/playbooks/src/types/backstage.ts | 19 - webapp/playbooks/src/types/backstage_rhs.ts | 8 - webapp/playbooks/src/types/category.ts | 27 - webapp/playbooks/src/types/channel_actions.ts | 41 - webapp/playbooks/src/types/compass.tsx | 7 - webapp/playbooks/src/types/global.d.ts | 4 - webapp/playbooks/src/types/insights.ts | 11 - .../src/types/js-trim-multiline-string.d.ts | 1 - webapp/playbooks/src/types/permissions.ts | 55 - webapp/playbooks/src/types/playbook.ts | 300 - webapp/playbooks/src/types/playbook_run.ts | 150 - webapp/playbooks/src/types/rhs.ts | 80 - webapp/playbooks/src/types/settings.ts | 27 - webapp/playbooks/src/types/stats.ts | 47 - webapp/playbooks/src/types/store.ts | 13 - webapp/playbooks/src/types/telemetry.ts | 49 - webapp/playbooks/src/types/type_utils.d.ts | 6 - .../playbooks/src/types/websocket_events.ts | 10 - webapp/playbooks/src/utils.test.ts | 22 - webapp/playbooks/src/utils.ts | 134 - webapp/playbooks/src/webapp_globals.ts | 26 - webapp/playbooks/src/websocket_events.test.ts | 55 - webapp/playbooks/src/websocket_events.ts | 228 - webapp/playbooks/tsconfig.json | 38 - webapp/playbooks/webpack.config.js | 135 - webapp/scripts/build.js | 14 - webapp/scripts/dev-server.js | 1 - webapp/scripts/run.js | 1 - 879 files changed, 66 insertions(+), 127144 deletions(-) delete mode 100644 server/playbooks/client/LICENSE delete mode 100644 server/playbooks/client/action.go delete mode 100644 server/playbooks/client/actions.go delete mode 100644 server/playbooks/client/client.go delete mode 100644 server/playbooks/client/doc.go delete mode 100644 server/playbooks/client/doc_test.go delete mode 100644 server/playbooks/client/error_response.go delete mode 100644 server/playbooks/client/playbook.go delete mode 100644 server/playbooks/client/playbook_run.go delete mode 100644 server/playbooks/client/playbook_runs.go delete mode 100644 server/playbooks/client/playbook_runs_test.go delete mode 100644 server/playbooks/client/playbooks.go delete mode 100644 server/playbooks/client/playbooks_test.go delete mode 100644 server/playbooks/client/reminder.go delete mode 100644 server/playbooks/client/reminders.go delete mode 100644 server/playbooks/client/settings.go delete mode 100644 server/playbooks/client/stats.go delete mode 100644 server/playbooks/client/telemetry.go delete mode 100644 server/playbooks/client/unexport_test.go delete mode 100644 server/playbooks/product/api_adapter.go delete mode 100644 server/playbooks/product/imports/playbooks_imports.go delete mode 100644 server/playbooks/product/logrus.go delete mode 100644 server/playbooks/product/playbooks_product.go delete mode 100644 server/playbooks/product/pluginapi/cluster/job.go delete mode 100644 server/playbooks/product/pluginapi/cluster/job_once.go delete mode 100644 server/playbooks/product/pluginapi/cluster/job_once_scheduler.go delete mode 100644 server/playbooks/product/pluginapi/cluster/mutex.go delete mode 100644 server/playbooks/product/pluginapi/cluster/wait.go delete mode 100644 server/playbooks/product/pluginapi/license.go delete mode 100644 server/playbooks/server/.gitignore delete mode 100644 server/playbooks/server/api/actions.go delete mode 100644 server/playbooks/server/api/api.go delete mode 100644 server/playbooks/server/api/api.yaml delete mode 100644 server/playbooks/server/api/bot.go delete mode 100644 server/playbooks/server/api/categories.go delete mode 100644 server/playbooks/server/api/context.go delete mode 100644 server/playbooks/server/api/error_handler.go delete mode 100644 server/playbooks/server/api/graph_dataloader.go delete mode 100644 server/playbooks/server/api/graphql.go delete mode 100644 server/playbooks/server/api/graphql_loader_favorite.go delete mode 100644 server/playbooks/server/api/graphql_loader_playbook.go delete mode 100644 server/playbooks/server/api/graphql_playbook.go delete mode 100644 server/playbooks/server/api/graphql_root.go delete mode 100644 server/playbooks/server/api/graphql_root_playbook.go delete mode 100644 server/playbooks/server/api/graphql_root_run.go delete mode 100644 server/playbooks/server/api/graphql_run.go delete mode 100644 server/playbooks/server/api/graphqli.html delete mode 100644 server/playbooks/server/api/logger.go delete mode 100644 server/playbooks/server/api/playbook_runs.go delete mode 100644 server/playbooks/server/api/playbooks.go delete mode 100644 server/playbooks/server/api/schema.graphqls delete mode 100644 server/playbooks/server/api/settings.go delete mode 100644 server/playbooks/server/api/signal.go delete mode 100644 server/playbooks/server/api/stats.go delete mode 100644 server/playbooks/server/api/telemetry.go delete mode 100644 server/playbooks/server/api/urls.go delete mode 100644 server/playbooks/server/api_actions_test.go delete mode 100644 server/playbooks/server/api_bot_test.go delete mode 100644 server/playbooks/server/api_general_test.go delete mode 100644 server/playbooks/server/api_graphql_playbooks_test.go delete mode 100644 server/playbooks/server/api_graphql_runs_test.go delete mode 100644 server/playbooks/server/api_playbooks_test.go delete mode 100644 server/playbooks/server/api_runs_test.go delete mode 100644 server/playbooks/server/api_settings_test.go delete mode 100644 server/playbooks/server/api_stats_test.go delete mode 100644 server/playbooks/server/api_telemetry_test.go delete mode 100644 server/playbooks/server/app/action.go delete mode 100644 server/playbooks/server/app/actions_service.go delete mode 100644 server/playbooks/server/app/category.go delete mode 100644 server/playbooks/server/app/category_service.go delete mode 100644 server/playbooks/server/app/errors.go delete mode 100644 server/playbooks/server/app/export.go delete mode 100644 server/playbooks/server/app/export_test.go delete mode 100644 server/playbooks/server/app/keywords_ignore.go delete mode 100644 server/playbooks/server/app/mocks/mock_job_once_scheduler.go delete mode 100644 server/playbooks/server/app/permissions_service.go delete mode 100644 server/playbooks/server/app/playbook.go delete mode 100644 server/playbooks/server/app/playbook_run.go delete mode 100644 server/playbooks/server/app/playbook_run_service.go delete mode 100644 server/playbooks/server/app/playbook_run_test.go delete mode 100644 server/playbooks/server/app/playbook_service.go delete mode 100644 server/playbooks/server/app/playbook_test.go delete mode 100644 server/playbooks/server/app/plugin_api_tools.go delete mode 100644 server/playbooks/server/app/regular_digest_service.go delete mode 100644 server/playbooks/server/app/regular_digest_service_test.go delete mode 100644 server/playbooks/server/app/reminder.go delete mode 100644 server/playbooks/server/app/sort.go delete mode 100644 server/playbooks/server/app/task_actions.go delete mode 100644 server/playbooks/server/app/task_actions_test.go delete mode 100644 server/playbooks/server/app/telemetry.go delete mode 100644 server/playbooks/server/app/urls.go delete mode 100644 server/playbooks/server/app/urls_test.go delete mode 100644 server/playbooks/server/app/user_info.go delete mode 100644 server/playbooks/server/app/variables.go delete mode 100644 server/playbooks/server/app/variables_test.go delete mode 100644 server/playbooks/server/bot/bot.go delete mode 100644 server/playbooks/server/bot/mocks/mock_poster.go delete mode 100644 server/playbooks/server/bot/poster.go delete mode 100644 server/playbooks/server/command/command.go delete mode 100644 server/playbooks/server/config/config.go delete mode 100644 server/playbooks/server/config/configuration.go delete mode 100644 server/playbooks/server/config/service.go delete mode 100644 server/playbooks/server/e2etest.config.json.sample delete mode 100644 server/playbooks/server/enterprise/LICENSE delete mode 100644 server/playbooks/server/enterprise/license.go delete mode 100644 server/playbooks/server/httptools/client.go delete mode 100644 server/playbooks/server/main_test.go delete mode 100644 server/playbooks/server/metrics/metrics.go delete mode 100644 server/playbooks/server/metrics/service.go delete mode 100644 server/playbooks/server/playbooks/service_api.go delete mode 100644 server/playbooks/server/scheduler/scheduler.go delete mode 100644 server/playbooks/server/sqlstore/actions.go delete mode 100644 server/playbooks/server/sqlstore/actions_test.go delete mode 100644 server/playbooks/server/sqlstore/category.go delete mode 100644 server/playbooks/server/sqlstore/category_test.go delete mode 100644 server/playbooks/server/sqlstore/migrate.go delete mode 100644 server/playbooks/server/sqlstore/migrations.go delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.59.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.59.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.60.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.60.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.61.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.61.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.62.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.62.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.63.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/mysql/0.63.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.59.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.59.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.60.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.60.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.61.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.61.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.62.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.62.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.63.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/future/postgres/0.63.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000065_drop_index_posts_unique.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000065_drop_index_posts_unique.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000078_drop_index_ir_statusposts_posts_unique2.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000078_drop_index_ir_statusposts_posts_unique2.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql delete mode 100644 server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql delete mode 100644 server/playbooks/server/sqlstore/migrations_test.go delete mode 100644 server/playbooks/server/sqlstore/migrations_tests_utils.go delete mode 100644 server/playbooks/server/sqlstore/migrations_utils.go delete mode 100644 server/playbooks/server/sqlstore/mockmocks/mock_storeapi.go delete mode 100644 server/playbooks/server/sqlstore/mocks/mock_configurationapi.go delete mode 100644 server/playbooks/server/sqlstore/mocks/mock_kvapi.go delete mode 100644 server/playbooks/server/sqlstore/mocks/mock_storeapi.go delete mode 100644 server/playbooks/server/sqlstore/playbook.go delete mode 100644 server/playbooks/server/sqlstore/playbook_run.go delete mode 100644 server/playbooks/server/sqlstore/playbook_run_test.go delete mode 100644 server/playbooks/server/sqlstore/playbook_test.go delete mode 100644 server/playbooks/server/sqlstore/pluginapi_client.go delete mode 100644 server/playbooks/server/sqlstore/stats.go delete mode 100644 server/playbooks/server/sqlstore/stats_for_test.go delete mode 100644 server/playbooks/server/sqlstore/stats_test.go delete mode 100644 server/playbooks/server/sqlstore/store.go delete mode 100644 server/playbooks/server/sqlstore/store_test.go delete mode 100644 server/playbooks/server/sqlstore/support_for_test.go delete mode 100644 server/playbooks/server/sqlstore/system.go delete mode 100644 server/playbooks/server/sqlstore/timeline_event_test.go delete mode 100644 server/playbooks/server/sqlstore/user_info.go delete mode 100644 server/playbooks/server/sqlstore/user_info_test.go delete mode 100644 server/playbooks/server/sqlstore/versions.go delete mode 100644 server/playbooks/server/telemetry/noop.go delete mode 100644 server/playbooks/server/telemetry/rudder.go delete mode 100644 server/playbooks/server/telemetry/rudder_test.go delete mode 100644 server/playbooks/server/timeutils/timeutils.go delete mode 100644 server/playbooks/server/timeutils/timeutils_test.go delete mode 100644 webapp/playbooks/.eslintrc.json delete mode 100644 webapp/playbooks/.npmrc delete mode 100644 webapp/playbooks/babel.config.js delete mode 100644 webapp/playbooks/graphql_gen.ts delete mode 100644 webapp/playbooks/i18n/cs.json delete mode 100644 webapp/playbooks/i18n/de.json delete mode 100644 webapp/playbooks/i18n/en.json delete mode 100644 webapp/playbooks/i18n/en_AU.json delete mode 100644 webapp/playbooks/i18n/es.json delete mode 100644 webapp/playbooks/i18n/fa.json delete mode 100644 webapp/playbooks/i18n/fr.json delete mode 100644 webapp/playbooks/i18n/hr.json delete mode 100644 webapp/playbooks/i18n/hu.json delete mode 100644 webapp/playbooks/i18n/ja.json delete mode 100644 webapp/playbooks/i18n/kk.json delete mode 100644 webapp/playbooks/i18n/ko.json delete mode 100644 webapp/playbooks/i18n/ml.json delete mode 100644 webapp/playbooks/i18n/nl.json delete mode 100644 webapp/playbooks/i18n/pl.json delete mode 100644 webapp/playbooks/i18n/ru.json delete mode 100644 webapp/playbooks/i18n/sl.json delete mode 100644 webapp/playbooks/i18n/sv.json delete mode 100644 webapp/playbooks/i18n/tr.json delete mode 100644 webapp/playbooks/i18n/zh_Hans.json delete mode 100644 webapp/playbooks/i18n/zh_Hant.json delete mode 100644 webapp/playbooks/jest.config.js delete mode 100644 webapp/playbooks/package.json delete mode 100644 webapp/playbooks/rudder_transform.js delete mode 100644 webapp/playbooks/scripts/deploy.js delete mode 100644 webapp/playbooks/src/actions.ts delete mode 100644 webapp/playbooks/src/browser_routing.ts delete mode 100644 webapp/playbooks/src/client.ts delete mode 100644 webapp/playbooks/src/components/__snapshots__/formatted_duration.test.tsx.snap delete mode 100644 webapp/playbooks/src/components/actions_modal.tsx delete mode 100644 webapp/playbooks/src/components/actions_modal_action.tsx delete mode 100644 webapp/playbooks/src/components/actions_modal_action_children.tsx delete mode 100644 webapp/playbooks/src/components/actions_modal_trigger.tsx delete mode 100644 webapp/playbooks/src/components/assets/app-bar-icon.png delete mode 100644 webapp/playbooks/src/components/assets/buttons.tsx delete mode 100644 webapp/playbooks/src/components/assets/error_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/files_overlay.png delete mode 100644 webapp/playbooks/src/components/assets/icons/clear_icon.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/clipboards_checkmark.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/clipboards_play.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/clock.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/exclamation.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/external_link.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/left_chevron.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/post_menu_icon.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/profiles.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/three_dots_icon.tsx delete mode 100644 webapp/playbooks/src/components/assets/icons/warning_icon.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/bug_search_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/clipboard_checklist_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/dumpster_fire_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/gears_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/handshake_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/light_bulb_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/rocket_man_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/rocket_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/search_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/illustrations/smiley_sunglasses_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/inputs.tsx delete mode 100644 webapp/playbooks/src/components/assets/loading_spinner.tsx delete mode 100644 webapp/playbooks/src/components/assets/mattermost_logo_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/no_content_playbook_runs_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/no_metrics_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/page_run_collaboration_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/success_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/upgrade_error_illustration_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/upgrade_illustration_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/upgrade_key_metrics_background_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/upgrade_playbook_background_svg.tsx delete mode 100644 webapp/playbooks/src/components/assets/upgrade_success_illustration_svg.tsx delete mode 100644 webapp/playbooks/src/components/backstage/archive_playbook_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/backstage.tsx delete mode 100644 webapp/playbooks/src/components/backstage/backstage_list_header.tsx delete mode 100644 webapp/playbooks/src/components/backstage/bar_graph.tsx delete mode 100644 webapp/playbooks/src/components/backstage/category_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/channel_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/convert_enterprise_notice.tsx delete mode 100644 webapp/playbooks/src/components/backstage/convert_private_playbook_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/css_utils.tsx delete mode 100644 webapp/playbooks/src/components/backstage/file_drag_detection.tsx delete mode 100644 webapp/playbooks/src/components/backstage/file_upload_overlay.tsx delete mode 100644 webapp/playbooks/src/components/backstage/follow_button.tsx delete mode 100644 webapp/playbooks/src/components/backstage/import_playbook.tsx delete mode 100644 webapp/playbooks/src/components/backstage/lhs_navigation.tsx delete mode 100644 webapp/playbooks/src/components/backstage/lhs_playbook_dot_menu.tsx delete mode 100644 webapp/playbooks/src/components/backstage/lhs_run_dot_menu.tsx delete mode 100644 webapp/playbooks/src/components/backstage/line_graph.tsx delete mode 100644 webapp/playbooks/src/components/backstage/main_body.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/metrics_card.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/metrics_row.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/metrics_run_list.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/metrics_run_list_header.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/metrics_stats_view.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/no_metrics_placeholder.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/playbook_key_metrics.tsx delete mode 100644 webapp/playbooks/src/components/backstage/metrics/upgrade_key_metrics_placeholder.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_access_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/assign_owner_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/auto_assign_owner.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/channel_access.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/clear_indicator.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/menu_list.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/patterned_input.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/styles.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/toggle.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/automation/webhook_setting.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_edit.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_view.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/metrics/metrics.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/metrics/shared.ts delete mode 100644 webapp/playbooks/src/components/backstage/playbook_edit/styles.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/controls.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/broadcast_channels_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/retrospective_interval_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/update_timer_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/webhooks_input.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/outline.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/scroll_nav.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/section.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/section_actions.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/section_retrospective.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/outline/section_status_updates.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_editor/playbook_editor.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_list.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_list_getting_started.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_list_row.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/add_participant_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/become_participant_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/checklists.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/context_menu.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/controls.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/enable_disable_run_status_update.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/finish_run.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/following.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/header.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/header_button.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/metrics/metric_input.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/metrics/metrics_data.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/playbook_run.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/restore_run.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/retrospective.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/retrospective/report.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/retrospective/report_text_area.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/retrospective/timeline_event_item.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_info.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_info_activity.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_info_metrics.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_info_overview.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_info_styles.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_participants.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_status_updates.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/rhs_timeline.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/send_message_button.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/status_update.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/summary.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/timeline_utils.ts delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/playbook_run/update_card.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_runs/shared.tsx delete mode 100644 webapp/playbooks/src/components/backstage/playbook_usage.tsx delete mode 100644 webapp/playbooks/src/components/backstage/profile_autocomplete.tsx delete mode 100644 webapp/playbooks/src/components/backstage/public_private_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/restore_playbook_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/rhs/rhs.tsx delete mode 100644 webapp/playbooks/src/components/backstage/rhs/task_inbox/task.tsx delete mode 100644 webapp/playbooks/src/components/backstage/rhs/task_inbox/task_inbox.tsx delete mode 100644 webapp/playbooks/src/components/backstage/route_leaving_guard.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_list/checkbox_input.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_list/filters.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_list/playbook_selector.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_list/row.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_list/run_list_header.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_list/runs_list.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_page.tsx delete mode 100644 webapp/playbooks/src/components/backstage/runs_page_no_content.tsx delete mode 100644 webapp/playbooks/src/components/backstage/search_input.tsx delete mode 100644 webapp/playbooks/src/components/backstage/select_users_below.tsx delete mode 100644 webapp/playbooks/src/components/backstage/shared.tsx delete mode 100644 webapp/playbooks/src/components/backstage/start_trial_notice.tsx delete mode 100644 webapp/playbooks/src/components/backstage/stats_view.tsx delete mode 100644 webapp/playbooks/src/components/backstage/status_badge.tsx delete mode 100644 webapp/playbooks/src/components/backstage/styles.tsx delete mode 100644 webapp/playbooks/src/components/backstage/toast.tsx delete mode 100644 webapp/playbooks/src/components/backstage/toast_banner.tsx delete mode 100644 webapp/playbooks/src/components/backstage/upgrade_modal.tsx delete mode 100644 webapp/playbooks/src/components/backstage/upgrade_modal_data.tsx delete mode 100644 webapp/playbooks/src/components/backstage/upgrade_modal_footer.tsx delete mode 100644 webapp/playbooks/src/components/backstage/upgrade_modal_header.tsx delete mode 100644 webapp/playbooks/src/components/backstage/upgrade_modal_illustration.tsx delete mode 100644 webapp/playbooks/src/components/backstage/upgrade_playbook_placeholder.tsx delete mode 100644 webapp/playbooks/src/components/broadcast_channel_selector.tsx delete mode 100644 webapp/playbooks/src/components/channel_actions_modal.tsx delete mode 100644 webapp/playbooks/src/components/channel_header.tsx delete mode 100644 webapp/playbooks/src/components/checklist/checklist_list.tsx delete mode 100644 webapp/playbooks/src/components/checklist/collapsible_checklist.tsx delete mode 100644 webapp/playbooks/src/components/checklist/collapsible_checklist_hover_menu.tsx delete mode 100644 webapp/playbooks/src/components/checklist/generic_checklist.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/assign_to.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/checklist_item.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/checklist_item_draggable.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/command.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/description.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/duedate.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/hover_menu.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/inputs.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/task_actions.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/task_actions_modal.tsx delete mode 100644 webapp/playbooks/src/components/checklist_item/title.tsx delete mode 100644 webapp/playbooks/src/components/cloud_modal.tsx delete mode 100644 webapp/playbooks/src/components/cloud_upgrade_post.tsx delete mode 100644 webapp/playbooks/src/components/command_input.tsx delete mode 100644 webapp/playbooks/src/components/create_playbook_modal.tsx delete mode 100644 webapp/playbooks/src/components/custom_post_styles.tsx delete mode 100644 webapp/playbooks/src/components/datetime_input.tsx delete mode 100644 webapp/playbooks/src/components/datetime_parsing.test.ts delete mode 100644 webapp/playbooks/src/components/datetime_parsing.ts delete mode 100644 webapp/playbooks/src/components/datetime_selector.tsx delete mode 100644 webapp/playbooks/src/components/dot_menu.tsx delete mode 100644 webapp/playbooks/src/components/dropdown.tsx delete mode 100644 webapp/playbooks/src/components/error_page.tsx delete mode 100644 webapp/playbooks/src/components/formatted_duration.test.tsx delete mode 100644 webapp/playbooks/src/components/formatted_duration.tsx delete mode 100644 webapp/playbooks/src/components/formatted_markdown.tsx delete mode 100644 webapp/playbooks/src/components/give_feedback_button.tsx delete mode 100644 webapp/playbooks/src/components/global_header_right.tsx delete mode 100644 webapp/playbooks/src/components/keywords_selector.tsx delete mode 100644 webapp/playbooks/src/components/login_hook.tsx delete mode 100644 webapp/playbooks/src/components/markdown_edit.tsx delete mode 100644 webapp/playbooks/src/components/markdown_textbox.tsx delete mode 100644 webapp/playbooks/src/components/modals/run_playbook_modal.tsx delete mode 100644 webapp/playbooks/src/components/modals/run_update_channel.tsx delete mode 100644 webapp/playbooks/src/components/modals/run_update_name.tsx delete mode 100644 webapp/playbooks/src/components/modals/update_run_status_modal.tsx delete mode 100644 webapp/playbooks/src/components/multi_checkbox.tsx delete mode 100644 webapp/playbooks/src/components/pagination_row.tsx delete mode 100644 webapp/playbooks/src/components/patterned_text_area.tsx delete mode 100644 webapp/playbooks/src/components/playbook_actions_modal.tsx delete mode 100644 webapp/playbooks/src/components/playbooks_selector.tsx delete mode 100644 webapp/playbooks/src/components/post_menu.tsx delete mode 100644 webapp/playbooks/src/components/post_menu_modal.tsx delete mode 100644 webapp/playbooks/src/components/post_text.tsx delete mode 100644 webapp/playbooks/src/components/profile/profile.tsx delete mode 100644 webapp/playbooks/src/components/profile/profile_button.tsx delete mode 100644 webapp/playbooks/src/components/profile/profile_selector.tsx delete mode 100644 webapp/playbooks/src/components/retrospective_post.tsx delete mode 100644 webapp/playbooks/src/components/retrospective_reminder_posts.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_about.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_about_buttons.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_about_description.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_about_title.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_checklist_list.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_home.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_home_item.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_main.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_participant.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_participants.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_post_update.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_post_update_button.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_run_details.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_run_details_title.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_run_details_tour_dialog.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_run_list.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_run_participants.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_run_participants_title.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_shared.tsx delete mode 100644 webapp/playbooks/src/components/rhs/rhs_title_common.tsx delete mode 100644 webapp/playbooks/src/components/run_actions_modal.tsx delete mode 100644 webapp/playbooks/src/components/sidebar/create_playbook_dropdown.tsx delete mode 100644 webapp/playbooks/src/components/sidebar/group.tsx delete mode 100644 webapp/playbooks/src/components/sidebar/item.tsx delete mode 100644 webapp/playbooks/src/components/sidebar/playbooks_sidebar.tsx delete mode 100644 webapp/playbooks/src/components/sidebar/sidebar.tsx delete mode 100644 webapp/playbooks/src/components/sortable_col_header.tsx delete mode 100644 webapp/playbooks/src/components/team/team_selector.tsx delete mode 100644 webapp/playbooks/src/components/templates/template_data.tsx delete mode 100644 webapp/playbooks/src/components/templates/template_item.tsx delete mode 100644 webapp/playbooks/src/components/templates/template_selector.tsx delete mode 100644 webapp/playbooks/src/components/text_edit.tsx delete mode 100644 webapp/playbooks/src/components/time_spec.tsx delete mode 100644 webapp/playbooks/src/components/tutorial/tours.ts delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/backdrop.tsx delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/dot.scss delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/dot.tsx delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/hooks.ts delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/index.ts delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/manager.ts delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/tutorial_tour_tip.scss delete mode 100644 webapp/playbooks/src/components/tutorial/tutorial_tour_tip/tutorial_tour_tip.tsx delete mode 100644 webapp/playbooks/src/components/update_post.tsx delete mode 100644 webapp/playbooks/src/components/update_request_post.tsx delete mode 100644 webapp/playbooks/src/components/upgrade_banner.tsx delete mode 100644 webapp/playbooks/src/components/widgets/conditional_tooltip.tsx delete mode 100644 webapp/playbooks/src/components/widgets/confirmation_modal.tsx delete mode 100644 webapp/playbooks/src/components/widgets/confirmation_modal_light.tsx delete mode 100644 webapp/playbooks/src/components/widgets/copy_link.tsx delete mode 100644 webapp/playbooks/src/components/widgets/generic_modal.tsx delete mode 100644 webapp/playbooks/src/components/widgets/header.tsx delete mode 100644 webapp/playbooks/src/components/widgets/menu/menu.tsx delete mode 100644 webapp/playbooks/src/components/widgets/menu/menu_group.tsx delete mode 100644 webapp/playbooks/src/components/widgets/menu/menu_item.tsx delete mode 100644 webapp/playbooks/src/components/widgets/menu/menu_wrapper.tsx delete mode 100644 webapp/playbooks/src/components/widgets/pill.tsx delete mode 100644 webapp/playbooks/src/components/widgets/show_more.tsx delete mode 100644 webapp/playbooks/src/components/widgets/text_with_tooltip.tsx delete mode 100644 webapp/playbooks/src/components/widgets/text_with_tooltip_when_ellipsis.tsx delete mode 100644 webapp/playbooks/src/components/widgets/tooltip.tsx delete mode 100644 webapp/playbooks/src/components/widgets/unsaved_changes_modal.tsx delete mode 100644 webapp/playbooks/src/constants.ts delete mode 100644 webapp/playbooks/src/graphql/apollo.tsx delete mode 100644 webapp/playbooks/src/graphql/generated/fragment-masking.ts delete mode 100644 webapp/playbooks/src/graphql/generated/gql.ts delete mode 100644 webapp/playbooks/src/graphql/generated/graphql.ts delete mode 100644 webapp/playbooks/src/graphql/generated/index.ts delete mode 100644 webapp/playbooks/src/graphql/hooks.ts delete mode 100644 webapp/playbooks/src/graphql/playbook.graphql delete mode 100644 webapp/playbooks/src/graphql/runs.graphql delete mode 100644 webapp/playbooks/src/graphql_client.ts delete mode 100644 webapp/playbooks/src/hooks/crud.ts delete mode 100644 webapp/playbooks/src/hooks/general.test.ts delete mode 100644 webapp/playbooks/src/hooks/general.ts delete mode 100644 webapp/playbooks/src/hooks/index.ts delete mode 100644 webapp/playbooks/src/hooks/license.ts delete mode 100644 webapp/playbooks/src/hooks/permissions.ts delete mode 100644 webapp/playbooks/src/hooks/routing.ts delete mode 100644 webapp/playbooks/src/hooks/run.tsx delete mode 100644 webapp/playbooks/src/hooks/telemetry.ts delete mode 100644 webapp/playbooks/src/index.tsx delete mode 100644 webapp/playbooks/src/license.test.ts delete mode 100644 webapp/playbooks/src/license.ts delete mode 100644 webapp/playbooks/src/manifest.js delete mode 100644 webapp/playbooks/src/mobile.ts delete mode 100644 webapp/playbooks/src/reducer.test.ts delete mode 100644 webapp/playbooks/src/reducer.ts delete mode 100644 webapp/playbooks/src/remote_entry.ts delete mode 100644 webapp/playbooks/src/rhs_opener.ts delete mode 100644 webapp/playbooks/src/rhs_title_remote_render.tsx delete mode 100644 webapp/playbooks/src/selectors.ts delete mode 100644 webapp/playbooks/src/slash_command.test.ts delete mode 100644 webapp/playbooks/src/slash_command.ts delete mode 100644 webapp/playbooks/src/styles/headings.tsx delete mode 100644 webapp/playbooks/src/types/actions.ts delete mode 100644 webapp/playbooks/src/types/assets.d.ts delete mode 100644 webapp/playbooks/src/types/backstage.ts delete mode 100644 webapp/playbooks/src/types/backstage_rhs.ts delete mode 100644 webapp/playbooks/src/types/category.ts delete mode 100644 webapp/playbooks/src/types/channel_actions.ts delete mode 100644 webapp/playbooks/src/types/compass.tsx delete mode 100644 webapp/playbooks/src/types/global.d.ts delete mode 100644 webapp/playbooks/src/types/insights.ts delete mode 100644 webapp/playbooks/src/types/js-trim-multiline-string.d.ts delete mode 100644 webapp/playbooks/src/types/permissions.ts delete mode 100644 webapp/playbooks/src/types/playbook.ts delete mode 100644 webapp/playbooks/src/types/playbook_run.ts delete mode 100644 webapp/playbooks/src/types/rhs.ts delete mode 100644 webapp/playbooks/src/types/settings.ts delete mode 100644 webapp/playbooks/src/types/stats.ts delete mode 100644 webapp/playbooks/src/types/store.ts delete mode 100644 webapp/playbooks/src/types/telemetry.ts delete mode 100644 webapp/playbooks/src/types/type_utils.d.ts delete mode 100644 webapp/playbooks/src/types/websocket_events.ts delete mode 100644 webapp/playbooks/src/utils.test.ts delete mode 100644 webapp/playbooks/src/utils.ts delete mode 100644 webapp/playbooks/src/webapp_globals.ts delete mode 100644 webapp/playbooks/src/websocket_events.test.ts delete mode 100644 webapp/playbooks/src/websocket_events.ts delete mode 100644 webapp/playbooks/tsconfig.json delete mode 100644 webapp/playbooks/webpack.config.js diff --git a/e2e-tests/cypress/tests/support/api/plugin.js b/e2e-tests/cypress/tests/support/api/plugin.js index 0581401ec17..0d9b7e07343 100644 --- a/e2e-tests/cypress/tests/support/api/plugin.js +++ b/e2e-tests/cypress/tests/support/api/plugin.js @@ -148,6 +148,7 @@ const prepackagedPlugins = [ 'com.mattermost.nps', 'com.mattermost.welcomebot', 'zoom', + 'playbooks', ]; Cypress.Commands.add('apiDisableNonPrepackagedPlugins', () => { diff --git a/e2e-tests/playwright/support/server/default_config.ts b/e2e-tests/playwright/support/server/default_config.ts index 8bb831bd47e..8c6467b437b 100644 --- a/e2e-tests/playwright/support/server/default_config.ts +++ b/e2e-tests/playwright/support/server/default_config.ts @@ -606,9 +606,7 @@ const defaultServerConfig: AdminConfig = { CleanupJobsThresholdDays: -1, CleanupConfigThresholdDays: -1, }, - ProductSettings: { - EnablePlaybooks: true, - }, + ProductSettings: {}, PluginSettings: { Enable: true, EnableUploads: false, diff --git a/server/Makefile b/server/Makefile index 7568c89eae6..229a39895af 100644 --- a/server/Makefile +++ b/server/Makefile @@ -147,8 +147,7 @@ DIST_PATH_WIN=$(DIST_ROOT)/windows/mattermost TESTS=. # Packages lists -TE_PACKAGES=$(shell $(GO) list ./... | grep -vE 'server/v8/playbooks|server/v8/cmd/mmctl') -PLAYBOOKS_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/playbooks') +TE_PACKAGES=$(shell $(GO) list ./... | grep -vE 'server/v8/cmd/mmctl') SUITE_PACKAGES=$(shell $(GO) list ./...| grep -vE 'server/v8/cmd/mmctl') MMCTL_PACKAGES=$(shell $(GO) list ./... | grep -E 'server/v8/cmd/mmctl') @@ -164,6 +163,7 @@ PLUGIN_PACKAGES += mattermost-plugin-confluence-v1.3.0 PLUGIN_PACKAGES += mattermost-plugin-custom-attributes-v1.3.1 PLUGIN_PACKAGES += mattermost-plugin-github-v2.1.6 PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.6.0 +PLUGIN_PACKAGES += mattermost-plugin-playbooks-v1.36.1 PLUGIN_PACKAGES += mattermost-plugin-jenkins-v1.1.0 PLUGIN_PACKAGES += mattermost-plugin-jira-v3.2.5 PLUGIN_PACKAGES += mattermost-plugin-jitsi-v2.0.1 @@ -188,9 +188,9 @@ endif EE_PACKAGES=$(shell $(GO) list $(BUILD_ENTERPRISE_DIR)/...) ifeq ($(BUILD_ENTERPRISE_READY),true) - ALL_PACKAGES=$(TE_PACKAGES) $(PLAYBOOKS_PACKAGES) $(EE_PACKAGES) + ALL_PACKAGES=$(TE_PACKAGES) $(EE_PACKAGES) else - ALL_PACKAGES=$(TE_PACKAGES) $(PLAYBOOKS_PACKAGES) + ALL_PACKAGES=$(TE_PACKAGES) endif all: run ## Alias for 'run'. @@ -460,7 +460,6 @@ endif test-server-race: test-server-pre ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(TE_PACKAGES) $(EE_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" - ./scripts/test.sh "$(GO)" "-race $(GOFLAGS)" "$(PLAYBOOKS_PACKAGES)" "$(TESTS)" "$(TESTFLAGS)" "$(GOBIN)" "90m" ifneq ($(IS_CI),true) ifneq ($(MM_NO_DOCKER),true) ifneq ($(TEMP_DOCKER_SERVICES),) diff --git a/server/channels/app/plugin_install_test.go b/server/channels/app/plugin_install_test.go index 352d1a5e351..0a5cf901d67 100644 --- a/server/channels/app/plugin_install_test.go +++ b/server/channels/app/plugin_install_test.go @@ -172,7 +172,7 @@ func TestInstallPluginLocally(t *testing.T) { defer th.TearDown() cleanExistingBundles(t, th) - _, appErr := installPlugin(t, th, "playbooks", "0.0.1", installPluginLocallyAlways) + _, appErr := installPlugin(t, th, "com.mattermost.plugin-incident-response", "0.0.1", installPluginLocallyAlways) require.NotNil(t, appErr) require.Equal(t, "app.plugin.blocked.app_error", appErr.Id) assertBundleInfoManifests(t, th, []*model.Manifest{}) diff --git a/server/channels/app/product.go b/server/channels/app/product.go index 323eb103b90..82399bcec78 100644 --- a/server/channels/app/product.go +++ b/server/channels/app/product.go @@ -83,16 +83,6 @@ func (s *Server) shouldStart(product string) bool { return false } } - if product == "playbooks" { - if os.Getenv("MM_DISABLE_PLAYBOOKS") == "true" { - s.Log().Warn("Skipping Playbooks start: disabled via env var") - return false - } - if !*s.Config().ProductSettings.EnablePlaybooks { - s.Log().Warn("Skipping Playbooks start: disabled via configuration") - return false - } - } return true } diff --git a/server/cmd/mattermost/main.go b/server/cmd/mattermost/main.go index 1201fcf7e9c..5e4d43aa576 100644 --- a/server/cmd/mattermost/main.go +++ b/server/cmd/mattermost/main.go @@ -17,7 +17,6 @@ import ( // Blank imports for each product to register themselves _ "github.com/mattermost/mattermost/server/v8/boards/product" - _ "github.com/mattermost/mattermost/server/v8/playbooks/product" ) func main() { diff --git a/server/config/client.go b/server/config/client.go index b2ed9b2886f..95037e774bc 100644 --- a/server/config/client.go +++ b/server/config/client.go @@ -144,8 +144,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li props["AllowSyncedDrafts"] = strconv.FormatBool(*c.ServiceSettings.AllowSyncedDrafts) props["DelayChannelAutocomplete"] = strconv.FormatBool(*c.ExperimentalSettings.DelayChannelAutocomplete) - props["EnablePlaybooks"] = strconv.FormatBool(*c.ProductSettings.EnablePlaybooks) - if license != nil { props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer) diff --git a/server/config/client_test.go b/server/config/client_test.go index 56b9ddb3f11..34af837d06a 100644 --- a/server/config/client_test.go +++ b/server/config/client_test.go @@ -326,20 +326,6 @@ func TestGetClientConfig(t *testing.T) { "ExperimentalSharedChannels": "true", }, }, - { - "Default Playbooks Enabled", - &model.Config{ - ProductSettings: model.ProductSettings{}, - }, - "", - &model.License{ - Features: &model.Features{}, - SkuShortName: "other", - }, - map[string]string{ - "EnablePlaybooks": "true", - }, - }, } for _, testCase := range testCases { diff --git a/server/config/diff_test.go b/server/config/diff_test.go index b3dd7bcf298..3dd0bdc6d8a 100644 --- a/server/config/diff_test.go +++ b/server/config/diff_test.go @@ -810,6 +810,9 @@ func TestDiff(t *testing.T) { "com.mattermost.calls": { Enable: true, }, + "playbooks": { + Enable: true, + }, }, }, }, @@ -839,6 +842,9 @@ func TestDiff(t *testing.T) { "com.mattermost.calls": { Enable: true, }, + "playbooks": { + Enable: true, + }, }, }, }, @@ -860,6 +866,9 @@ func TestDiff(t *testing.T) { "com.mattermost.calls": { Enable: true, }, + "playbooks": { + Enable: true, + }, }, }, }, diff --git a/server/go.mod b/server/go.mod index bfffbfa7384..ea9095c2117 100644 --- a/server/go.mod +++ b/server/go.mod @@ -22,13 +22,11 @@ require ( github.com/golang-migrate/migrate/v4 v4.15.2 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/mock v1.6.0 - github.com/google/go-querystring v1.1.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 github.com/gorilla/schema v1.2.0 github.com/gorilla/websocket v1.5.0 github.com/graph-gophers/dataloader/v6 v6.0.0 - github.com/graph-gophers/dataloader/v7 v7.1.0 github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a github.com/h2non/go-is-svg v0.0.0-20160927212452-35e8c4b0612c github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b @@ -54,7 +52,6 @@ require ( github.com/mholt/archiver/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.23 github.com/minio/minio-go/v7 v7.0.51 - github.com/mitchellh/mapstructure v1.5.0 github.com/oklog/run v1.1.0 github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb github.com/opentracing/opentracing-go v1.2.0 @@ -78,17 +75,14 @@ require ( github.com/uber/jaeger-lib v2.4.1+incompatible github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/wiggin77/merror v1.0.4 - github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c github.com/yuin/goldmark v1.5.4 golang.org/x/crypto v0.8.0 golang.org/x/image v0.7.0 golang.org/x/net v0.10.0 - golang.org/x/oauth2 v0.7.0 golang.org/x/sync v0.2.0 golang.org/x/term v0.8.0 golang.org/x/tools v0.9.1 - gopkg.in/guregu/null.v4 v4.0.0 gopkg.in/mail.v2 v2.3.1 gopkg.in/olivere/elastic.v6 v6.2.37 gopkg.in/yaml.v2 v2.4.0 @@ -178,6 +172,7 @@ require ( github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect @@ -224,7 +219,6 @@ require ( golang.org/x/mod v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect - google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.54.0 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/server/go.sum b/server/go.sum index a8e16e22458..b9d5d05de7b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -722,7 +722,6 @@ github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYV github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -787,8 +786,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/graph-gophers/dataloader/v6 v6.0.0 h1:qBpmq3B8PIQesoh0EJXKGfw+ulMUb+KFl4IZOe9ScWg= github.com/graph-gophers/dataloader/v6 v6.0.0/go.mod h1:J15OZSnOoZgMkijpbZcwCmglIDYqlUiTEE1xLPbyqZM= -github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc= -github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q= github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a h1:i0+Se9S+2zL5CBxJouqn2Ej6UQMwH1c57ZB6DVnqck4= github.com/graph-gophers/graphql-go v1.5.1-0.20230110080634-edea822f558a/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -1559,8 +1556,6 @@ github.com/wiggin77/srslog v1.0.1 h1:gA2XjSMy3DrRdX9UqLuDtuVAAshb8bE1NhX1YK0Qe+8 github.com/wiggin77/srslog v1.0.1/go.mod h1:fehkyYDq1QfuYn60TDPu9YdY2bB85VUW2mvN1WynEls= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw= -github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= @@ -1841,8 +1836,6 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -2181,7 +2174,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -2336,8 +2328,6 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= -gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/server/i18n/en.json b/server/i18n/en.json index 07f1ec9f9c7..88224be89f5 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -4947,10 +4947,6 @@ "id": "app.command.deletecommand.internal_error", "translation": "Unable to delete the command." }, - { - "id": "app.command.execute.error", - "translation": "Unable to execute command." - }, { "id": "app.command.getcommand.internal_error", "translation": "Unable to get the command." @@ -6815,88 +6811,6 @@ "id": "app.user.demote_user_to_guest.user_update.app_error", "translation": "Failed to update the user." }, - { - "id": "app.user.digest.overdue_status_updates.heading", - "translation": "Overdue Status Updates" - }, - { - "id": "app.user.digest.overdue_status_updates.num_overdue", - "translation": { - "one": "You have {{.Count}} run overdue for a status update:", - "other": "You have {{.Count}} runs overdue for a status update:" - } - }, - { - "id": "app.user.digest.overdue_status_updates.zero_overdue", - "translation": "You have 0 runs overdue." - }, - { - "id": "app.user.digest.runs_in_progress.heading", - "translation": "Runs in Progress" - }, - { - "id": "app.user.digest.runs_in_progress.num_in_progress", - "translation": { - "one": "You have {{.Count}} run currently in progress:", - "other": "You have {{.Count}} runs currently in progress:" - } - }, - { - "id": "app.user.digest.runs_in_progress.zero_in_progress", - "translation": "You have 0 runs currently in progress." - }, - { - "id": "app.user.digest.tasks.all_tasks_command", - "translation": "Please use `/playbook todo` to see all your tasks." - }, - { - "id": "app.user.digest.tasks.due_after_today", - "translation": { - "one": "You have **{{.Count}} assigned task due after today**.", - "other": "You have **{{.Count}} assigned tasks due after today**." - } - }, - { - "id": "app.user.digest.tasks.due_in_x_days", - "translation": { - "one": "Due in {{.Count}} day", - "other": "Due in {{.Count}} days" - } - }, - { - "id": "app.user.digest.tasks.due_today", - "translation": "Due today" - }, - { - "id": "app.user.digest.tasks.due_x_days_ago", - "translation": "Due {{.Count}} days ago" - }, - { - "id": "app.user.digest.tasks.due_yesterday", - "translation": "Due yesterday" - }, - { - "id": "app.user.digest.tasks.heading", - "translation": "Your assigned tasks" - }, - { - "id": "app.user.digest.tasks.num_assigned", - "translation": { - "one": "You have {{.Count}} assigned task:", - "other": "You have {{.Count}} total assigned tasks:" - } - }, - { - "id": "app.user.digest.tasks.num_assigned_due_until_today", - "translation": { - "one": "You have {{.Count}} assigned task that is now due:", - "other": "You have {{.Count}} assigned tasks that are now due:" - } - }, - { - "id": "app.user.digest.tasks.zero_assigned", - "translation": "You have 0 assigned tasks." - }, { "id": "app.user.get.app_error", "translation": "We encountered an error finding the account." @@ -6973,26 +6887,6 @@ "id": "app.user.missing_account.const", "translation": "Unable to find the user." }, - { - "id": "app.user.new_run.intro", - "translation": "**Owner** {{.Username}}" - }, - { - "id": "app.user.new_run.playbook", - "translation": "Playbook" - }, - { - "id": "app.user.new_run.run_name", - "translation": "Run name" - }, - { - "id": "app.user.new_run.submit_label", - "translation": "Start run" - }, - { - "id": "app.user.new_run.title", - "translation": "Run playbook" - }, { "id": "app.user.permanent_delete.app_error", "translation": "Unable to delete the existing account." @@ -7005,108 +6899,6 @@ "id": "app.user.promote_guest.user_update.app_error", "translation": "Failed to update the user." }, - { - "id": "app.user.run.add_checklist_item.description", - "translation": "Description" - }, - { - "id": "app.user.run.add_checklist_item.name", - "translation": "Name" - }, - { - "id": "app.user.run.add_checklist_item.submit_label", - "translation": "Add task" - }, - { - "id": "app.user.run.add_checklist_item.title", - "translation": "Add new task" - }, - { - "id": "app.user.run.add_to_timeline.playbook_run", - "translation": "Playbook Run" - }, - { - "id": "app.user.run.add_to_timeline.submit_label", - "translation": "Add to run timeline" - }, - { - "id": "app.user.run.add_to_timeline.summary", - "translation": "Summary" - }, - { - "id": "app.user.run.add_to_timeline.summary.help", - "translation": "Max 64 chars" - }, - { - "id": "app.user.run.add_to_timeline.summary.placeholder", - "translation": "Short summary shown in the timeline" - }, - { - "id": "app.user.run.add_to_timeline.title", - "translation": "Add to run timeline" - }, - { - "id": "app.user.run.confirm_finish.num_outstanding", - "translation": { - "one": "There is **{{.Count}} outstanding task**. Are you sure you want to finish the run *{{.RunName}}* for all participants?", - "other": "There are **{{.Count}} outstanding tasks**. Are you sure you want to finish the run *{{.RunName}}* for all participants?" - } - }, - { - "id": "app.user.run.confirm_finish.submit_label", - "translation": "Finish run" - }, - { - "id": "app.user.run.confirm_finish.title", - "translation": "Confirm finish run" - }, - { - "id": "app.user.run.request_join_channel", - "translation": "@{{.Name}} is a run participant and wants join this channel. Any member of the channel can invite them.\n" - }, - { - "id": "app.user.run.request_update", - "translation": "@here — @{{.Name}} requested a status update for [{{.RunName}}]({{.RunURL}}). \n" - }, - { - "id": "app.user.run.status_disable", - "translation": "@{{.Username}} disabled the status updates for [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.status_enable", - "translation": "@{{.Username}} enabled the status updates for [{{.RunName}}]({{.RunURL}})" - }, - { - "id": "app.user.run.update_status.change_since_last_update", - "translation": "Change since last update" - }, - { - "id": "app.user.run.update_status.finish_run", - "translation": "Finish run" - }, - { - "id": "app.user.run.update_status.finish_run.placeholder", - "translation": "Also mark the run as finished" - }, - { - "id": "app.user.run.update_status.num_channel", - "translation": { - "one": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channel.", - "other": "Provide an update to the stakeholders. This post will be broadcasted to {{.Count}} channels." - } - }, - { - "id": "app.user.run.update_status.reminder_for_next_update", - "translation": "Reminder for next update" - }, - { - "id": "app.user.run.update_status.submit_label", - "translation": "Update status" - }, - { - "id": "app.user.run.update_status.title", - "translation": "Status update" - }, { "id": "app.user.save.app_error", "translation": "Unable to save the account." diff --git a/server/playbooks/client/LICENSE b/server/playbooks/client/LICENSE deleted file mode 100644 index d6456956733..00000000000 --- a/server/playbooks/client/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/server/playbooks/client/action.go b/server/playbooks/client/action.go deleted file mode 100644 index c66ae5a1a58..00000000000 --- a/server/playbooks/client/action.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -type GenericChannelActionWithoutPayload struct { - ID string `json:"id"` - ChannelID string `json:"channel_id"` - Enabled bool `json:"enabled"` - DeleteAt int64 `json:"delete_at"` - ActionType string `json:"action_type"` - TriggerType string `json:"trigger_type"` -} - -type GenericChannelAction struct { - GenericChannelActionWithoutPayload - Payload interface{} `json:"payload"` -} - -type WelcomeMessagePayload struct { - Message string `json:"message" mapstructure:"message"` -} - -type PromptRunPlaybookFromKeywordsPayload struct { - Keywords []string `json:"keywords" mapstructure:"keywords"` - PlaybookID string `json:"playbook_id" mapstructure:"playbook_id"` -} - -type CategorizeChannelPayload struct { - CategoryName string `json:"category_name" mapstructure:"category_name"` -} - -type WelcomeMessageAction struct { - GenericChannelActionWithoutPayload - Payload WelcomeMessagePayload `json:"payload"` -} - -const ( - // Action types - ActionTypeWelcomeMessage = "send_welcome_message" - ActionTypePromptRunPlaybook = "prompt_run_playbook" - ActionTypeCategorizeChannel = "categorize_channel" - - // Trigger types - TriggerTypeNewMemberJoins = "new_member_joins" - TriggerTypeKeywordsPosted = "keywords" -) - -// ChannelActionListOptions specifies the optional parameters to the -// ActionsService.List method. -type ChannelActionListOptions struct { - TriggerType string `url:"trigger_type,omitempty"` - ActionType string `url:"action_type,omitempty"` -} - -// ChannelActionCreateOptions specifies the parameters for ActionsService.Create method. -type ChannelActionCreateOptions struct { - ChannelID string `json:"channel_id"` - Enabled bool `json:"enabled"` - ActionType string `json:"action_type"` - TriggerType string `json:"trigger_type"` - Payload interface{} `json:"payload"` -} diff --git a/server/playbooks/client/actions.go b/server/playbooks/client/actions.go deleted file mode 100644 index 2c4219eb4c5..00000000000 --- a/server/playbooks/client/actions.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "context" - "fmt" - "net/http" -) - -// ActionsService handles communication with the actions related -// methods of the Playbook API. -type ActionsService struct { - client *Client -} - -// Create an action. Returns the id of the newly created action. -func (s *ActionsService) Create(ctx context.Context, channelID string, opts ChannelActionCreateOptions) (string, error) { - actionURL := fmt.Sprintf("actions/channels/%s", channelID) - req, err := s.client.newRequest(http.MethodPost, actionURL, opts) - if err != nil { - return "", err - } - - var result struct { - ID string `json:"id"` - } - resp, err := s.client.do(ctx, req, &result) - if err != nil { - return "", err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return "", fmt.Errorf("expected status code %d", http.StatusCreated) - } - - return result.ID, nil -} - -// List the actions in a channel. -func (s *ActionsService) List(ctx context.Context, channelID string, opts ChannelActionListOptions) ([]GenericChannelAction, error) { - actionURL, err := addOptions(fmt.Sprintf("actions/channels/%s", channelID), opts) - if err != nil { - return nil, fmt.Errorf("failed to build options: %w", err) - } - - req, err := s.client.newRequest(http.MethodGet, actionURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to build request: %w", err) - } - - result := []GenericChannelAction{} - resp, err := s.client.do(ctx, req, &result) - if err != nil { - return nil, fmt.Errorf("failed to execute request: %w", err) - } - resp.Body.Close() - - return result, nil -} - -// Update an existing action. -func (s *ActionsService) Update(ctx context.Context, action GenericChannelAction) error { - updateURL := fmt.Sprintf("actions/channels/%s/%s", action.ChannelID, action.ID) - req, err := s.client.newRequest(http.MethodPut, updateURL, action) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - - return nil -} diff --git a/server/playbooks/client/client.go b/server/playbooks/client/client.go deleted file mode 100644 index c5e19a1771b..00000000000 --- a/server/playbooks/client/client.go +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "reflect" - "strconv" - - "github.com/google/go-querystring/query" - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "golang.org/x/oauth2" -) - -const ( - apiVersion = "v0" - manifestID = "playbooks" - userAgent = "go-client/" + apiVersion -) - -// Client manages communication with the Playbooks API. -type Client struct { - // client is the underlying HTTP client used to make API requests. - client *http.Client - // BaseURL is the base HTTP endpoint for the Playbooks plugin. - BaseURL *url.URL - // User agent used when communicating with the Playbooks API. - UserAgent string - - // PlaybookRuns is a collection of methods used to interact with playbook runs. - PlaybookRuns *PlaybookRunService - // Playbooks is a collection of methods used to interact with playbooks. - Playbooks *PlaybooksService - // Settings is a collection of methods used to interact with settings. - Settings *SettingsService - // Actions is a collection of methods used to interact with actions. - Actions *ActionsService - // Stats is a collection of methods used to interact with stats. - Stats *StatsService - // Reminders is a collection of methods used to interact with reminders. - Reminders *RemindersService - // Telemetry is a collection of methods used to interact with telemetry. - Telemetry *TelemetryService -} - -// New creates a new instance of Client using the configuration from the given Mattermost Client. -func New(client4 *model.Client4) (*Client, error) { - ctx := context.Background() - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: client4.AuthToken}, - ) - - return newClient(client4.URL, oauth2.NewClient(ctx, ts)) -} - -// newClient creates a new instance of Client from the given URL and http.Client. -func newClient(mattermostSiteURL string, httpClient *http.Client) (*Client, error) { - siteURL, err := url.Parse(mattermostSiteURL) - if err != nil { - return nil, err - } - - c := &Client{client: httpClient, BaseURL: siteURL, UserAgent: userAgent} - c.PlaybookRuns = &PlaybookRunService{c} - c.Playbooks = &PlaybooksService{c} - c.Settings = &SettingsService{c} - c.Actions = &ActionsService{c} - c.Stats = &StatsService{c} - c.Reminders = &RemindersService{c} - c.Telemetry = &TelemetryService{c} - return c, nil -} - -// newRequest creates an API request, JSON-encoding any given body parameter. -func (c *Client) newRequest(method, endpoint string, body interface{}) (*http.Request, error) { - u, err := c.BaseURL.Parse(buildAPIURL(endpoint)) - if err != nil { - return nil, errors.Wrapf(err, "invalid endpoint %s", endpoint) - } - - var buf io.ReadWriter - if body != nil { - buf = &bytes.Buffer{} - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - err = enc.Encode(body) - if err != nil { - return nil, errors.Wrapf(err, "failed to encode body %s", body) - } - } - - req, err := http.NewRequest(method, u.String(), buf) - if err != nil { - return nil, errors.Wrapf(err, "failed to create http request for url %s", u) - } - - if buf != nil { - req.Header.Set("Content-Type", "application/json") - } - if c.UserAgent != "" { - req.Header.Set("User-Agent", c.UserAgent) - } - return req, nil -} - -// buildAPIURL constructs the path to the given endpoint. -func buildAPIURL(endpoint string) string { - return fmt.Sprintf("plugins/%s/api/%s/%s", manifestID, apiVersion, endpoint) -} - -// do sends an API request and returns the API response. -// -// The API response is JSON decoded and stored in the value pointed to by v, or returned as an -// error if an API error has occurred. If v implements the io.Writer -// interface, the raw response body will be written to v, without attempting to -// first decode it. -func (c *Client) do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { - if ctx == nil { - return nil, errors.New("context must be non-nil") - } - req = req.WithContext(ctx) - - resp, err := c.client.Do(req) - if err != nil { - select { - case <-ctx.Done(): - return nil, errors.Wrapf(ctx.Err(), "client err=%s", err.Error()) - default: - } - - return nil, err - } - defer resp.Body.Close() - - err = checkResponse(resp) - if err != nil { - return resp, err - } - - if v != nil { - if w, ok := v.(io.Writer); ok { - if _, err = io.Copy(w, resp.Body); err != nil { - return nil, err - } - } else { - body, _ := ioutil.ReadAll(resp.Body) - - decErr := json.NewDecoder(bytes.NewReader(body)).Decode(v) - if decErr == io.EOF { - // TODO: Confirm if this happens only on empty bodies. If so, check that first before decoding. - decErr = nil // ignore EOF errors caused by empty response body - } - if decErr != nil { - err = decErr - } - } - } - - return resp, err -} - -type GraphQLInput struct { - Query string `json:"query"` - OperationName string `json:"operationName"` - Variables map[string]interface{} `json:"variables"` -} - -func (c *Client) DoGraphql(ctx context.Context, input *GraphQLInput, v interface{}) error { - url := "query" - req, err := c.newRequest(http.MethodPost, url, input) - if err != nil { - return err - } - - _, err = c.do(ctx, req, v) - if err != nil { - return err - } - - return nil -} - -// checkResponse checks the API response for an error. -// -// Any response with a status code outside 2xx is considered an error, and its body inspected for -// an optional `Error` property in a JSON struct. -func checkResponse(r *http.Response) error { - if c := r.StatusCode; http.StatusOK <= c && c <= 299 { - return nil - } - - errorResponse := &ErrorResponse{ - StatusCode: r.StatusCode, - Method: r.Request.Method, - URL: r.Request.URL.String(), - } - data, err := ioutil.ReadAll(r.Body) - if err != nil { - errorResponse.Err = fmt.Errorf("failed to read response body: %w", err) - } - r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) - - if data != nil { - _ = json.Unmarshal(data, errorResponse) - } - - return errorResponse -} - -// addOption adds the given parameter as an URL query parameters to s. -func addOption(s string, name, value string) (string, error) { - u, err := url.Parse(s) - if err != nil { - return s, errors.Wrapf(err, "failed to parse %s", s) - } - - qa := u.Query() - qa.Add(name, value) - u.RawQuery = qa.Encode() - - return u.String(), nil -} - -// addOptions adds the parameters in opts as URL query parameters to s. opts -// must be a struct whose fields may contain "url" tags. -func addOptions(s string, opts interface{}) (string, error) { - v := reflect.ValueOf(opts) - if v.Kind() == reflect.Ptr && v.IsNil() { - return s, nil - } - - u, err := url.Parse(s) - if err != nil { - return s, errors.Wrapf(err, "failed to parse %s", s) - } - - qs, err := query.Values(opts) - if err != nil { - return s, errors.Wrapf(err, "failed to opts %+v", opts) - } - - // Append to the existing query parameters. - qa := u.Query() - for key, values := range qs { - for _, value := range values { - qa.Add(key, value) - } - } - - u.RawQuery = qa.Encode() - return u.String(), nil -} - -// addPaginationOptions adds the given pagination parameters as URL query parameters to s. -func addPaginationOptions(s string, page, perPage int) (string, error) { - u, err := url.Parse(s) - if err != nil { - return s, errors.Wrapf(err, "failed to parse %s", s) - } - - qa := u.Query() - qa.Add("page", strconv.Itoa(page)) - qa.Add("per_page", strconv.Itoa(perPage)) - u.RawQuery = qa.Encode() - - return u.String(), nil -} diff --git a/server/playbooks/client/doc.go b/server/playbooks/client/doc.go deleted file mode 100644 index aa43f1a4f60..00000000000 --- a/server/playbooks/client/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Package client provides an HTTP client for using the Playbooks API. -package client diff --git a/server/playbooks/client/doc_test.go b/server/playbooks/client/doc_test.go deleted file mode 100644 index fe7d2990d34..00000000000 --- a/server/playbooks/client/doc_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client_test - -import ( - "context" - "fmt" - "log" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" -) - -func Example() { - ctx := context.Background() - - client4 := model.NewAPIv4Client("http://localhost:8065") - _, _, err := client4.Login(context.Background(), "test@example.com", "testtest") - if err != nil { - log.Fatal(err) - } - - c, err := client.New(client4) - if err != nil { - log.Fatal(err) - } - - playbookRunID := "h4n3h7s1qjf5pkis4dn6cuxgwa" - playbookRun, err := c.PlaybookRuns.Get(ctx, playbookRunID) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name) -} diff --git a/server/playbooks/client/error_response.go b/server/playbooks/client/error_response.go deleted file mode 100644 index 50306d7afa8..00000000000 --- a/server/playbooks/client/error_response.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "encoding/json" - "errors" - "fmt" -) - -// ErrorResponse is an error from an API request. -type ErrorResponse struct { - // Method is the HTTP verb used in the API request. - Method string - // URL is the HTTP endpoint used in the API request. - URL string - // StatusCode is the HTTP status code returned by the API. - StatusCode int - - // Err is the error parsed from the API response. - Err error `json:"error"` -} - -func (e *ErrorResponse) UnmarshalJSON(data []byte) error { - type Alias ErrorResponse - temp := &struct { - Err string `json:"error"` - *Alias - }{ - Alias: (*Alias)(e), - } - - // Try to extract a structured error from the body, otherwise fall back to using - // the whole body as the error message. - if err := json.Unmarshal(data, &temp); err != nil || temp.Err == "" { - e.Err = errors.New(string(data)) - } else { - e.Err = errors.New(temp.Err) - } - return nil -} - -// Unwrap exposes the underlying error of an ErrorResponse. -func (e *ErrorResponse) Unwrap() error { - return e.Err -} - -// Error describes the error from the API request. -func (e *ErrorResponse) Error() string { - return fmt.Sprintf("%s %s [%d]: %v", e.Method, e.URL, e.StatusCode, e.Err) -} diff --git a/server/playbooks/client/playbook.go b/server/playbooks/client/playbook.go deleted file mode 100644 index 43163e40f6e..00000000000 --- a/server/playbooks/client/playbook.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "fmt" - - "gopkg.in/guregu/null.v4" -) - -// Playbook represents the planning before a playbook run is initiated. -type Playbook struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Public bool `json:"public"` - TeamID string `json:"team_id"` - CreatePublicPlaybookRun bool `json:"create_public_playbook_run"` - CreateAt int64 `json:"create_at"` - DeleteAt int64 `json:"delete_at"` - NumStages int64 `json:"num_stages"` - NumSteps int64 `json:"num_steps"` - Checklists []Checklist `json:"checklists"` - Members []PlaybookMember `json:"members"` - ReminderMessageTemplate string `json:"reminder_message_template"` - ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"` - InvitedUserIDs []string `json:"invited_user_ids"` - InvitedGroupIDs []string `json:"invited_group_ids"` - InviteUsersEnabled bool `json:"invite_users_enabled"` - DefaultOwnerID string `json:"default_owner_id"` - DefaultOwnerEnabled bool `json:"default_owner_enabled"` - BroadcastChannelIDs []string `json:"broadcast_channel_ids"` - BroadcastEnabled bool `json:"broadcast_enabled"` - WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"` - WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled"` - Metrics []PlaybookMetricConfig `json:"metrics"` - CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"` - RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"` - ChannelID string `json:"channel_id" export:"channel_id"` - ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"` -} - -type PlaybookMember struct { - UserID string `json:"user_id"` - Roles []string `json:"roles"` - SchemeRoles []string `json:"scheme_roles"` -} - -const ( - MetricTypeDuration = "metric_duration" - MetricTypeCurrency = "metric_currency" - MetricTypeInteger = "metric_integer" -) - -// Checklist represents a checklist in a playbook -type Checklist struct { - ID string `json:"id"` - Title string `json:"title"` - Items []ChecklistItem `json:"items"` -} - -// ChecklistItem represents an item in a checklist -type ChecklistItem struct { - ID string `json:"id"` - Title string `json:"title"` - State string `json:"state"` - StateModified int64 `json:"state_modified"` - AssigneeID string `json:"assignee_id"` - AssigneeModified int64 `json:"assignee_modified"` - Command string `json:"command"` - CommandLastRun int64 `json:"command_last_run"` - Description string `json:"description"` - LastSkipped int64 `json:"delete_at"` - DueDate int64 `json:"due_date"` - TaskActions []TaskAction `json:"task_actions"` -} - -// TaskAction represents a task action in an item -type TaskAction struct { - Trigger TriggerAction `json:"trigger"` - Actions []TriggerAction `json:"actions"` -} - -// TriggerAction represents a trigger or action in a Task Action -type TriggerAction struct { - Type string `json:"type"` - Payload string `json:"payload"` -} - -// PlaybookCreateOptions specifies the parameters for PlaybooksService.Create method. -type PlaybookCreateOptions struct { - Title string `json:"title"` - Description string `json:"description"` - TeamID string `json:"team_id"` - Public bool `json:"public"` - CreatePublicPlaybookRun bool `json:"create_public_playbook_run"` - Checklists []Checklist `json:"checklists"` - Members []PlaybookMember `json:"members"` - BroadcastChannelID string `json:"broadcast_channel_id"` - ReminderMessageTemplate string `json:"reminder_message_template"` - ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"` - InvitedUserIDs []string `json:"invited_user_ids"` - InvitedGroupIDs []string `json:"invited_group_ids"` - InviteUsersEnabled bool `json:"invite_users_enabled"` - DefaultOwnerID string `json:"default_owner_id"` - DefaultOwnerEnabled bool `json:"default_owner_enabled"` - BroadcastChannelIDs []string `json:"broadcast_channel_ids"` - BroadcastEnabled bool `json:"broadcast_enabled"` - Metrics []PlaybookMetricConfig `json:"metrics"` - CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"` - RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"` - ChannelID string `json:"channel_id" export:"channel_id"` - ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"` -} - -type PlaybookMetricConfig struct { - ID string `json:"id"` - PlaybookID string `json:"playbook_id"` - Title string `json:"title"` - Description string `json:"description"` - Type string `json:"type"` - Target null.Int `json:"target"` -} - -// PlaybookListOptions specifies the optional parameters to the -// PlaybooksService.List method. -type PlaybookListOptions struct { - Sort Sort `url:"sort,omitempty"` - Direction SortDirection `url:"direction,omitempty"` - SearchTeam string `url:"search_term,omitempty"` - WithArchived bool `url:"with_archived,omitempty"` -} - -type GetPlaybooksResults struct { - TotalCount int `json:"total_count"` - PageCount int `json:"page_count"` - HasMore bool `json:"has_more"` - Items []Playbook `json:"items"` -} - -type PlaybookStats struct { - RunsInProgress int `json:"runs_in_progress"` - ParticipantsActive int `json:"participants_active"` - RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"` - RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"` - RunsStartedPerWeek []int `json:"runs_started_per_week"` - RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"` - ActiveRunsPerDay []int `json:"active_runs_per_day"` - ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"` - ActiveParticipantsPerDay []int `json:"active_participants_per_day"` - ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"` - MetricOverallAverage []null.Int `json:"metric_overall_average"` - MetricRollingAverage []null.Int `json:"metric_rolling_average"` - MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"` - MetricValueRange [][]int64 `json:"metric_value_range"` - MetricRollingValues [][]int64 `json:"metric_rolling_values"` - LastXRunNames []string `json:"last_x_run_names"` -} - -type ChannelPlaybookMode int - -const ( - PlaybookRunCreateNewChannel ChannelPlaybookMode = iota - PlaybookRunLinkExistingChannel -) - -var channelPlaybookTypes = [...]string{ - PlaybookRunCreateNewChannel: "create_new_channel", - PlaybookRunLinkExistingChannel: "link_existing_channel", -} - -// String creates the string version of the TelemetryTrack -func (cpm ChannelPlaybookMode) String() string { - return channelPlaybookTypes[cpm] -} - -// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON) -func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) { - return []byte(channelPlaybookTypes[cpm]), nil -} - -// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON) -func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error { - for i, st := range channelPlaybookTypes { - if st == string(text) { - *cpm = ChannelPlaybookMode(i) - return nil - } - } - return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text)) -} diff --git a/server/playbooks/client/playbook_run.go b/server/playbooks/client/playbook_run.go deleted file mode 100644 index 6e3bb7d8818..00000000000 --- a/server/playbooks/client/playbook_run.go +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "time" - - "gopkg.in/guregu/null.v4" -) - -// Me is a constant that refers to the current user, and can be used in various APIs in place of -// explicitly specifying the current user's id. -const Me = "me" - -// PlaybookRun represents a playbook run. -type PlaybookRun struct { - ID string `json:"id"` - Name string `json:"name"` - Summary string `json:"summary"` - SummaryModifiedAt int64 `json:"summary_modified_at"` - OwnerUserID string `json:"owner_user_id"` - ReporterUserID string `json:"reporter_user_id"` - TeamID string `json:"team_id"` - ChannelID string `json:"channel_id"` - CreateAt int64 `json:"create_at"` - EndAt int64 `json:"end_at"` - DeleteAt int64 `json:"delete_at"` - ActiveStage int `json:"active_stage"` - ActiveStageTitle string `json:"active_stage_title"` - PostID string `json:"post_id"` - PlaybookID string `json:"playbook_id"` - Checklists []Checklist `json:"checklists"` - StatusPosts []StatusPost `json:"status_posts"` - CurrentStatus string `json:"current_status"` - LastStatusUpdateAt int64 `json:"last_status_update_at"` - ReminderPostID string `json:"reminder_post_id"` - PreviousReminder time.Duration `json:"previous_reminder"` - ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"` - StatusUpdateEnabled bool `json:"status_update_enabled"` - BroadcastChannelIDs []string `json:"broadcast_channel_ids"` - WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"` - StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"` - StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"` - ReminderMessageTemplate string `json:"reminder_message_template"` - InvitedUserIDs []string `json:"invited_user_ids"` - InvitedGroupIDs []string `json:"invited_group_ids"` - TimelineEvents []TimelineEvent `json:"timeline_events"` - DefaultOwnerID string `json:"default_owner_id"` - WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"` - Retrospective string `json:"retrospective"` - RetrospectivePublishedAt int64 `json:"retrospective_published_at"` - RetrospectiveWasCanceled bool `json:"retrospective_was_canceled"` - RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds"` - RetrospectiveEnabled bool `json:"retrospective_enabled"` - MessageOnJoin string `json:"message_on_join"` - ParticipantIDs []string `json:"participant_ids"` - CategoryName string `json:"category_name"` - MetricsData []RunMetricData `json:"metrics_data"` - CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"` - RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"` -} - -// StatusPost is information added to the playbook run when selecting from the db and sent to the -// client; it is not saved to the db. -type StatusPost struct { - ID string `json:"id"` - CreateAt int64 `json:"create_at"` - DeleteAt int64 `json:"delete_at"` -} - -// StatusPostComplete is the complete status update (post) -// it's similar to StatusPost but with extended info. -type StatusPostComplete struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - Message string `json:"message"` - AuthorUserName string `json:"author_user_name"` -} - -// Metadata tracks ancillary metadata about a playbook run. -type Metadata struct { - ChannelName string `json:"channel_name"` - ChannelDisplayName string `json:"channel_display_name"` - TeamName string `json:"team_name"` - NumParticipants int64 `json:"num_participants"` - TotalPosts int64 `json:"total_posts"` - Followers []string `json:"followers"` -} - -// TimelineEventType describes a type of timeline event. -type TimelineEventType string - -const ( - PlaybookRunCreated TimelineEventType = "incident_created" - TaskStateModified TimelineEventType = "task_state_modified" - StatusUpdated TimelineEventType = "status_updated" - StatusUpdateRequested TimelineEventType = "status_update_requested" - OwnerChanged TimelineEventType = "owner_changed" - AssigneeChanged TimelineEventType = "assignee_changed" - RanSlashCommand TimelineEventType = "ran_slash_command" - EventFromPost TimelineEventType = "event_from_post" - UserJoinedLeft TimelineEventType = "user_joined_left" - PublishedRetrospective TimelineEventType = "published_retrospective" - CanceledRetrospective TimelineEventType = "canceled_retrospective" - RunFinished TimelineEventType = "run_finished" - RunRestored TimelineEventType = "run_restored" - StatusUpdatesEnabled TimelineEventType = "status_updates_enabled" - StatusUpdatesDisabled TimelineEventType = "status_updates_disabled" -) - -// TimelineEvent represents an event recorded to a playbook run's timeline. -type TimelineEvent struct { - ID string `json:"id"` - PlaybookRunID string `json:"playbook_run"` - CreateAt int64 `json:"create_at"` - DeleteAt int64 `json:"delete_at"` - EventAt int64 `json:"event_at"` - EventType TimelineEventType `json:"event_type"` - Summary string `json:"summary"` - Details string `json:"details"` - PostID string `json:"post_id"` - SubjectUserID string `json:"subject_user_id"` - CreatorUserID string `json:"creator_user_id"` -} - -// PlaybookRunCreateOptions specifies the parameters for PlaybookRunService.Create method. -type PlaybookRunCreateOptions struct { - Name string `json:"name"` - OwnerUserID string `json:"owner_user_id"` - TeamID string `json:"team_id"` - ChannelID string `json:"channel_id"` - Description string `json:"description"` - PostID string `json:"post_id"` - PlaybookID string `json:"playbook_id"` - CreatePublicRun *bool `json:"create_public_run"` - Type string `json:"type"` -} - -// RunAction represents the run action settings. Frontend passes this struct to update settings. -type RunAction struct { - BroadcastChannelIDs []string `json:"broadcast_channel_ids"` - WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"` - - StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"` - StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"` -} - -// RetrospectiveUpdate represents the run retrospective info -type RetrospectiveUpdate struct { - Text string `json:"retrospective"` - Metrics []RunMetricData `json:"metrics"` -} - -// Sort enumerates the available fields we can sort on. -type Sort string - -const ( - // SortByCreateAt sorts by the "create_at" field. It is the default. - SortByCreateAt Sort = "create_at" - - // SortByID sorts by the "id" field. - SortByID Sort = "id" - - // SortByName sorts by the "name" field. - SortByName Sort = "name" - - // SortByOwnerUserID sorts by the "owner_user_id" field. - SortByOwnerUserID Sort = "owner_user_id" - - // SortByTeamID sorts by the "team_id" field. - SortByTeamID Sort = "team_id" - - // SortByEndAt sorts by the "end_at" field. - SortByEndAt Sort = "end_at" - - // SortBySteps sorts playbooks by the number of steps in the playbook. - SortBySteps Sort = "steps" - - // SortByStages sorts playbooks by the number of stages in the playbook. - SortByStages Sort = "stages" - - // SortByTitle sorts by the "title" field. - SortByTitle Sort = "title" - - // SortByRuns sorts by the number of times a playbook has been run. - SortByRuns Sort = "runs" -) - -// SortDirection determines whether results are sorted ascending or descending. -type SortDirection string - -const ( - // Desc sorts the results in descending order. - SortDesc SortDirection = "desc" - - // Asc sorts the results in ascending order. - SortAsc SortDirection = "asc" -) - -// PlaybookRunListOptions specifies the optional parameters to the -// PlaybookRunService.List method. -type PlaybookRunListOptions struct { - // TeamID filters playbook runs to those in the given team. - TeamID string `url:"team_id,omitempty"` - - Sort Sort `url:"sort,omitempty"` - Direction SortDirection `url:"direction,omitempty"` - - // Statuses filters by InProgress or Ended; defaults to All when no status specified. - Statuses []Status `url:"statuses,omitempty"` - - // OwnerID filters by owner's Mattermost user ID. Defaults to blank (no filter). Specify "me" for current user. - OwnerID string `url:"owner_user_id,omitempty"` - - // ParticipantID filters playbook runs that have this user as a participant. Defaults to blank (no filter). Specify "me" for current user. - ParticipantID string `url:"participant_id,omitempty"` - - // ParticipantOrFollowerID filters playbook runs that have this user as member or as follower. Defaults to blank (no filter). Specify "me" for current user. - ParticipantOrFollowerID string `url:"participant_or_follower,omitempty"` - - // SearchTerm returns results of the search term and respecting the other header filter options. - // The search term acts as a filter and respects the Sort and Direction fields (i.e., results are - // not returned in relevance order). - SearchTerm string `url:"search_term,omitempty"` - - // PlaybookID filters playbook runs that are derived from this playbook id. - // Defaults to blank (no filter). - PlaybookID string `url:"playbook_id,omitempty"` - - // ActiveGTE filters playbook runs that were active after (or equal) to the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - ActiveGTE int64 `url:"active_gte,omitempty"` - - // ActiveLT filters playbook runs that were active before the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - ActiveLT int64 `url:"active_lt,omitempty"` - - // StartedGTE filters playbook runs that were started after (or equal) to the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - StartedGTE int64 `url:"started_gte,omitempty"` - - // StartedLT filters playbook runs that were started before the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - StartedLT int64 `url:"started_lt,omitempty"` -} - -// PlaybookRunList contains the paginated result. -type PlaybookRunList struct { - TotalCount int `json:"total_count"` - PageCount int `json:"page_count"` - HasMore bool `json:"has_more"` - Items []*PlaybookRun -} - -// Status is the type used to specify the activity status of the playbook run. -type Status string - -const ( - StatusInProgress Status = "InProgress" - StatusFinished Status = "Finished" -) - -type GetPlaybookRunsResults struct { - TotalCount int `json:"total_count"` - PageCount int `json:"page_count"` - HasMore bool `json:"has_more"` - Items []PlaybookRun `json:"items"` -} - -// StatusUpdateOptions are the fields required to update a playbook run's status -type StatusUpdateOptions struct { - Message string `json:"message"` - Reminder time.Duration `json:"reminder"` - FinishRun bool `json:"finish_run"` -} - -type RunMetricData struct { - MetricConfigID string `json:"metric_config_id"` - Value null.Int `json:"value"` -} - -// OwnerInfo holds the summary information of a owner. -type OwnerInfo struct { - UserID string `json:"user_id"` - Username string `json:"username"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Nickname string `json:"nickname"` -} diff --git a/server/playbooks/client/playbook_runs.go b/server/playbooks/client/playbook_runs.go deleted file mode 100644 index 949a4006f31..00000000000 --- a/server/playbooks/client/playbook_runs.go +++ /dev/null @@ -1,375 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "context" - "fmt" - "net/http" - "time" -) - -// PlaybookRunService handles communication with the playbook run related -// methods of the Playbooks API. -type PlaybookRunService struct { - client *Client -} - -// Get a playbook run. -func (s *PlaybookRunService) Get(ctx context.Context, playbookRunID string) (*PlaybookRun, error) { - playbookRunURL := fmt.Sprintf("runs/%s", playbookRunID) - req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil) - if err != nil { - return nil, err - } - - playbookRun := new(PlaybookRun) - resp, err := s.client.do(ctx, req, playbookRun) - if err != nil { - return nil, err - } - resp.Body.Close() - - return playbookRun, nil -} - -// GetByChannelID gets a playbook run by ChannelID. -func (s *PlaybookRunService) GetByChannelID(ctx context.Context, channelID string) (*PlaybookRun, error) { - channelURL := fmt.Sprintf("runs/channel/%s", channelID) - req, err := s.client.newRequest(http.MethodGet, channelURL, nil) - if err != nil { - return nil, err - } - - playbookRun := new(PlaybookRun) - resp, err := s.client.do(ctx, req, playbookRun) - if err != nil { - return nil, err - } - resp.Body.Close() - - return playbookRun, nil -} - -// Get a playbook run's metadata. -func (s *PlaybookRunService) GetMetadata(ctx context.Context, playbookRunID string) (*Metadata, error) { - playbookRunURL := fmt.Sprintf("runs/%s/metadata", playbookRunID) - req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil) - if err != nil { - return nil, err - } - - playbookRun := new(Metadata) - resp, err := s.client.do(ctx, req, playbookRun) - if err != nil { - return nil, err - } - resp.Body.Close() - - return playbookRun, nil -} - -// Get all playbook status updates. -func (s *PlaybookRunService) GetStatusUpdates(ctx context.Context, playbookRunID string) ([]StatusPostComplete, error) { - playbookRunURL := fmt.Sprintf("runs/%s/status-updates", playbookRunID) - req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil) - if err != nil { - return nil, err - } - - var statusUpdates []StatusPostComplete - resp, err := s.client.do(ctx, req, &statusUpdates) - if err != nil { - return nil, err - } - resp.Body.Close() - - return statusUpdates, nil -} - -// List the playbook runs. -func (s *PlaybookRunService) List(ctx context.Context, page, perPage int, opts PlaybookRunListOptions) (*GetPlaybookRunsResults, error) { - playbookRunURL := "runs" - playbookRunURL, err := addOptions(playbookRunURL, opts) - if err != nil { - return nil, fmt.Errorf("failed to build options: %w", err) - } - playbookRunURL, err = addPaginationOptions(playbookRunURL, page, perPage) - if err != nil { - return nil, fmt.Errorf("failed to build pagination options: %w", err) - } - - req, err := s.client.newRequest(http.MethodGet, playbookRunURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to build request: %w", err) - } - - result := &GetPlaybookRunsResults{} - resp, err := s.client.do(ctx, req, result) - if err != nil { - return nil, fmt.Errorf("failed to execute request: %w", err) - } - resp.Body.Close() - - return result, nil -} - -// Create a playbook run. -func (s *PlaybookRunService) Create(ctx context.Context, opts PlaybookRunCreateOptions) (*PlaybookRun, error) { - playbookRunURL := "runs" - req, err := s.client.newRequest(http.MethodPost, playbookRunURL, opts) - if err != nil { - return nil, err - } - - playbookRun := new(PlaybookRun) - resp, err := s.client.do(ctx, req, playbookRun) - if err != nil { - return nil, err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return nil, fmt.Errorf("expected status code %d", http.StatusCreated) - } - - return playbookRun, nil -} - -func (s *PlaybookRunService) UpdateStatus(ctx context.Context, playbookRunID string, message string, reminderInSeconds int64) error { - updateURL := fmt.Sprintf("runs/%s/status", playbookRunID) - opts := StatusUpdateOptions{ - Message: message, - Reminder: time.Duration(reminderInSeconds), - } - req, err := s.client.newRequest(http.MethodPost, updateURL, opts) - if err != nil { - return err - } - - resp, err := s.client.do(ctx, req, nil) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status code %d", http.StatusOK) - } - - return nil -} - -func (s *PlaybookRunService) RequestUpdate(ctx context.Context, playbookRunID, userID string) error { - requestURL := fmt.Sprintf("runs/%s/request-update", playbookRunID) - req, err := s.client.newRequest(http.MethodPost, requestURL, nil) - if err != nil { - return err - } - - resp, err := s.client.do(ctx, req, nil) - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status code %d", http.StatusOK) - } - - return err -} - -func (s *PlaybookRunService) Finish(ctx context.Context, playbookRunID string) error { - finishURL := fmt.Sprintf("runs/%s/finish", playbookRunID) - req, err := s.client.newRequest(http.MethodPut, finishURL, nil) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - - return nil -} - -func (s *PlaybookRunService) CreateChecklist(ctx context.Context, playbookRunID string, checklist Checklist) error { - createURL := fmt.Sprintf("runs/%s/checklists", playbookRunID) - req, err := s.client.newRequest(http.MethodPost, createURL, checklist) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) RemoveChecklist(ctx context.Context, playbookRunID string, checklistNumber int) error { - createURL := fmt.Sprintf("runs/%s/checklists/%d", playbookRunID, checklistNumber) - req, err := s.client.newRequest(http.MethodDelete, createURL, nil) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) RenameChecklist(ctx context.Context, playbookRunID string, checklistNumber int, newTitle string) error { - createURL := fmt.Sprintf("runs/%s/checklists/%d/rename", playbookRunID, checklistNumber) - req, err := s.client.newRequest(http.MethodPut, createURL, struct{ Title string }{newTitle}) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) AddChecklistItem(ctx context.Context, playbookRunID string, checklistNumber int, checklistItem ChecklistItem) error { - addURL := fmt.Sprintf("runs/%s/checklists/%d/add", playbookRunID, checklistNumber) - req, err := s.client.newRequest(http.MethodPost, addURL, checklistItem) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) MoveChecklist(ctx context.Context, playbookRunID string, sourceChecklistIdx, destChecklistIdx int) error { - createURL := fmt.Sprintf("runs/%s/checklists/move", playbookRunID) - body := struct { - SourceChecklistIdx int `json:"source_checklist_idx"` - DestChecklistIdx int `json:"dest_checklist_idx"` - }{sourceChecklistIdx, destChecklistIdx} - - req, err := s.client.newRequest(http.MethodPost, createURL, body) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) MoveChecklistItem(ctx context.Context, playbookRunID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error { - createURL := fmt.Sprintf("runs/%s/checklists/move-item", playbookRunID) - body := struct { - SourceChecklistIdx int `json:"source_checklist_idx"` - SourceItemIdx int `json:"source_item_idx"` - DestChecklistIdx int `json:"dest_checklist_idx"` - DestItemIdx int `json:"dest_item_idx"` - }{sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx} - - req, err := s.client.newRequest(http.MethodPost, createURL, body) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -// UpdateRetrospective updates the run's retrospective info -func (s *PlaybookRunService) UpdateRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error { - createURL := fmt.Sprintf("runs/%s/retrospective", playbookRunID) - req, err := s.client.newRequest(http.MethodPost, createURL, retroUpdate) - if err != nil { - return err - } - - resp, err := s.client.do(ctx, req, nil) - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status code %d", http.StatusOK) - } - - return err -} - -// PublishRetrospective publishes the run's retrospective -func (s *PlaybookRunService) PublishRetrospective(ctx context.Context, playbookRunID, userID string, retroUpdate RetrospectiveUpdate) error { - createURL := fmt.Sprintf("runs/%s/retrospective/publish", playbookRunID) - req, err := s.client.newRequest(http.MethodPost, createURL, retroUpdate) - if err != nil { - return err - } - - resp, err := s.client.do(ctx, req, nil) - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected status code %d", http.StatusOK) - } - - return err -} - -func (s *PlaybookRunService) SetItemAssignee(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, assigneeID string) error { - createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/assignee", playbookRunID, checklistIdx, itemIdx) - body := struct { - AssigneeID string `json:"assignee_id"` - }{assigneeID} - - req, err := s.client.newRequest(http.MethodPut, createURL, body) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) SetItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, newCommand string) error { - createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/command", playbookRunID, checklistIdx, itemIdx) - body := struct { - Command string `json:"command"` - }{newCommand} - - req, err := s.client.newRequest(http.MethodPut, createURL, body) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) RunItemCommand(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int) error { - createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/run", playbookRunID, checklistIdx, itemIdx) - - req, err := s.client.newRequest(http.MethodPost, createURL, nil) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -func (s *PlaybookRunService) SetItemDueDate(ctx context.Context, playbookRunID string, checklistIdx int, itemIdx int, duedate int64) error { - createURL := fmt.Sprintf("runs/%s/checklists/%d/item/%d/duedate", playbookRunID, checklistIdx, itemIdx) - body := struct { - DueDate int64 `json:"due_date"` - }{duedate} - - req, err := s.client.newRequest(http.MethodPut, createURL, body) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - return err -} - -// Get a playbook run. -func (s *PlaybookRunService) GetOwners(ctx context.Context) ([]OwnerInfo, error) { - req, err := s.client.newRequest(http.MethodGet, "runs/owners", nil) - if err != nil { - return nil, err - } - - owners := make([]OwnerInfo, 0) - resp, err := s.client.do(ctx, req, &owners) - if err != nil { - return nil, err - } - resp.Body.Close() - - return owners, nil -} diff --git a/server/playbooks/client/playbook_runs_test.go b/server/playbooks/client/playbook_runs_test.go deleted file mode 100644 index 015250d89f8..00000000000 --- a/server/playbooks/client/playbook_runs_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client_test - -import ( - "context" - "fmt" - "log" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" -) - -func ExamplePlaybookRunService_Get() { - ctx := context.Background() - - client4 := model.NewAPIv4Client("http://localhost:8065") - client4.Login(context.Background(), "test@example.com", "testtest") - - c, err := client.New(client4) - if err != nil { - log.Fatal(err) - } - - playbookRunID := "h4n3h7s1qjf5pkis4dn6cuxgwa" - playbookRun, err := c.PlaybookRuns.Get(ctx, playbookRunID) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name) -} - -func ExamplePlaybookRunService_List() { - ctx := context.Background() - - client4 := model.NewAPIv4Client("http://localhost:8065") - _, _, err := client4.Login(context.Background(), "test@example.com", "testtest") - if err != nil { - log.Fatal(err.Error()) - } - - teams, _, err := client4.GetAllTeams(context.Background(), "", 0, 1) - if err != nil { - log.Fatal(err.Error()) - } - if len(teams) == 0 { - log.Fatal("no teams for this user") - } - - c, err := client.New(client4) - if err != nil { - log.Fatal(err) - } - - var playbookRuns []client.PlaybookRun - for page := 0; ; page++ { - result, err := c.PlaybookRuns.List(ctx, page, 100, client.PlaybookRunListOptions{ - TeamID: teams[0].Id, - Sort: client.SortByCreateAt, - Direction: client.SortDesc, - }) - if err != nil { - log.Fatal(err) - } - - playbookRuns = append(playbookRuns, result.Items...) - if !result.HasMore { - break - } - } - - for _, playbookRun := range playbookRuns { - fmt.Printf("Playbook Run Name: %s\n", playbookRun.Name) - } -} diff --git a/server/playbooks/client/playbooks.go b/server/playbooks/client/playbooks.go deleted file mode 100644 index ffbf52683c7..00000000000 --- a/server/playbooks/client/playbooks.go +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "bytes" - "context" - "fmt" - "io/ioutil" - "net/http" - - "github.com/pkg/errors" -) - -// PlaybooksService handles communication with the playbook related -// methods of the Playbook API. -type PlaybooksService struct { - client *Client -} - -// Get a playbook. -func (s *PlaybooksService) Get(ctx context.Context, playbookID string) (*Playbook, error) { - playbookURL := fmt.Sprintf("playbooks/%s", playbookID) - req, err := s.client.newRequest(http.MethodGet, playbookURL, nil) - if err != nil { - return nil, err - } - - playbook := new(Playbook) - resp, err := s.client.do(ctx, req, playbook) - if err != nil { - return nil, err - } - resp.Body.Close() - - return playbook, nil -} - -// List the playbooks. -func (s *PlaybooksService) List(ctx context.Context, teamId string, page, perPage int, opts PlaybookListOptions) (*GetPlaybooksResults, error) { - playbookURL := "playbooks" - playbookURL, err := addOption(playbookURL, "team_id", teamId) - if err != nil { - return nil, fmt.Errorf("failed to build options: %w", err) - } - - playbookURL, err = addPaginationOptions(playbookURL, page, perPage) - if err != nil { - return nil, fmt.Errorf("failed to build pagination options: %w", err) - } - - playbookURL, err = addOptions(playbookURL, opts) - if err != nil { - return nil, fmt.Errorf("failed to build options: %w", err) - } - - req, err := s.client.newRequest(http.MethodGet, playbookURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to build request: %w", err) - } - - result := &GetPlaybooksResults{} - resp, err := s.client.do(ctx, req, result) - if err != nil { - return nil, fmt.Errorf("failed to execute request: %w", err) - } - resp.Body.Close() - - return result, nil -} - -// Create a playbook. Returns the id of the newly created playbook -func (s *PlaybooksService) Create(ctx context.Context, opts PlaybookCreateOptions) (string, error) { - // For ease of use set the default if not specificed so it doesn't just error - if opts.ReminderTimerDefaultSeconds == 0 { - opts.ReminderTimerDefaultSeconds = 86400 - } - - playbookURL := "playbooks" - req, err := s.client.newRequest(http.MethodPost, playbookURL, opts) - if err != nil { - return "", err - } - - var result struct { - ID string `json:"id"` - } - resp, err := s.client.do(ctx, req, &result) - if err != nil { - return "", err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return "", fmt.Errorf("expected status code %d", http.StatusCreated) - } - - return result.ID, nil -} - -func (s *PlaybooksService) Update(ctx context.Context, playbook Playbook) error { - updateURL := fmt.Sprintf("playbooks/%s", playbook.ID) - req, err := s.client.newRequest(http.MethodPut, updateURL, playbook) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - - return nil -} - -func (s *PlaybooksService) Archive(ctx context.Context, playbookID string) error { - updateURL := fmt.Sprintf("playbooks/%s", playbookID) - req, err := s.client.newRequest(http.MethodDelete, updateURL, nil) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - - return nil -} - -func (s *PlaybooksService) Export(ctx context.Context, playbookID string) ([]byte, error) { - url := fmt.Sprintf("playbooks/%s/export", playbookID) - req, err := s.client.newRequest(http.MethodGet, url, nil) - if err != nil { - return nil, err - } - - resp, err := s.client.client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - result, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("expected status code %d", http.StatusOK) - } - - return result, nil -} - -// Duplicate a playbook. Returns the id of the newly created playbook -func (s *PlaybooksService) Duplicate(ctx context.Context, playbookID string) (string, error) { - url := fmt.Sprintf("playbooks/%s/duplicate", playbookID) - req, err := s.client.newRequest(http.MethodPost, url, nil) - if err != nil { - return "", err - } - - var result struct { - ID string `json:"id"` - } - resp, err := s.client.do(ctx, req, &result) - if err != nil { - return "", err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return "", fmt.Errorf("expected status code %d", http.StatusCreated) - } - - return result.ID, nil -} - -// Imports a playbook. Returns the id of the newly created playbook -func (s *PlaybooksService) Import(ctx context.Context, toImport []byte, team string) (string, error) { - url := "playbooks/import?team_id=" + team - u, err := s.client.BaseURL.Parse(buildAPIURL(url)) - if err != nil { - return "", errors.Wrapf(err, "invalid endpoint %s", url) - } - req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(toImport)) - if err != nil { - return "", errors.Wrapf(err, "failed to create http request for import") - } - req.Header.Set("Content-Type", "application/json") - - var result struct { - ID string `json:"id"` - } - resp, err := s.client.do(ctx, req, &result) - if err != nil { - return "", err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return "", fmt.Errorf("expected status code %d", http.StatusCreated) - } - - return result.ID, nil -} - -func (s *PlaybooksService) Stats(ctx context.Context, playbookID string) (*PlaybookStats, error) { - playbookStatsURL := fmt.Sprintf("stats/playbook?playbook_id=%s", playbookID) - req, err := s.client.newRequest(http.MethodGet, playbookStatsURL, nil) - if err != nil { - return nil, err - } - - stats := new(PlaybookStats) - resp, err := s.client.do(ctx, req, stats) - if err != nil { - return nil, err - } - resp.Body.Close() - - return stats, nil -} - -func (s *PlaybooksService) AutoFollow(ctx context.Context, playbookID string, userID string) error { - followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID) - req, err := s.client.newRequest(http.MethodPut, followsURL, nil) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - return nil -} - -func (s *PlaybooksService) AutoUnfollow(ctx context.Context, playbookID string, userID string) error { - followsURL := fmt.Sprintf("playbooks/%s/autofollows/%s", playbookID, userID) - req, err := s.client.newRequest(http.MethodDelete, followsURL, nil) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - return nil -} - -func (s *PlaybooksService) GetAutoFollows(ctx context.Context, playbookID string) ([]string, error) { - autofollowsURL := fmt.Sprintf("playbooks/%s/autofollows", playbookID) - req, err := s.client.newRequest(http.MethodGet, autofollowsURL, nil) - if err != nil { - return nil, err - } - - var followers []string - resp, err := s.client.do(ctx, req, &followers) - if err != nil { - return nil, err - } - resp.Body.Close() - - return followers, nil -} diff --git a/server/playbooks/client/playbooks_test.go b/server/playbooks/client/playbooks_test.go deleted file mode 100644 index c3f46df4cbc..00000000000 --- a/server/playbooks/client/playbooks_test.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client_test - -import ( - "context" - "fmt" - "log" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" -) - -func ExamplePlaybooksService_Get() { - ctx := context.Background() - - client4 := model.NewAPIv4Client("http://localhost:8065") - client4.Login(context.Background(), "test@example.com", "testtest") - - c, err := client.New(client4) - if err != nil { - log.Fatal(err) - } - - playbookID := "h4n3h7s1qjf5pkis4dn6cuxgwa" - playbook, err := c.Playbooks.Get(ctx, playbookID) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Playbook Name: %s\n", playbook.Title) -} - -func ExamplePlaybooksService_List() { - ctx := context.Background() - - client4 := model.NewAPIv4Client("http://localhost:8065") - _, _, err := client4.Login(context.Background(), "test@example.com", "testtest") - if err != nil { - log.Fatal(err.Error()) - } - - teams, _, err := client4.GetAllTeams(context.Background(), "", 0, 1) - if err != nil { - log.Fatal(err.Error()) - } - if len(teams) == 0 { - log.Fatal("no teams for this user") - } - - c, err := client.New(client4) - if err != nil { - log.Fatal(err) - } - - var playbooks []client.Playbook - for page := 0; ; page++ { - result, err := c.Playbooks.List(ctx, teams[0].Id, page, 100, client.PlaybookListOptions{ - Sort: client.SortByCreateAt, - Direction: client.SortDesc, - }) - if err != nil { - log.Fatal(err) - } - - playbooks = append(playbooks, result.Items...) - if !result.HasMore { - break - } - } - - for _, playbook := range playbooks { - fmt.Printf("Playbook Name: %s\n", playbook.Title) - } -} diff --git a/server/playbooks/client/reminder.go b/server/playbooks/client/reminder.go deleted file mode 100644 index 56a1167b30a..00000000000 --- a/server/playbooks/client/reminder.go +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -type ReminderResetPayload struct { - NewReminderSeconds int `json:"new_reminder_seconds"` -} diff --git a/server/playbooks/client/reminders.go b/server/playbooks/client/reminders.go deleted file mode 100644 index 76134a8c978..00000000000 --- a/server/playbooks/client/reminders.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" -) - -type RemindersService struct { - client *Client -} - -func (s *RemindersService) Reset(ctx context.Context, playbookRunID string, payload ReminderResetPayload) error { - resetURL := fmt.Sprintf("runs/%s/reminder", playbookRunID) - - req, err := s.client.newRequest(http.MethodPost, resetURL, payload) - if err != nil { - return err - } - - resp, err := s.client.do(ctx, req, ioutil.Discard) - if err != nil { - return err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusNoContent { - return fmt.Errorf("expected status code %d", http.StatusNoContent) - } - - return nil -} diff --git a/server/playbooks/client/settings.go b/server/playbooks/client/settings.go deleted file mode 100644 index e2e9a699c2b..00000000000 --- a/server/playbooks/client/settings.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "context" - "net/http" -) - -type GlobalSettings struct { - // EnableExperimentalFeatures is a read-only field set to true when experimental features - // are enabled. Changing this field requires access to the system console plugin - // configuration. - EnableExperimentalFeatures bool `json:"enable_experimental_features"` -} - -// SettingsService handles communication with the settings related methods. -type SettingsService struct { - client *Client -} - -// Get the configured settings. -func (s *SettingsService) Get(ctx context.Context) (*GlobalSettings, error) { - settingsURL := "settings" - req, err := s.client.newRequest(http.MethodGet, settingsURL, nil) - if err != nil { - return nil, err - } - - settings := new(GlobalSettings) - resp, err := s.client.do(ctx, req, settings) - if err != nil { - return nil, err - } - resp.Body.Close() - - return settings, nil -} - -// Update the configured settings. -func (s *SettingsService) Update(ctx context.Context, settings GlobalSettings) error { - settingsURL := "settings" - req, err := s.client.newRequest(http.MethodPut, settingsURL, settings) - if err != nil { - return err - } - - _, err = s.client.do(ctx, req, nil) - if err != nil { - return err - } - - return nil -} diff --git a/server/playbooks/client/stats.go b/server/playbooks/client/stats.go deleted file mode 100644 index 91cd47289eb..00000000000 --- a/server/playbooks/client/stats.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "context" - "net/http" -) - -// StatsService handles communication with the stats related methods. -type StatsService struct { - client *Client -} - -// PlaybookSiteStats holds the data that we want to expose in system console -type PlaybookSiteStats struct { - TotalPlaybooks int `json:"total_playbooks"` - TotalPlaybookRuns int `json:"total_playbook_runs"` -} - -// Get the stats that should be displayed in system console. -func (s *StatsService) GetSiteStats(ctx context.Context) (*PlaybookSiteStats, error) { - statsURL := "stats/site" - req, err := s.client.newRequest(http.MethodGet, statsURL, nil) - if err != nil { - return nil, err - } - - stats := new(PlaybookSiteStats) - resp, err := s.client.do(ctx, req, stats) - if err != nil { - return nil, err - } - resp.Body.Close() - - return stats, nil -} diff --git a/server/playbooks/client/telemetry.go b/server/playbooks/client/telemetry.go deleted file mode 100644 index 25e7e8aaff5..00000000000 --- a/server/playbooks/client/telemetry.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" -) - -type TelemetryService struct { - client *Client -} - -func (s *TelemetryService) CreateEvent(ctx context.Context, name string, eventType string, properties map[string]interface{}) error { - - payload := struct { - Type string - Name string - Properties map[string]interface{} - }{ - Type: eventType, - Name: name, - Properties: properties, - } - - req, err := s.client.newRequest(http.MethodPost, "telemetry", payload) - if err != nil { - return err - } - - resp, err := s.client.do(ctx, req, nil) - if err != nil { - return err - } - resp.Body.Close() - - if resp.StatusCode != http.StatusNoContent { - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return err - } - return fmt.Errorf("expected status code %d, got %d: %s", http.StatusNoContent, resp.StatusCode, body) - } - - return nil -} diff --git a/server/playbooks/client/unexport_test.go b/server/playbooks/client/unexport_test.go deleted file mode 100644 index 76c252283e8..00000000000 --- a/server/playbooks/client/unexport_test.go +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package client - -var BuildAPIURL = buildAPIURL -var NewClient = newClient diff --git a/server/playbooks/product/api_adapter.go b/server/playbooks/product/api_adapter.go deleted file mode 100644 index 968d2535006..00000000000 --- a/server/playbooks/product/api_adapter.go +++ /dev/null @@ -1,497 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package product - -import ( - "database/sql" - "encoding/json" - "net/http" - "strconv" - "strings" - - "github.com/gorilla/mux" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/mattermost/mattermost/server/public/model" - mm_model "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/channels/app/request" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -// normalizeAppError returns a truly nil error if appErr is nil -// See https://golang.org/doc/faq#nil_error for more details. -func normalizeAppErr(appErr *mm_model.AppError) error { - if appErr == nil { - return nil - } - - if appErr.StatusCode == http.StatusNotFound { - return app.ErrNotFound - } - - return appErr -} - -// serviceAPIAdapter is an adapter that flattens the APIs provided by suite services so they can -// be used as per the Plugin API. -// Note: when supporting a plugin build is no longer needed this adapter may be removed as the Boards app -// can be modified to use the services in modular fashion. -type serviceAPIAdapter struct { - api *playbooksProduct - ctx *request.Context -} - -func newServiceAPIAdapter(api *playbooksProduct) *serviceAPIAdapter { - return &serviceAPIAdapter{ - api: api, - ctx: request.EmptyContext(api.logger), - } -} - -// -// Channels service. -// - -func (a *serviceAPIAdapter) GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) { - channel, appErr := a.api.channelService.GetDirectChannel(userID1, userID2) - return channel, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelByID(channelID string) (*mm_model.Channel, error) { - channel, appErr := a.api.channelService.GetChannelByID(channelID) - return channel, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) { - member, appErr := a.api.channelService.GetChannelMember(channelID, userID) - return member, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) { - opts := &mm_model.ChannelSearchOpts{ - IncludeDeleted: includeDeleted, - } - channels, appErr := a.api.channelService.GetChannelsForTeamForUser(teamID, userID, opts) - return channels, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelSidebarCategories(userID, teamID string) (*mm_model.OrderedSidebarCategories, error) { - categories, appErr := a.api.channelService.GetChannelSidebarCategories(userID, teamID) - return categories, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetChannelMembers(channelID string, page, perPage int) (mm_model.ChannelMembers, error) { - channelMembers, appErr := a.api.channelService.GetChannelMembers(channelID, page, perPage) - return channelMembers, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) CreateChannelSidebarCategory(userID, teamID string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) { - channels, appErr := a.api.channelService.CreateChannelSidebarCategory(userID, teamID, newCategory) - return channels, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdateChannelSidebarCategories(userID, teamID string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) { - channels, appErr := a.api.channelService.UpdateChannelSidebarCategories(userID, teamID, categories) - return channels, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) CreateChannel(channel *mm_model.Channel) error { - _, appErr := a.api.channelService.CreateChannel(channel) - return normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) AddMemberToChannel(channelID, userID string) (*mm_model.ChannelMember, error) { - channelMember, appErr := a.api.channelService.AddChannelMember(channelID, userID) - return channelMember, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) AddUserToChannel(channelID, userID, asUserID string) (*mm_model.ChannelMember, error) { - channel, appErr := a.api.channelService.AddUserToChannel(channelID, userID, asUserID) - return channel, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdateChannelMemberRoles(channelID, userID, newRoles string) (*mm_model.ChannelMember, error) { - channelMember, appErr := a.api.channelService.UpdateChannelMemberRoles(channelID, userID, newRoles) - return channelMember, normalizeAppErr(appErr) -} -func (a *serviceAPIAdapter) DeleteChannelMember(channelID, userID string) error { - appErr := a.api.channelService.DeleteChannelMember(channelID, userID) - return normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) AddChannelMember(channelID, userID string) (*mm_model.ChannelMember, error) { - channelMember, appErr := a.api.channelService.AddChannelMember(channelID, userID) - return channelMember, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) { - channel, appErr := a.api.channelService.GetDirectChannelOrCreate(userID1, userID2) - return channel, normalizeAppErr(appErr) -} - -// -// Post service. -// - -func (a *serviceAPIAdapter) CreatePost(post *mm_model.Post) (*mm_model.Post, error) { - createdPost, appErr := a.api.postService.CreatePost(a.ctx, post) - if appErr != nil { - return nil, normalizeAppErr(appErr) - } - - err := createdPost.ShallowCopy(post) - if err != nil { - return nil, err - } - - return post, nil -} - -func (a *serviceAPIAdapter) GetPostsByIds(postIDs []string) ([]*mm_model.Post, error) { - post, _, appErr := a.api.postService.GetPostsByIds(postIDs) - return post, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) SendEphemeralPost(userID string, post *mm_model.Post) { - *post = *a.api.postService.SendEphemeralPost(a.ctx, userID, post) -} - -func (a *serviceAPIAdapter) GetPost(postID string) (*mm_model.Post, error) { - post, appErr := a.api.postService.GetPost(postID) - return post, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) DeletePost(postID string) (*mm_model.Post, error) { - post, appErr := a.api.postService.DeletePost(a.ctx, postID, playbooksProductID) - return post, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdatePost(post *mm_model.Post) (*mm_model.Post, error) { - post, appErr := a.api.postService.UpdatePost(a.ctx, post, false) - return post, normalizeAppErr(appErr) -} - -// -// User service. -// - -func (a *serviceAPIAdapter) GetUserByID(userID string) (*mm_model.User, error) { - user, appErr := a.api.userService.GetUser(userID) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetUserByUsername(name string) (*mm_model.User, error) { - user, appErr := a.api.userService.GetUserByUsername(name) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetUserByEmail(email string) (*mm_model.User, error) { - user, appErr := a.api.userService.GetUserByEmail(email) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdateUser(user *mm_model.User) (*mm_model.User, error) { - user, appErr := a.api.userService.UpdateUser(a.ctx, user, true) - return user, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) { - user, appErr := a.api.userService.GetUsersFromProfiles(options) - return user, normalizeAppErr(appErr) -} - -// -// Team service. -// - -func (a *serviceAPIAdapter) GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) { - member, appErr := a.api.teamService.GetMember(teamID, userID) - return member, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) { - member, appErr := a.api.teamService.CreateMember(a.ctx, teamID, userID) - return member, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetGroup(groupID string) (*model.Group, error) { - group, appErr := a.api.teamService.GetGroup(groupID) - return group, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetTeam(teamID string) (*mm_model.Team, error) { - team, appErr := a.api.teamService.GetTeam(teamID) - return team, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) GetGroupMemberUsers(groupID string, page, perPage int) ([]*mm_model.User, error) { - users, appErr := a.api.teamService.GetGroupMemberUsers(groupID, page, perPage) - return users, normalizeAppErr(appErr) -} - -// -// Permissions service. -// - -func (a *serviceAPIAdapter) HasPermissionTo(userID string, permission *mm_model.Permission) bool { - return a.api.permissionsService.HasPermissionTo(userID, permission) -} - -func (a *serviceAPIAdapter) HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool { - return a.api.permissionsService.HasPermissionToTeam(userID, teamID, permission) -} - -func (a *serviceAPIAdapter) HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool { - return a.api.permissionsService.HasPermissionToChannel(askingUserID, channelID, permission) -} - -func (a *serviceAPIAdapter) RolesGrantPermission(roleNames []string, permissionID string) bool { - return a.api.permissionsService.RolesGrantPermission(roleNames, permissionID) -} - -// -// Bot service. -// - -func (a *serviceAPIAdapter) EnsureBot(bot *mm_model.Bot) (string, error) { - return a.api.botService.EnsureBot(a.ctx, playbooksProductID, bot) -} - -// -// License service. -// - -func (a *serviceAPIAdapter) GetLicense() *mm_model.License { - return a.api.licenseService.GetLicense() -} - -func (a *serviceAPIAdapter) RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) error { - return normalizeAppErr(a.api.licenseService.RequestTrialLicense(requesterID, users, termsAccepted, receiveEmailsAccepted)) -} - -// -// FileInfoStore service. -// - -func (a *serviceAPIAdapter) GetFileInfo(fileID string) (*mm_model.FileInfo, error) { - fi, appErr := a.api.fileInfoStoreService.GetFileInfo(fileID) - return fi, normalizeAppErr(appErr) -} - -// -// Cluster store. -// - -func (a *serviceAPIAdapter) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) { - a.api.clusterService.PublishWebSocketEvent(playbooksProductID, event, payload, broadcast) -} - -func (a *serviceAPIAdapter) PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error { - return a.api.clusterService.PublishPluginClusterEvent(playbooksProductID, ev, opts) -} - -// -// Cloud service. -// - -func (a *serviceAPIAdapter) GetCloudLimits() (*mm_model.ProductLimits, error) { - return a.api.cloudService.GetCloudLimits() -} - -// -// Config service. -// - -func (a *serviceAPIAdapter) GetConfig() *mm_model.Config { - cfg := a.api.configService.Config().Clone() - cfg.Sanitize() - - return cfg -} - -func (a *serviceAPIAdapter) LoadPluginConfiguration(dest any) error { - finalConfig := make(map[string]any) - - // If we have settings given we override the defaults with them - for setting, value := range a.api.configService.Config().PluginSettings.Plugins[playbooksProductID] { - finalConfig[strings.ToLower(setting)] = value - } - - pluginSettingsJSONBytes, err := json.Marshal(finalConfig) - if err != nil { - logrus.WithError(err).Error("Error marshaling config for plugin") - return nil - } - err = json.Unmarshal(pluginSettingsJSONBytes, dest) - if err != nil { - logrus.WithError(err).Error("Error unmarshaling config for plugin") - } - return nil -} - -func (a *serviceAPIAdapter) SavePluginConfig(pluginConfig map[string]any) error { - cfg := a.GetConfig() - cfg.PluginSettings.Plugins["playbooks"] = pluginConfig - _, _, err := a.api.configService.SaveConfig(cfg, true) - - return normalizeAppErr(err) -} - -// -// KVStore service. -// - -func (a *serviceAPIAdapter) KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) { - b, appErr := a.api.kvStoreService.SetPluginKeyWithOptions(playbooksProductID, key, value, options) - return b, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) KVGet(key string) ([]byte, error) { - data, appErr := a.api.kvStoreService.KVGet(playbooksProductID, key) - return data, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) KVDelete(key string) error { - appErr := a.api.kvStoreService.KVDelete(playbooksProductID, key) - return normalizeAppErr(appErr) -} -func (a *serviceAPIAdapter) KVList(page, perPage int) ([]string, error) { - data, appErr := a.api.kvStoreService.KVList(playbooksProductID, page, perPage) - return data, normalizeAppErr(appErr) -} - -// Get gets the value for the given key into the given interface. -// -// An error is returned only if the value cannot be fetched. A non-existent key will return no -// error, with nothing written to the given interface. -// -// Minimum server version: 5.2 -func (a *serviceAPIAdapter) Get(key string, o interface{}) error { - data, appErr := a.api.kvStoreService.KVGet(playbooksProductID, key) - if appErr != nil { - return normalizeAppErr(appErr) - } - - if len(data) == 0 { - return nil - } - - if bytesOut, ok := o.(*[]byte); ok { - *bytesOut = data - return nil - } - - if err := json.Unmarshal(data, o); err != nil { - return errors.Wrapf(err, "failed to unmarshal value for key %s", key) - } - - return nil -} - -// -// Store service. -// - -func (a *serviceAPIAdapter) GetMasterDB() (*sql.DB, error) { - return a.api.storeService.GetMasterDB(), nil -} - -// DriverName returns the driver name for the datasource. -func (a *serviceAPIAdapter) DriverName() string { - return *a.api.configService.Config().SqlSettings.DriverName -} - -// -// System service. -// - -func (a *serviceAPIAdapter) GetDiagnosticID() string { - return a.api.systemService.GetDiagnosticId() -} - -func (a *serviceAPIAdapter) GetServerVersion() string { - return model.CurrentVersion -} - -// -// Router service. -// - -func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) { - a.api.routerService.RegisterRouter(playbooksProductName, sub) -} - -// -// Preferences service. -// - -func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) { - p, appErr := a.api.preferencesService.GetPreferencesForUser(userID) - return p, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error { - appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences) - return normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error { - appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences) - return normalizeAppErr(appErr) -} - -// -// Session service. -// - -func (a *serviceAPIAdapter) GetSession(sessionID string) (*mm_model.Session, error) { - session, appErr := a.api.sessionService.GetSessionById(sessionID) - return session, normalizeAppErr(appErr) -} - -// -// Frontend service. -// - -func (a *serviceAPIAdapter) OpenInteractiveDialog(dialog model.OpenDialogRequest) error { - return normalizeAppErr(a.api.frontendService.OpenInteractiveDialog(dialog)) -} - -// -// Command service. -// - -func (a *serviceAPIAdapter) Execute(command *mm_model.CommandArgs) (*mm_model.CommandResponse, error) { - user, err := a.GetUserByID(command.UserId) - if err != nil { - return nil, err - } - command.T = i18n.GetUserTranslations(user.Locale) - command.SiteURL = *a.GetConfig().ServiceSettings.SiteURL - response, appErr := a.api.commandService.ExecuteCommand(a.ctx, command) - return response, normalizeAppErr(appErr) -} - -func (a *serviceAPIAdapter) RegisterCommand(command *mm_model.Command) error { - return a.api.commandService.RegisterProductCommand(playbooksProductName, command) -} - -func (a *serviceAPIAdapter) IsEnterpriseReady() bool { - result, _ := strconv.ParseBool(model.BuildEnterpriseReady) - return result -} - -// -// Threads service -// - -func (a *serviceAPIAdapter) RegisterCollectionAndTopic(collectionType, topicType string) error { - return a.api.threadsService.RegisterCollectionAndTopic(playbooksProductID, collectionType, topicType) -} - -// Ensure the adapter implements ServicesAPI. -var _ playbooks.ServicesAPI = &serviceAPIAdapter{} diff --git a/server/playbooks/product/imports/playbooks_imports.go b/server/playbooks/product/imports/playbooks_imports.go deleted file mode 100644 index 4a92bf66203..00000000000 --- a/server/playbooks/product/imports/playbooks_imports.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package imports - -import ( - // Needed to ensure the init() method in the Playbooks product is run. - // This file is copied to the mmserver imports package via makefile. - _ "github.com/mattermost/mattermost/server/v8/playbooks/product" -) diff --git a/server/playbooks/product/logrus.go b/server/playbooks/product/logrus.go deleted file mode 100644 index aae38e06f21..00000000000 --- a/server/playbooks/product/logrus.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package product - -import ( - "fmt" - "io" - - "github.com/mattermost/logr/v2" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/sirupsen/logrus" -) - -// LogrusHook is a logrus.Hook for emitting plugin logs through the RPC API for inclusion in the -// server logs. -// -// To configure the default Logrus logger for use with plugin logging, simply invoke: -// -// pluginapi.ConfigureLogrus(logrus.StandardLogger(), pluginAPIClient) -// -// Alternatively, construct your own logger to pass to pluginapi.ConfigureLogrus. -type LogrusHook struct { - log mlog.LoggerIFace -} - -// NewLogrusHook creates a new instance of LogrusHook. -func NewLogrusHook(log mlog.LoggerIFace) *LogrusHook { - return &LogrusHook{ - log: log, - } -} - -// Levels allows LogrusHook to process any log level. -func (lh *LogrusHook) Levels() []logrus.Level { - return logrus.AllLevels -} - -// Fire proxies logrus entries through the plugin API at the appropriate level. -func (lh *LogrusHook) Fire(entry *logrus.Entry) error { - fields := []logr.Field{} - for key, value := range entry.Data { - field := logr.Field{ - Key: key, - Interface: value, - } - if key == "error" { - field.Type = logr.ErrorType - } - - fields = append(fields, field) - } - - if entry.Caller != nil { - fields = append(fields, - logr.Field{ - Key: "plugin_caller", - String: fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line), - }) - } - - switch entry.Level { - case logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel: - lh.log.Error(entry.Message, fields...) - case logrus.WarnLevel: - lh.log.Warn(entry.Message, fields...) - case logrus.InfoLevel: - lh.log.Info(entry.Message, fields...) - case logrus.DebugLevel, logrus.TraceLevel: - lh.log.Debug(entry.Message, fields...) - } - - return nil -} - -// ConfigureLogrus configures the given logrus logger with a hook to proxy through the RPC API, -// discarding the default output to avoid duplicating the events across the standard STDOUT proxy. -func ConfigureLogrus(logger *logrus.Logger, log mlog.LoggerIFace) { - hook := NewLogrusHook(log) - logger.Hooks.Add(hook) - logger.SetOutput(io.Discard) - logrus.SetReportCaller(true) - - // By default, log everything to the server, and let it decide what gets through. - logrus.SetLevel(logrus.TraceLevel) -} diff --git a/server/playbooks/product/playbooks_product.go b/server/playbooks/product/playbooks_product.go deleted file mode 100644 index c8c5db64ed9..00000000000 --- a/server/playbooks/product/playbooks_product.go +++ /dev/null @@ -1,818 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package product - -import ( - "fmt" - "net/http" - "os" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost/server/public/shared/mlog" - mmapp "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/product" - "github.com/mattermost/mattermost/server/v8/playbooks/product/pluginapi/cluster" - "github.com/mattermost/mattermost/server/v8/playbooks/server/api" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/command" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/enterprise" - "github.com/mattermost/mattermost/server/v8/playbooks/server/metrics" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/mattermost/mattermost/server/v8/playbooks/server/scheduler" - "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore" - "github.com/mattermost/mattermost/server/v8/playbooks/server/telemetry" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - playbooksProductName = "playbooks" - playbooksProductID = "playbooks" -) - -const ( - updateMetricsTaskFrequency = 15 * time.Minute - - metricsExposePort = ":9093" - - // Topic represents a start of a thread. In playbooks we support 2 types of topics: - // status topic - indicating the start of the thread below status update and - // task topic - indicating the start of the thread below task(checklist item) - TopicTypeStatus = "status" - TopicTypeTask = "task" - - // Collection is a group of topics and their corresponding threads. - // In Playbooks we support a single type of collection - a run - CollectionTypeRun = "run" -) - -const ServerKey product.ServiceKey = "server" - -const ( - rudderDataplaneURL = "https://pdat.matterlytics.com" - rudderKeyProd = "1ag0Mv7LPf5uJNhcnKomqg0ENFd" - rudderKeyTest = "1Zu3mOF6U6M9zeaJsfmmhYigWLt" - - // These are placeholders to allow the existing release pipelines to run without failing to - // insert the values that are now hard-coded above. Remove this once we converge on the - // unified delivery pipeline in GitHub. - _ = "placeholder_rudder_dataplane_url" - _ = "placeholder_playbooks_rudder_key" -) - -var errServiceTypeAssert = errors.New("type assertion failed") - -type TelemetryClient interface { - app.PlaybookRunTelemetry - app.PlaybookTelemetry - app.GenericTelemetry - bot.Telemetry - app.UserInfoTelemetry - app.ChannelActionTelemetry - app.CategoryTelemetry - Enable() error - Disable() error -} - -func init() { - product.RegisterProduct(playbooksProductName, product.Manifest{ - Initializer: newPlaybooksProduct, - Dependencies: map[product.ServiceKey]struct{}{ - product.TeamKey: {}, - product.ChannelKey: {}, - product.UserKey: {}, - product.PostKey: {}, - product.BotKey: {}, - product.ClusterKey: {}, - product.ConfigKey: {}, - product.LogKey: {}, - product.LicenseKey: {}, - product.FilestoreKey: {}, - product.FileInfoStoreKey: {}, - product.RouterKey: {}, - product.CloudKey: {}, - product.KVStoreKey: {}, - product.StoreKey: {}, - product.SystemKey: {}, - product.PreferencesKey: {}, - product.SessionKey: {}, - product.FrontendKey: {}, - product.CommandKey: {}, - product.ThreadsKey: {}, - }, - }) -} - -type playbooksProduct struct { - server *mmapp.Server - teamService product.TeamService - channelService product.ChannelService - userService product.UserService - postService product.PostService - permissionsService product.PermissionService - botService product.BotService - clusterService product.ClusterService - configService product.ConfigService - logger mlog.LoggerIFace - licenseService product.LicenseService - filestoreService product.FilestoreService - fileInfoStoreService product.FileInfoStoreService - routerService product.RouterService - cloudService product.CloudService - kvStoreService product.KVStoreService - storeService product.StoreService - systemService product.SystemService - preferencesService product.PreferencesService - hooksService product.HooksService - sessionService product.SessionService - frontendService product.FrontendService - commandService product.CommandService - threadsService product.ThreadsService - - handler *api.Handler - config *config.ServiceImpl - playbookRunService app.PlaybookRunService - playbookService app.PlaybookService - permissions *app.PermissionsService - channelActionService app.ChannelActionService - categoryService app.CategoryService - bot *bot.Bot - userInfoStore app.UserInfoStore - telemetryClient TelemetryClient - licenseChecker app.LicenseChecker - metricsService *metrics.Metrics - playbookStore app.PlaybookStore - playbookRunStore app.PlaybookRunStore - metricsServer *metrics.Service - metricsUpdaterTask *scheduler.ScheduledTask - - serviceAdapter playbooks.ServicesAPI -} - -func newPlaybooksProduct(services map[product.ServiceKey]interface{}) (product.Product, error) { - playbooks := &playbooksProduct{} - err := playbooks.setProductServices(services) - if err != nil { - return nil, err - } - - playbooks.server = services[ServerKey].(*mmapp.Server) - - playbooks.serviceAdapter = newServiceAPIAdapter(playbooks) - - return playbooks, nil -} - -func (pp *playbooksProduct) setProductServices(services map[product.ServiceKey]interface{}) error { - for key, service := range services { - switch key { - case product.TeamKey: - teamService, ok := service.(product.TeamService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.teamService = teamService - case product.ChannelKey: - channelService, ok := service.(product.ChannelService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.channelService = channelService - case product.UserKey: - userService, ok := service.(product.UserService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.userService = userService - case product.PostKey: - postService, ok := service.(product.PostService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.postService = postService - case product.PermissionsKey: - permissionsService, ok := service.(product.PermissionService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.permissionsService = permissionsService - case product.BotKey: - botService, ok := service.(product.BotService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.botService = botService - case product.ClusterKey: - clusterService, ok := service.(product.ClusterService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.clusterService = clusterService - case product.ConfigKey: - configService, ok := service.(product.ConfigService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.configService = configService - case product.LogKey: - logger, ok := service.(mlog.LoggerIFace) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.logger = logger.With(mlog.String("product", playbooksProductName)) - case product.LicenseKey: - licenseService, ok := service.(product.LicenseService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.licenseService = licenseService - case product.FilestoreKey: - filestoreService, ok := service.(product.FilestoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.filestoreService = filestoreService - case product.FileInfoStoreKey: - fileInfoStoreService, ok := service.(product.FileInfoStoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.fileInfoStoreService = fileInfoStoreService - case product.RouterKey: - routerService, ok := service.(product.RouterService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.routerService = routerService - case product.CloudKey: - cloudService, ok := service.(product.CloudService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.cloudService = cloudService - case product.KVStoreKey: - kvStoreService, ok := service.(product.KVStoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.kvStoreService = kvStoreService - case product.StoreKey: - storeService, ok := service.(product.StoreService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.storeService = storeService - case product.SystemKey: - systemService, ok := service.(product.SystemService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.systemService = systemService - case product.PreferencesKey: - preferencesService, ok := service.(product.PreferencesService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.preferencesService = preferencesService - case product.HooksKey: - hooksService, ok := service.(product.HooksService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.hooksService = hooksService - case product.SessionKey: - sessionService, ok := service.(product.SessionService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.sessionService = sessionService - case product.FrontendKey: - frontendService, ok := service.(product.FrontendService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.frontendService = frontendService - case product.CommandKey: - commandService, ok := service.(product.CommandService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.commandService = commandService - case product.ThreadsKey: - threadsService, ok := service.(product.ThreadsService) - if !ok { - return fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert) - } - pp.threadsService = threadsService - } - } - return nil -} - -func (pp *playbooksProduct) Start() error { - logger := logrus.StandardLogger() - ConfigureLogrus(logger, pp.logger) - - botID, err := pp.serviceAdapter.EnsureBot(&model.Bot{ - Username: "playbooks", - DisplayName: "Playbooks", - Description: "Playbooks bot.", - OwnerId: "playbooks", - }) - if err != nil { - return errors.Wrapf(err, "failed to ensure bot") - } - - pp.config = config.NewConfigService(pp.serviceAdapter) - err = pp.config.UpdateConfiguration(func(c *config.Configuration) { - c.BotUserID = botID - c.AdminLogLevel = "debug" - }) - if err != nil { - return errors.Wrapf(err, "failed save bot to config") - } - - pp.handler = api.NewHandler(pp.config) - - rudderWriteKey := "" - switch model.GetServiceEnvironment() { - case model.ServiceEnvironmentProduction: - rudderWriteKey = rudderKeyProd - case model.ServiceEnvironmentTest: - rudderWriteKey = rudderKeyTest - case model.ServiceEnvironmentDev: - } - - if rudderWriteKey == "" { - logrus.Warn("Rudder credentials are not set. Disabling analytics.") - pp.telemetryClient = &telemetry.NoopTelemetry{} - } else { - logrus.Info("Rudder credentials are set. Enabling analytics.") - diagnosticID := pp.serviceAdapter.GetDiagnosticID() - serverVersion := pp.serviceAdapter.GetServerVersion() - pp.telemetryClient, err = telemetry.NewRudder(rudderDataplaneURL, rudderWriteKey, diagnosticID, serverVersion) - if err != nil { - return errors.Wrapf(err, "failed init telemetry client") - } - } - - toggleTelemetry := func() { - diagnosticsFlag := pp.serviceAdapter.GetConfig().LogSettings.EnableDiagnostics - telemetryEnabled := diagnosticsFlag != nil && *diagnosticsFlag - - if telemetryEnabled { - if err = pp.telemetryClient.Enable(); err != nil { - logrus.WithError(err).Error("Telemetry could not be enabled") - } - return - } - - if err = pp.telemetryClient.Disable(); err != nil { - logrus.WithError(err).Error("Telemetry could not be disabled") - } - } - - toggleTelemetry() - pp.config.RegisterConfigChangeListener(toggleTelemetry) - - apiClient := sqlstore.NewClient(pp.serviceAdapter) - pp.bot = bot.New(pp.serviceAdapter, pp.config.GetConfiguration().BotUserID, pp.config, pp.telemetryClient) - scheduler := cluster.GetJobOnceScheduler(pp.serviceAdapter) - - sqlStore, err := sqlstore.New(apiClient, scheduler) - if err != nil { - return errors.Wrapf(err, "failed creating the SQL store") - } - - pp.playbookRunStore = sqlstore.NewPlaybookRunStore(apiClient, sqlStore) - pp.playbookStore = sqlstore.NewPlaybookStore(apiClient, sqlStore) - statsStore := sqlstore.NewStatsStore(apiClient, sqlStore) - pp.userInfoStore = sqlstore.NewUserInfoStore(sqlStore) - channelActionStore := sqlstore.NewChannelActionStore(apiClient, sqlStore) - categoryStore := sqlstore.NewCategoryStore(apiClient, sqlStore) - - pp.handler = api.NewHandler(pp.config) - - pp.playbookService = app.NewPlaybookService(pp.playbookStore, pp.bot, pp.telemetryClient, pp.serviceAdapter, pp.metricsService) - - keywordsThreadIgnorer := app.NewKeywordsThreadIgnorer() - pp.channelActionService = app.NewChannelActionsService(pp.serviceAdapter, pp.bot, pp.config, channelActionStore, pp.playbookService, keywordsThreadIgnorer, pp.telemetryClient) - pp.categoryService = app.NewCategoryService(categoryStore, pp.serviceAdapter, pp.telemetryClient) - - pp.licenseChecker = enterprise.NewLicenseChecker(pp.serviceAdapter) - - pp.playbookRunService = app.NewPlaybookRunService( - pp.playbookRunStore, - pp.bot, - pp.config, - scheduler, - pp.telemetryClient, - pp.telemetryClient, - pp.serviceAdapter, - pp.playbookService, - pp.channelActionService, - pp.licenseChecker, - pp.metricsService, - ) - - if err = scheduler.SetCallback(pp.playbookRunService.HandleReminder); err != nil { - logrus.WithError(err).Error("JobOnceScheduler could not add the playbookRunService's HandleReminder") - } - if err = scheduler.Start(); err != nil { - logrus.WithError(err).Error("JobOnceScheduler could not start") - } - - // Migrations use the scheduler, so they have to be run after playbookRunService and scheduler have started - mutex, err := cluster.NewMutex(pp.serviceAdapter, "IR_dbMutex") - if err != nil { - return errors.Wrapf(err, "failed creating cluster mutex") - } - mutex.Lock() - if err = sqlStore.RunMigrations(); err != nil { - mutex.Unlock() - return errors.Wrapf(err, "failed to run migrations") - } - mutex.Unlock() - - pp.permissions = app.NewPermissionsService( - pp.playbookService, - pp.playbookRunService, - pp.serviceAdapter, - pp.config, - pp.licenseChecker, - ) - - // register collections and topics. - // TODO bump the minimum server version - if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeStatus); err != nil { - logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeStatus).Warnf("failed to register collection and topic") - } - if err = pp.serviceAdapter.RegisterCollectionAndTopic(CollectionTypeRun, TopicTypeTask); err != nil { - logrus.WithError(err).WithField("collection_type", CollectionTypeRun).WithField("topic_type", TopicTypeTask).Warnf("failed to register collection and topic") - } - - api.NewGraphQLHandler( - pp.handler.APIRouter, - pp.playbookService, - pp.playbookRunService, - pp.categoryService, - pp.serviceAdapter, - pp.config, - pp.permissions, - pp.playbookStore, - pp.licenseChecker, - ) - api.NewPlaybookHandler( - pp.handler.APIRouter, - pp.playbookService, - pp.serviceAdapter, - pp.config, - pp.permissions, - ) - api.NewPlaybookRunHandler( - pp.handler.APIRouter, - pp.playbookRunService, - pp.playbookService, - pp.permissions, - pp.licenseChecker, - pp.serviceAdapter, - pp.bot, - pp.config, - ) - api.NewStatsHandler( - pp.handler.APIRouter, - pp.serviceAdapter, - statsStore, - pp.playbookService, - pp.permissions, - pp.licenseChecker, - ) - api.NewBotHandler( - pp.handler.APIRouter, - pp.serviceAdapter, pp.bot, - pp.config, - pp.playbookRunService, - pp.userInfoStore, - ) - api.NewTelemetryHandler( - pp.handler.APIRouter, - pp.playbookRunService, - pp.serviceAdapter, - pp.telemetryClient, - pp.playbookService, - pp.telemetryClient, - pp.telemetryClient, - pp.telemetryClient, - pp.permissions, - ) - api.NewSignalHandler( - pp.handler.APIRouter, - pp.serviceAdapter, - pp.playbookRunService, - pp.playbookService, - keywordsThreadIgnorer, - ) - api.NewSettingsHandler( - pp.handler.APIRouter, - pp.serviceAdapter, - pp.config, - ) - api.NewActionsHandler( - pp.handler.APIRouter, - pp.channelActionService, - pp.serviceAdapter, - pp.permissions, - ) - api.NewCategoryHandler( - pp.handler.APIRouter, - pp.serviceAdapter, - pp.categoryService, - pp.playbookService, - pp.playbookRunService, - ) - - isTestingEnabled := false - flag := pp.serviceAdapter.GetConfig().ServiceSettings.EnableTesting - if flag != nil { - isTestingEnabled = *flag - } - - if err = command.RegisterCommands(pp.serviceAdapter.RegisterCommand, isTestingEnabled); err != nil { - return errors.Wrapf(err, "failed register commands") - } - - if err := pp.hooksService.RegisterHooks(playbooksProductName, pp); err != nil { - return fmt.Errorf("failed to register hooks: %w", err) - } - - enableMetrics := pp.configService.Config().MetricsSettings.Enable - if enableMetrics != nil && *enableMetrics { - pp.metricsService = newMetricsInstance() - // run metrics server to expose data - pp.runMetricsServer() - // run metrics updater recurring task - pp.runMetricsUpdaterTask(pp.playbookStore, pp.playbookRunStore, updateMetricsTaskFrequency) - // set error counter middleware handler - pp.handler.APIRouter.Use(pp.getErrorCounterHandler()) - } - - pp.routerService.RegisterRouter(playbooksProductName, pp.handler.APIRouter) - - logrus.Debug("Playbooks product successfully started.") - return nil -} - -func (pp *playbooksProduct) Stop() error { - if pp.metricsServer != nil { - err := pp.metricsServer.Shutdown() - if err != nil { - logrus.WithError(err).Warn("unable to shut down metric server") - } - } - if pp.metricsUpdaterTask != nil { - pp.metricsUpdaterTask.Cancel() - } - return nil -} - -func newMetricsInstance() *metrics.Metrics { - // Init metrics - instanceInfo := metrics.InstanceInfo{ - Version: model.BuildHash, - InstallationID: os.Getenv("MM_CLOUD_INSTALLATION_ID"), - } - return metrics.NewMetrics(instanceInfo) -} - -func (pp *playbooksProduct) runMetricsServer() { - logrus.WithField("port", metricsExposePort).Info("Starting Playbooks metrics server") - - pp.metricsServer = metrics.NewMetricsServer(metricsExposePort, pp.metricsService) - // Run server to expose metrics - go func() { - err := pp.metricsServer.Run() - if err != nil && !errors.Is(err, http.ErrServerClosed) { - logrus.WithError(err).Error("Metrics server could not be started") - } - }() -} - -func (pp *playbooksProduct) runMetricsUpdaterTask(playbookStore app.PlaybookStore, playbookRunStore app.PlaybookRunStore, updateMetricsTaskFrequency time.Duration) { - metricsUpdater := func() { - if playbooksActiveTotal, err := playbookStore.GetPlaybooksActiveTotal(); err == nil { - pp.metricsService.ObservePlaybooksActiveTotal(playbooksActiveTotal) - } else { - logrus.WithError(err).Error("error updating metrics, playbooks_active_total") - } - - if runsActiveTotal, err := playbookRunStore.GetRunsActiveTotal(); err == nil { - pp.metricsService.ObserveRunsActiveTotal(runsActiveTotal) - } else { - logrus.WithError(err).Error("error updating metrics, runs_active_total") - } - - if remindersOverdueTotal, err := playbookRunStore.GetOverdueUpdateRunsTotal(); err == nil { - pp.metricsService.ObserveRemindersOutstandingTotal(remindersOverdueTotal) - } else { - logrus.WithError(err).Error("error updating metrics, reminders_outstanding_total") - } - - if retrosOverdueTotal, err := playbookRunStore.GetOverdueRetroRunsTotal(); err == nil { - pp.metricsService.ObserveRetrosOutstandingTotal(retrosOverdueTotal) - } else { - logrus.WithError(err).Error("error updating metrics, retros_outstanding_total") - } - - if followersActiveTotal, err := playbookRunStore.GetFollowersActiveTotal(); err == nil { - pp.metricsService.ObserveFollowersActiveTotal(followersActiveTotal) - } else { - logrus.WithError(err).Error("error updating metrics, followers_active_total") - } - - if participantsActiveTotal, err := playbookRunStore.GetParticipantsActiveTotal(); err == nil { - pp.metricsService.ObserveParticipantsActiveTotal(participantsActiveTotal) - } else { - logrus.WithError(err).Error("error updating metrics, participants_active_total") - } - } - - pp.metricsUpdaterTask = scheduler.CreateRecurringTask("metricsUpdater", metricsUpdater, updateMetricsTaskFrequency) -} - -func (pp *playbooksProduct) getErrorCounterHandler() func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - recorder := &StatusRecorder{ - ResponseWriter: w, - Status: 200, - } - next.ServeHTTP(recorder, r) - if recorder.Status < 200 || recorder.Status > 299 { - pp.metricsService.IncrementErrorsCount(1) - } - }) - } -} - -type StatusRecorder struct { - http.ResponseWriter - Status int -} - -func (r *StatusRecorder) WriteHeader(status int) { - r.Status = status - r.ResponseWriter.WriteHeader(status) -} - -// ServeHTTP routes incoming HTTP requests to the plugin's REST API. -func (pp *playbooksProduct) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - pp.handler.ServeHTTP(w, r) -} - -// -// These callbacks are called by the suite automatically -// - -func (pp *playbooksProduct) OnConfigurationChange() error { - if pp.config == nil { - return nil - } - return pp.config.OnConfigurationChange() -} - -// ExecuteCommand executes a command that has been previously registered via the RegisterCommand. -func (pp *playbooksProduct) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - runner := command.NewCommandRunner(c, args, pp.serviceAdapter, pp.bot, - pp.playbookRunService, pp.playbookService, pp.config, pp.userInfoStore, pp.telemetryClient, pp.permissions) - - if err := runner.Execute(); err != nil { - return nil, model.NewAppError("Playbooks.ExecuteCommand", "app.command.execute.error", nil, err.Error(), http.StatusInternalServerError) - } - - return &model.CommandResponse{}, nil -} - -func (pp *playbooksProduct) UserHasJoinedChannel(c *plugin.Context, channelMember *model.ChannelMember, actor *model.User) { - actorID := "" - if actor != nil && actor.Id != channelMember.UserId { - actorID = actor.Id - } - pp.channelActionService.UserHasJoinedChannel(channelMember.UserId, channelMember.ChannelId, actorID) -} - -func (pp *playbooksProduct) MessageHasBeenPosted(c *plugin.Context, post *model.Post) { - pp.channelActionService.MessageHasBeenPosted(post) - pp.playbookRunService.MessageHasBeenPosted(post) -} - -func (pp *playbooksProduct) UserHasPermissionToCollection(c *plugin.Context, userID string, collectionType, collectionID string, permission *model.Permission) (bool, error) { - if collectionType != CollectionTypeRun { - return false, errors.Errorf("collection %s is not registered by playbooks", collectionType) - } - - run, err := pp.playbookRunService.GetPlaybookRun(collectionID) - if err != nil { - return false, errors.Wrapf(err, "No run with id - %s", collectionID) - } - return pp.permissions.HasPermissionsToRun(userID, run, permission), nil -} - -func (pp *playbooksProduct) GetAllCollectionIDsForUser(c *plugin.Context, userID, collectionType string) ([]string, error) { - if collectionType != CollectionTypeRun { - return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType) - } - - ids, err := pp.playbookRunService.GetPlaybookRunIDsForUser(userID) - if err != nil { - return nil, err - } - - return ids, nil -} - -func (pp *playbooksProduct) GetAllUserIdsForCollection(c *plugin.Context, collectionType, collectionID string) ([]string, error) { - if collectionType != CollectionTypeRun { - return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType) - } - - run, err := pp.playbookRunService.GetPlaybookRun(collectionID) - if err != nil { - return nil, errors.Wrapf(err, "No run with id - %s", collectionID) - } - followers, err := pp.playbookRunService.GetFollowers(collectionID) - if err != nil { - return nil, errors.Wrapf(err, "can't get followers for run - %s", collectionID) - } - return mergeSlice(run.ParticipantIDs, followers), nil -} - -func (pp *playbooksProduct) GetCollectionMetadataByIds(c *plugin.Context, collectionType string, collectionIDs []string) (map[string]*model.CollectionMetadata, error) { - if collectionType != CollectionTypeRun { - return nil, errors.Errorf("collection %s is not registered by playbooks", collectionType) - } - runsMetadata := map[string]*model.CollectionMetadata{} - runs, err := pp.playbookRunService.GetRunMetadataByIDs(collectionIDs) - if err != nil { - return nil, errors.Wrap(err, "can't get playbook run metadata by ids") - } - for _, run := range runs { - runsMetadata[run.ID] = &model.CollectionMetadata{ - Id: run.ID, - CollectionType: CollectionTypeRun, - TeamId: run.TeamID, - Name: run.Name, - RelativeURL: app.GetRunDetailsRelativeURL(run.ID), - } - } - return runsMetadata, nil -} - -func (pp *playbooksProduct) GetTopicMetadataByIds(c *plugin.Context, topicType string, topicIDs []string) (map[string]*model.TopicMetadata, error) { - topicsMetadata := map[string]*model.TopicMetadata{} - - var getTopicMetadataByIDs func(topicIDs []string) ([]app.TopicMetadata, error) - switch topicType { - case TopicTypeStatus: - getTopicMetadataByIDs = pp.playbookRunService.GetStatusMetadataByIDs - case TopicTypeTask: - getTopicMetadataByIDs = pp.playbookRunService.GetTaskMetadataByIDs - default: - return map[string]*model.TopicMetadata{}, errors.Errorf("topic type %s is not registered by playbooks", topicType) - } - - topics, err := getTopicMetadataByIDs(topicIDs) - if err != nil { - return nil, errors.Wrap(err, "can't get metadata by topic ids") - } - for _, topic := range topics { - topicsMetadata[topic.ID] = &model.TopicMetadata{ - Id: topic.ID, - TopicType: topicType, - CollectionType: CollectionTypeRun, - TeamId: topic.TeamID, - CollectionId: topic.RunID, - } - } - - return topicsMetadata, nil -} - -func mergeSlice(a, b []string) []string { - m := make(map[string]struct{}, len(a)+len(b)) - for _, elem := range a { - m[elem] = struct{}{} - } - for _, elem := range b { - m[elem] = struct{}{} - } - merged := make([]string, 0, len(m)) - for key := range m { - merged = append(merged, key) - } - return merged -} diff --git a/server/playbooks/product/pluginapi/cluster/job.go b/server/playbooks/product/pluginapi/cluster/job.go deleted file mode 100644 index 7de02f11a36..00000000000 --- a/server/playbooks/product/pluginapi/cluster/job.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package cluster - -import ( - "encoding/json" - "sync" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - // cronPrefix is used to namespace key values created for a job from other key values - // created by a plugin. - cronPrefix = "cron_" -) - -// JobPluginAPI is the plugin API interface required to schedule jobs. -type JobPluginAPI interface { - MutexPluginAPI - KVGet(key string) ([]byte, error) - KVDelete(key string) error - KVList(page, count int) ([]string, error) -} - -// JobConfig defines the configuration of a scheduled job. -type JobConfig struct { - // Interval is the period of execution for the job. - Interval time.Duration -} - -// NextWaitInterval is a callback computing the next wait interval for a job. -type NextWaitInterval func(now time.Time, metadata JobMetadata) time.Duration - -// MakeWaitForInterval creates a function to scheduling a job to run on the given interval relative -// to the last finished timestamp. -// -// For example, if the job first starts at 12:01 PM, and is configured with interval 5 minutes, -// it will next run at: -// -// 12:06, 12:11, 12:16, ... -// -// If the job has not previously started, it will run immediately. -func MakeWaitForInterval(interval time.Duration) NextWaitInterval { - if interval == 0 { - panic("must specify non-zero ready interval") - } - - return func(now time.Time, metadata JobMetadata) time.Duration { - sinceLastFinished := now.Sub(metadata.LastFinished) - if sinceLastFinished < interval { - return interval - sinceLastFinished - } - - return 0 - } -} - -// MakeWaitForRoundedInterval creates a function, scheduling a job to run on the nearest rounded -// interval relative to the last finished timestamp. -// -// For example, if the job first starts at 12:04 PM, and is configured with interval 5 minutes, -// and is configured to round to 5 minute intervals, it will next run at: -// -// 12:05 PM, 12:10 PM, 12:15 PM, ... -// -// If the job has not previously started, it will run immediately. Note that this wait interval -// strategy does not guarantee a minimum interval between runs, only that subsequent runs will be -// scheduled on the rounded interval. -func MakeWaitForRoundedInterval(interval time.Duration) NextWaitInterval { - if interval == 0 { - panic("must specify non-zero ready interval") - } - - return func(now time.Time, metadata JobMetadata) time.Duration { - if metadata.LastFinished.IsZero() { - return 0 - } - - target := metadata.LastFinished.Add(interval).Truncate(interval) - untilTarget := target.Sub(now) - if untilTarget > 0 { - return untilTarget - } - - return 0 - } -} - -// Job is a scheduled job whose callback function is executed on a configured interval by at most -// one plugin instance at a time. -// -// Use scheduled jobs to perform background activity on a regular interval without having to -// explicitly coordinate with other instances of the same plugin that might repeat that effort. -type Job struct { - pluginAPI JobPluginAPI - key string - mutex *Mutex - nextWaitInterval NextWaitInterval - callback func() - - stopOnce sync.Once - stop chan bool - done chan bool -} - -// JobMetadata persists metadata about job execution. -type JobMetadata struct { - // LastFinished is the last time the job finished anywhere in the cluster. - LastFinished time.Time -} - -// Schedule creates a scheduled job. -func Schedule(pluginAPI JobPluginAPI, key string, nextWaitInterval NextWaitInterval, callback func()) (*Job, error) { - key = cronPrefix + key - - mutex, err := NewMutex(pluginAPI, key) - if err != nil { - return nil, errors.Wrap(err, "failed to create job mutex") - } - - job := &Job{ - pluginAPI: pluginAPI, - key: key, - mutex: mutex, - nextWaitInterval: nextWaitInterval, - callback: callback, - stop: make(chan bool), - done: make(chan bool), - } - - go job.run() - - return job, nil -} - -// readMetadata reads the job execution metadata from the kv store. -func (j *Job) readMetadata() (JobMetadata, error) { - data, appErr := j.pluginAPI.KVGet(j.key) - if appErr != nil { - return JobMetadata{}, errors.Wrap(appErr, "failed to read data") - } - - if data == nil { - return JobMetadata{}, nil - } - - var metadata JobMetadata - err := json.Unmarshal(data, &metadata) - if err != nil { - return JobMetadata{}, errors.Wrap(err, "failed to decode data") - } - - return metadata, nil -} - -// saveMetadata writes updated job execution metadata from the kv store. -// -// It is assumed that the job mutex is held, negating the need to require an atomic write. -func (j *Job) saveMetadata(metadata JobMetadata) error { - data, err := json.Marshal(metadata) - if err != nil { - return errors.Wrap(err, "failed to marshal data") - } - - ok, appErr := j.pluginAPI.KVSetWithOptions(j.key, data, model.PluginKVSetOptions{}) - if appErr != nil || !ok { - return errors.Wrap(appErr, "failed to set data") - } - - return nil -} - -// run attempts to run the scheduled job, guaranteeing only one instance is executing concurrently. -func (j *Job) run() { - defer close(j.done) - - var waitInterval time.Duration - - for { - select { - case <-j.stop: - return - case <-time.After(waitInterval): - } - - func() { - // Acquire the corresponding job lock and hold it throughout execution. - j.mutex.Lock() - defer j.mutex.Unlock() - - metadata, err := j.readMetadata() - if err != nil { - logrus.WithError(err).WithField("key", j.key).Error("failed to read job metadata") - waitInterval = nextWaitInterval(waitInterval, err) - return - } - - // Is it time to run the job? - waitInterval = j.nextWaitInterval(time.Now(), metadata) - if waitInterval > 0 { - return - } - - // Run the job - j.callback() - - metadata.LastFinished = time.Now() - - err = j.saveMetadata(metadata) - if err != nil { - logrus.WithError(err).WithField("key", j.key).Error("failed to write job data") - } - - waitInterval = j.nextWaitInterval(time.Now(), metadata) - }() - } -} - -// Close terminates a scheduled job, preventing it from being scheduled on this plugin instance. -func (j *Job) Close() error { - j.stopOnce.Do(func() { - close(j.stop) - }) - <-j.done - - return nil -} diff --git a/server/playbooks/product/pluginapi/cluster/job_once.go b/server/playbooks/product/pluginapi/cluster/job_once.go deleted file mode 100644 index b29c786d0dd..00000000000 --- a/server/playbooks/product/pluginapi/cluster/job_once.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package cluster - -import ( - "encoding/json" - "math/rand" - "sync" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" -) - -const ( - // oncePrefix is used to namespace key values created for a scheduleOnce job - oncePrefix = "once_" - - // keysPerPage is the maximum number of keys to retrieve from the db per call - keysPerPage = 1000 - - // maxNumFails is the maximum number of KVStore read fails or failed attempts to run the - // callback until the scheduler cancels a job. - maxNumFails = 3 - - // waitAfterFail is the amount of time to wait after a failure - waitAfterFail = 1 * time.Second - - // pollNewJobsInterval is the amount of time to wait between polling the db for new scheduled jobs - pollNewJobsInterval = 5 * time.Minute - - // scheduleOnceJitter is the range of jitter to add to intervals to avoid contention issues - scheduleOnceJitter = 100 * time.Millisecond -) - -type JobOnceMetadata struct { - Key string - RunAt time.Time -} - -type JobOnce struct { - pluginAPI JobPluginAPI - clusterMutex *Mutex - - // key is the original key. It is prefixed with oncePrefix when used as a key in the KVStore - key string - runAt time.Time - numFails int - - // done signals the job.run go routine to exit - done chan bool - doneOnce sync.Once - - // join is a join point for the job.run() goroutine to join the calling goroutine (in this case, - // the one calling job.Cancel) - join chan bool - joinOnce sync.Once - - storedCallback *syncedCallback - activeJobs *syncedJobs -} - -// Cancel terminates a scheduled job, preventing it from being scheduled on this plugin instance. -// It also removes the job from the db, preventing it from being run in the future. -func (j *JobOnce) Cancel() { - j.clusterMutex.Lock() - defer j.clusterMutex.Unlock() - - j.cancelWhileHoldingMutex() - - // join the running goroutine - j.joinOnce.Do(func() { - <-j.join - }) -} - -func newJobOnce(pluginAPI JobPluginAPI, key string, runAt time.Time, callback *syncedCallback, jobs *syncedJobs) (*JobOnce, error) { - mutex, err := NewMutex(pluginAPI, key) - if err != nil { - return nil, errors.Wrap(err, "failed to create job mutex") - } - - return &JobOnce{ - pluginAPI: pluginAPI, - clusterMutex: mutex, - key: key, - runAt: runAt, - done: make(chan bool), - join: make(chan bool), - storedCallback: callback, - activeJobs: jobs, - }, nil -} - -func (j *JobOnce) run() { - defer close(j.join) - - wait := time.Until(j.runAt) - - for { - select { - case <-j.done: - return - case <-time.After(wait + addJitter()): - } - - func() { - // Acquire the cluster mutex while we're trying to do the job - j.clusterMutex.Lock() - defer j.clusterMutex.Unlock() - - // Check that the job has not been completed - metadata, err := readMetadata(j.pluginAPI, j.key) - if err != nil { - j.numFails++ - if j.numFails > maxNumFails { - j.cancelWhileHoldingMutex() - return - } - - // wait a bit of time and try again - wait = waitAfterFail - return - } - - // If key doesn't exist, or if the runAt has changed, the original job has been completed already - if metadata == nil || !j.runAt.Equal(metadata.RunAt) { - j.cancelWhileHoldingMutex() - return - } - - j.executeJob() - - j.cancelWhileHoldingMutex() - }() - } -} - -func (j *JobOnce) executeJob() { - j.storedCallback.mu.Lock() - defer j.storedCallback.mu.Unlock() - - j.storedCallback.callback(j.key) -} - -// readMetadata reads the job's stored metadata. If the caller wishes to make an atomic -// read/write, the cluster mutex for job's key should be held. -func readMetadata(pluginAPI JobPluginAPI, key string) (*JobOnceMetadata, error) { - data, err := pluginAPI.KVGet(oncePrefix + key) - if err != nil { - return nil, errors.Wrap(err, "failed to read data") - } - - if data == nil { - return nil, nil - } - - var metadata JobOnceMetadata - if err := json.Unmarshal(data, &metadata); err != nil { - return nil, errors.Wrap(err, "failed to decode data") - } - - return &metadata, nil -} - -// saveMetadata writes the job's metadata to the kvstore. saveMetadata acquires the job's cluster lock. -// saveMetadata will not overwrite an existing key. -func (j *JobOnce) saveMetadata() error { - j.clusterMutex.Lock() - defer j.clusterMutex.Unlock() - - metadata := JobOnceMetadata{ - Key: j.key, - RunAt: j.runAt, - } - data, err := json.Marshal(metadata) - if err != nil { - return errors.Wrap(err, "failed to marshal data") - } - - ok, err := j.pluginAPI.KVSetWithOptions(oncePrefix+j.key, data, model.PluginKVSetOptions{ - Atomic: true, - OldValue: nil, - }) - if err != nil { - return err - } - if !ok { - return errors.New("failed to set data") - } - - return nil -} - -// cancelWhileHoldingMutex assumes the caller holds the job's mutex. -func (j *JobOnce) cancelWhileHoldingMutex() { - // remove the job from the kv store, if it exists - _ = j.pluginAPI.KVDelete(oncePrefix + j.key) - - j.activeJobs.mu.Lock() - defer j.activeJobs.mu.Unlock() - delete(j.activeJobs.jobs, j.key) - - j.doneOnce.Do(func() { - close(j.done) - }) -} - -func addJitter() time.Duration { - return time.Duration(rand.Int63n(int64(scheduleOnceJitter))) -} diff --git a/server/playbooks/product/pluginapi/cluster/job_once_scheduler.go b/server/playbooks/product/pluginapi/cluster/job_once_scheduler.go deleted file mode 100644 index 07fb6eaee3f..00000000000 --- a/server/playbooks/product/pluginapi/cluster/job_once_scheduler.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package cluster - -import ( - "strings" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// syncedCallback uses the mutex to make things predictable for the client: the callback will be -// called once at a time (the client does not need to worry about concurrency within the callback) -type syncedCallback struct { - mu sync.Mutex - callback func(string) -} - -type syncedJobs struct { - mu sync.RWMutex - jobs map[string]*JobOnce -} - -type JobOnceScheduler struct { - pluginAPI JobPluginAPI - - startedMu sync.RWMutex - started bool - - activeJobs *syncedJobs - storedCallback *syncedCallback -} - -// GetJobOnceScheduler returns a scheduler which is ready to have its callback set. Repeated -// calls will return the same scheduler. -func GetJobOnceScheduler(pluginAPI JobPluginAPI) *JobOnceScheduler { - return &JobOnceScheduler{ - pluginAPI: pluginAPI, - activeJobs: &syncedJobs{ - jobs: make(map[string]*JobOnce), - }, - storedCallback: &syncedCallback{}, - } -} - -// Start starts the Scheduler. It finds all previous ScheduleOnce jobs and starts them running, and -// fires any jobs that have reached or exceeded their runAt time. Thus, even if a cluster goes down -// and is restarted, Start will restart previously scheduled jobs. -func (s *JobOnceScheduler) Start() error { - s.startedMu.Lock() - defer s.startedMu.Unlock() - if s.started { - return errors.New("scheduler has already been started") - } - - if err := s.verifyCallbackExists(); err != nil { - return errors.Wrap(err, "callback not found; cannot start scheduler") - } - - if err := s.scheduleNewJobsFromDB(); err != nil { - return errors.Wrap(err, "could not start JobOnceScheduler due to error") - } - - go s.pollForNewScheduledJobs() - - s.started = true - - return nil -} - -// SetCallback sets the scheduler's callback. When a job fires, the callback will be called with -// the job's id. -func (s *JobOnceScheduler) SetCallback(callback func(string)) error { - if callback == nil { - return errors.New("callback cannot be nil") - } - - s.storedCallback.mu.Lock() - defer s.storedCallback.mu.Unlock() - - s.storedCallback.callback = callback - return nil -} - -// ListScheduledJobs returns a list of the jobs in the db that have been scheduled. There is no -// guarantee that list is accurate by the time the caller reads the list. E.g., the jobs in the list -// may have been run, canceled, or new jobs may have scheduled. -func (s *JobOnceScheduler) ListScheduledJobs() ([]JobOnceMetadata, error) { - var ret []JobOnceMetadata - for i := 0; ; i++ { - keys, err := s.pluginAPI.KVList(i, keysPerPage) - if err != nil { - return nil, errors.Wrap(err, "error getting KVList") - } - for _, k := range keys { - if strings.HasPrefix(k, oncePrefix) { - metadata, err := readMetadata(s.pluginAPI, k[len(oncePrefix):]) - if err != nil { - logrus.WithError(err).WithField("key", k).Error("could not retrieve data from plugin kvstore") - continue - } - if metadata == nil { - continue - } - - ret = append(ret, *metadata) - } - } - - if len(keys) < keysPerPage { - break - } - } - - return ret, nil -} - -// ScheduleOnce creates a scheduled job that will run once. When the clock reaches runAt, the -// callback will be called with key as the argument. -// -// If the job key already exists in the db, this will return an error. To reschedule a job, first -// cancel the original then schedule it again. -func (s *JobOnceScheduler) ScheduleOnce(key string, runAt time.Time) (*JobOnce, error) { - s.startedMu.RLock() - defer s.startedMu.RUnlock() - if !s.started { - return nil, errors.New("start the scheduler before adding jobs") - } - - job, err := newJobOnce(s.pluginAPI, key, runAt, s.storedCallback, s.activeJobs) - if err != nil { - return nil, errors.Wrap(err, "could not create new job") - } - - if err = job.saveMetadata(); err != nil { - return nil, errors.Wrap(err, "could not save job metadata") - } - - s.runAndTrack(job) - - return job, nil -} - -// Cancel cancels a job by its key. This is useful if the plugin lost the original *JobOnce, or -// is stopping a job found in ListScheduledJobs(). -func (s *JobOnceScheduler) Cancel(key string) { - // using an anonymous function because job.Close() below needs access to the activeJobs mutex - job := func() *JobOnce { - s.activeJobs.mu.RLock() - defer s.activeJobs.mu.RUnlock() - j, ok := s.activeJobs.jobs[key] - if ok { - return j - } - - // Job wasn't active, so no need to call CancelWhileHoldingMutex (which shuts down the - // goroutine). There's a condition where another server in the cluster started the job, and - // the current server hasn't polled for it yet. To solve that case, delete it from the db. - mutex, err := NewMutex(s.pluginAPI, key) - if err != nil { - logrus.WithError(err).WithField("key", key).Error("failed to create job mutex in Cancel") - } - mutex.Lock() - defer mutex.Unlock() - - _ = s.pluginAPI.KVDelete(oncePrefix + key) - - return nil - }() - - if job != nil { - job.Cancel() - } -} - -func (s *JobOnceScheduler) scheduleNewJobsFromDB() error { - scheduled, err := s.ListScheduledJobs() - if err != nil { - return errors.Wrap(err, "could not read scheduled jobs from db") - } - - for _, m := range scheduled { - job, err := newJobOnce(s.pluginAPI, m.Key, m.RunAt, s.storedCallback, s.activeJobs) - if err != nil { - logrus.WithError(err).WithField("key", m.Key).Error("could not create new job") - continue - } - - s.runAndTrack(job) - } - - return nil -} - -func (s *JobOnceScheduler) runAndTrack(job *JobOnce) { - s.activeJobs.mu.Lock() - defer s.activeJobs.mu.Unlock() - - // has this been scheduled already on this server? - if _, ok := s.activeJobs.jobs[job.key]; ok { - return - } - - go job.run() - - s.activeJobs.jobs[job.key] = job -} - -// pollForNewScheduledJobs will only be started once per plugin. It doesn't need to be stopped. -func (s *JobOnceScheduler) pollForNewScheduledJobs() { - for { - <-time.After(pollNewJobsInterval + addJitter()) - - if err := s.scheduleNewJobsFromDB(); err != nil { - logrus.WithError(err).Error("scheduleOnce poller encountered an error but is still polling") - } - } -} - -func (s *JobOnceScheduler) verifyCallbackExists() error { - s.storedCallback.mu.Lock() - defer s.storedCallback.mu.Unlock() - - if s.storedCallback.callback == nil { - return errors.New("set callback before starting the scheduler") - } - return nil -} diff --git a/server/playbooks/product/pluginapi/cluster/mutex.go b/server/playbooks/product/pluginapi/cluster/mutex.go deleted file mode 100644 index 68c315e4f97..00000000000 --- a/server/playbooks/product/pluginapi/cluster/mutex.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package cluster - -import ( - "context" - "sync" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - // mutexPrefix is used to namespace key values created for a mutex from other key values - // created by a plugin. - mutexPrefix = "mutex_" -) - -const ( - // ttl is the interval after which a locked mutex will expire unless refreshed - ttl = time.Second * 15 - - // refreshInterval is the interval on which the mutex will be refreshed when locked - refreshInterval = ttl / 2 -) - -// MutexPluginAPI is the plugin API interface required to manage mutexes. -type MutexPluginAPI interface { - KVSetWithOptions(key string, value []byte, options model.PluginKVSetOptions) (bool, error) -} - -// Mutex is similar to sync.Mutex, except usable by multiple plugin instances across a cluster. -// -// Internally, a mutex relies on an atomic key-value set operation as exposed by the Mattermost -// plugin API. -// -// Mutexes with different names are unrelated. Mutexes with the same name from different plugins -// are unrelated. Pick a unique name for each mutex your plugin requires. -// -// A Mutex must not be copied after first use. -type Mutex struct { - pluginAPI MutexPluginAPI - key string - - // lock guards the variables used to manage the refresh task, and is not itself related to - // the cluster-wide lock. - lock sync.Mutex - stopRefresh chan bool - refreshDone chan bool -} - -// NewMutex creates a mutex with the given key name. -// -// Panics if key is empty. -func NewMutex(pluginAPI MutexPluginAPI, key string) (*Mutex, error) { - key, err := makeLockKey(key) - if err != nil { - return nil, err - } - - return &Mutex{ - pluginAPI: pluginAPI, - key: key, - }, nil -} - -// makeLockKey returns the prefixed key used to namespace mutex keys. -func makeLockKey(key string) (string, error) { - if key == "" { - return "", errors.New("must specify valid mutex key") - } - - return mutexPrefix + key, nil -} - -// lock makes a single attempt to atomically lock the mutex, returning true only if successful. -func (m *Mutex) tryLock() (bool, error) { - ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{ - Atomic: true, - OldValue: nil, // No existing key value. - ExpireInSeconds: int64(ttl / time.Second), - }) - if err != nil { - return false, errors.Wrap(err, "failed to set mutex kv") - } - - return ok, nil -} - -// refreshLock rewrites the lock key value with a new expiry, returning true only if successful. -func (m *Mutex) refreshLock() error { - ok, err := m.pluginAPI.KVSetWithOptions(m.key, []byte{1}, model.PluginKVSetOptions{ - Atomic: true, - OldValue: []byte{1}, - ExpireInSeconds: int64(ttl / time.Second), - }) - if err != nil { - return errors.Wrap(err, "failed to refresh mutex kv") - } else if !ok { - return errors.New("unexpectedly failed to refresh mutex kv") - } - - return nil -} - -// Lock locks m. If the mutex is already locked by any plugin instance, including the current one, -// the calling goroutine blocks until the mutex can be locked. -func (m *Mutex) Lock() { - _ = m.LockWithContext(context.Background()) -} - -// LockWithContext locks m unless the context is canceled. If the mutex is already locked by any plugin -// instance, including the current one, the calling goroutine blocks until the mutex can be locked, -// or the context is canceled. -// -// The mutex is locked only if a nil error is returned. -func (m *Mutex) LockWithContext(ctx context.Context) error { - var waitInterval time.Duration - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(waitInterval): - } - - locked, err := m.tryLock() - if err != nil { - logrus.WithError(err).WithField("lock_key", m.key).Error("failed to lock mutex") - waitInterval = nextWaitInterval(waitInterval, err) - continue - } else if !locked { - waitInterval = nextWaitInterval(waitInterval, err) - continue - } - - stop := make(chan bool) - done := make(chan bool) - go func() { - defer close(done) - t := time.NewTicker(refreshInterval) - for { - select { - case <-t.C: - err := m.refreshLock() - if err != nil { - logrus.WithError(err).WithField("lock_key", m.key).Error("failed to refresh mutex") - return - } - case <-stop: - return - } - } - }() - - m.lock.Lock() - m.stopRefresh = stop - m.refreshDone = done - m.lock.Unlock() - - return nil - } -} - -// Unlock unlocks m. It is a run-time error if m is not locked on entry to Unlock. -// -// Just like sync.Mutex, a locked Lock is not associated with a particular goroutine or plugin -// instance. It is allowed for one goroutine or plugin instance to lock a Lock and then arrange -// for another goroutine or plugin instance to unlock it. In practice, ownership of the lock should -// remain within a single plugin instance. -func (m *Mutex) Unlock() { - m.lock.Lock() - if m.stopRefresh == nil { - m.lock.Unlock() - panic("mutex has not been acquired") - } - - close(m.stopRefresh) - m.stopRefresh = nil - <-m.refreshDone - m.lock.Unlock() - - // If an error occurs deleting, the mutex kv will still expire, allowing later retry. - _, _ = m.pluginAPI.KVSetWithOptions(m.key, nil, model.PluginKVSetOptions{}) -} diff --git a/server/playbooks/product/pluginapi/cluster/wait.go b/server/playbooks/product/pluginapi/cluster/wait.go deleted file mode 100644 index f1e61c51609..00000000000 --- a/server/playbooks/product/pluginapi/cluster/wait.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package cluster - -import ( - "math/rand" - "time" -) - -const ( - // minWaitInterval is the minimum amount of time to wait between locking attempts - minWaitInterval = 1 * time.Second - - // maxWaitInterval is the maximum amount of time to wait between locking attempts - maxWaitInterval = 5 * time.Minute - - // pollWaitInterval is the usual time to wait between unsuccessful locking attempts - pollWaitInterval = 1 * time.Second - - // jitterWaitInterval is the amount of jitter to add when waiting to avoid thundering herds - jitterWaitInterval = minWaitInterval / 2 -) - -// nextWaitInterval determines how long to wait until the next lock retry. -func nextWaitInterval(lastWaitInterval time.Duration, err error) time.Duration { - nextWaitInterval := lastWaitInterval - - if nextWaitInterval <= 0 { - nextWaitInterval = minWaitInterval - } - - if err != nil { - nextWaitInterval *= 2 - if nextWaitInterval > maxWaitInterval { - nextWaitInterval = maxWaitInterval - } - } else { - nextWaitInterval = pollWaitInterval - } - - // Add some jitter to avoid unnecessary collision between competing plugin instances. - nextWaitInterval += time.Duration(rand.Int63n(int64(jitterWaitInterval)) - int64(jitterWaitInterval)/2) - - return nextWaitInterval -} diff --git a/server/playbooks/product/pluginapi/license.go b/server/playbooks/product/pluginapi/license.go deleted file mode 100644 index fd7a6b09142..00000000000 --- a/server/playbooks/product/pluginapi/license.go +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package pluginapi - -import ( - "github.com/mattermost/mattermost/server/public/model" -) - -const ( - e10 = "E10" - e20 = "E20" - professional = "professional" - enterprise = "enterprise" -) - -// IsEnterpriseLicensedOrDevelopment returns true when the server is licensed with any Mattermost -// Enterprise License, or has `EnableDeveloper` and `EnableTesting` configuration settings -// enabled signaling a non-production, developer mode. -func IsEnterpriseLicensedOrDevelopment(config *model.Config, license *model.License) bool { - if license != nil { - return true - } - - return IsConfiguredForDevelopment(config) -} - -// isValidSkuShortName returns whether the SKU short name is one of the known strings; -// namely: E10 or professional, or E20 or enterprise -func isValidSkuShortName(license *model.License) bool { - if license == nil { - return false - } - - switch license.SkuShortName { - case e10, e20, professional, enterprise: - return true - default: - return false - } -} - -// IsE10LicensedOrDevelopment returns true when the server is at least licensed with a legacy Mattermost -// Enterprise E10 License or a Mattermost Professional License, or has `EnableDeveloper` and -// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode. -func IsE10LicensedOrDevelopment(config *model.Config, license *model.License) bool { - if license != nil && - (license.SkuShortName == e10 || license.SkuShortName == professional || - license.SkuShortName == e20 || license.SkuShortName == enterprise) { - return true - } - - if !isValidSkuShortName(license) { - // As a fallback for licenses whose SKU short name is unknown, make a best effort to try - // and use the presence of a known E10/Professional feature as a check to determine licensing. - if license != nil && - license.Features != nil && - license.Features.LDAP != nil && - *license.Features.LDAP { - return true - } - } - - return IsConfiguredForDevelopment(config) -} - -// IsE20LicensedOrDevelopment returns true when the server is licensed with a legacy Mattermost -// Enterprise E20 License or a Mattermost Enterprise License, or has `EnableDeveloper` and -// `EnableTesting` configuration settings enabled, signaling a non-production, developer mode. -func IsE20LicensedOrDevelopment(config *model.Config, license *model.License) bool { - if license != nil && (license.SkuShortName == e20 || license.SkuShortName == enterprise) { - return true - } - - if !isValidSkuShortName(license) { - // As a fallback for licenses whose SKU short name is unknown, make a best effort to try - // and use the presence of a known E20/Enterprise feature as a check to determine licensing. - if license != nil && - license.Features != nil && - license.Features.FutureFeatures != nil && - *license.Features.FutureFeatures { - return true - } - } - - return IsConfiguredForDevelopment(config) -} - -// IsConfiguredForDevelopment returns true when the server has `EnableDeveloper` and `EnableTesting` -// configuration settings enabled, signaling a non-production, developer mode. -func IsConfiguredForDevelopment(config *model.Config) bool { - if config != nil && - config.ServiceSettings.EnableTesting != nil && - *config.ServiceSettings.EnableTesting && - config.ServiceSettings.EnableDeveloper != nil && - *config.ServiceSettings.EnableDeveloper { - return true - } - - return false -} - -// IsCloud returns true when the server is on cloud, and false otherwise. -func IsCloud(license *model.License) bool { - if license == nil || license.Features == nil || license.Features.Cloud == nil { - return false - } - - return *license.Features.Cloud -} diff --git a/server/playbooks/server/.gitignore b/server/playbooks/server/.gitignore deleted file mode 100644 index 3a55ce53332..00000000000 --- a/server/playbooks/server/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -coverage.txt -dist -data diff --git a/server/playbooks/server/api/actions.go b/server/playbooks/server/api/actions.go deleted file mode 100644 index 06336651042..00000000000 --- a/server/playbooks/server/api/actions.go +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" - - "github.com/gorilla/mux" -) - -type ActionsHandler struct { - *ErrorHandler - channelActionsService app.ChannelActionService - api playbooks.ServicesAPI - permissions *app.PermissionsService -} - -func NewActionsHandler(router *mux.Router, channelActionsService app.ChannelActionService, api playbooks.ServicesAPI, permissions *app.PermissionsService) *ActionsHandler { - handler := &ActionsHandler{ - ErrorHandler: &ErrorHandler{}, - channelActionsService: channelActionsService, - api: api, - permissions: permissions, - } - - actionsRouter := router.PathPrefix("/actions").Subrouter() - - channelsActionsRouter := actionsRouter.PathPrefix("/channels").Subrouter() - channelActionsRouter := channelsActionsRouter.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter() - channelActionsRouter.HandleFunc("", withContext(handler.createChannelAction)).Methods(http.MethodPost) - channelActionsRouter.HandleFunc("", withContext(handler.getChannelActions)).Methods(http.MethodGet) - channelActionsRouter.HandleFunc("/check-and-send-message-on-join", withContext(handler.checkAndSendMessageOnJoin)).Methods(http.MethodGet) - - channelActionRouter := channelActionsRouter.PathPrefix("/{action_id:[A-Za-z0-9]+}").Subrouter() - channelActionRouter.HandleFunc("", withContext(handler.updateChannelAction)).Methods(http.MethodPut) - - return handler -} - -func (a *ActionsHandler) createChannelAction(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - vars := mux.Vars(r) - channelID := vars["channel_id"] - - if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionCreate(userID, channelID)) { - return - } - - var channelAction app.GenericChannelAction - if err := json.NewDecoder(r.Body).Decode(&channelAction); err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse action", err) - return - } - - // Ensure that the channel ID in both the URL and the body of the request are the same; - // otherwise the permission check done above no longer makes sense - if channelAction.ChannelID != channelID { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "channel ID in request body must match channel ID in URL", nil) - return - } - - // Validate the action type and payload - if err := a.channelActionsService.Validate(channelAction); err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid action", err) - return - } - - id, err := a.channelActionsService.Create(channelAction) - if err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to create action", err) - return - } - - result := struct { - ID string `json:"id"` - }{ - ID: id, - } - w.Header().Add("Location", makeAPIURL(a.api, "actions/channel/%s/%s", channelAction.ChannelID, id)) - - ReturnJSON(w, &result, http.StatusCreated) -} - -func isValidTrigger(trigger string) bool { - if trigger == "" { - return true - } - - for _, elem := range app.ValidTriggerTypes { - if trigger == string(elem) { - return true - } - } - - return false -} - -func isValidAction(action string) bool { - if action == "" { - return true - } - - for _, elem := range app.ValidActionTypes { - if action == string(elem) { - return true - } - } - - return false -} - -func parseGetChannelActionsOptions(query url.Values) (*app.GetChannelActionOptions, error) { - actionTypeStr := query.Get("action_type") - triggerTypeStr := query.Get("trigger_type") - - if !isValidAction(actionTypeStr) { - return nil, fmt.Errorf("action_type %q not recognized; valid values are %v", actionTypeStr, app.ValidActionTypes) - } - - if !isValidTrigger(triggerTypeStr) { - return nil, fmt.Errorf("trigger_type %q not recognized; valid values are %v", triggerTypeStr, app.ValidTriggerTypes) - } - - return &app.GetChannelActionOptions{ - ActionType: app.ActionType(actionTypeStr), - TriggerType: app.TriggerType(triggerTypeStr), - }, nil -} - -func (a *ActionsHandler) getChannelActions(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - vars := mux.Vars(r) - channelID := vars["channel_id"] - - if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionView(userID, channelID)) { - return - } - - options, err := parseGetChannelActionsOptions(r.URL.Query()) - if err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, errors.Wrapf(err, "bad options").Error(), err) - return - } - - actions, err := a.channelActionsService.GetChannelActions(channelID, *options) - if err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, fmt.Sprintf("unable to retrieve actions for channel %s", channelID), err) - return - } - - ReturnJSON(w, &actions, http.StatusOK) -} - -// checkAndSendMessageOnJoin handles the GET /actions/channels/{channel_id}/check_and_send_message_on_join endpoint. -func (a *ActionsHandler) checkAndSendMessageOnJoin(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - channelID := vars["channel_id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionView(userID, channelID)) { - return - } - - hasViewed := a.channelActionsService.CheckAndSendMessageOnJoin(userID, channelID) - ReturnJSON(w, map[string]interface{}{"viewed": hasViewed}, http.StatusOK) -} - -func (a *ActionsHandler) updateChannelAction(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - vars := mux.Vars(r) - channelID := vars["channel_id"] - - if !a.PermissionsCheck(w, c.logger, a.permissions.ChannelActionUpdate(userID, channelID)) { - return - } - - var newChannelAction app.GenericChannelAction - if err := json.NewDecoder(r.Body).Decode(&newChannelAction); err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse action", err) - return - } - - // Ensure that the channel ID in both the URL and the body of the request are the same; - // otherwise the permission check done above no longer makes sense - if newChannelAction.ChannelID != channelID { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "channel ID in request body must match channel ID in URL", nil) - return - } - - // Validate the new action type and payload - if err := a.channelActionsService.Validate(newChannelAction); err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid action", err) - return - } - - err := a.channelActionsService.Update(newChannelAction, userID) - if err != nil { - a.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, fmt.Sprintf("unable to update action with ID %q", newChannelAction.ID), err) - return - } - - w.WriteHeader(http.StatusOK) -} diff --git a/server/playbooks/server/api/api.go b/server/playbooks/server/api/api.go deleted file mode 100644 index 294dce4afc7..00000000000 --- a/server/playbooks/server/api/api.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/sirupsen/logrus" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" -) - -// MaxRequestSize is the size limit for any incoming request -// The default limit set by mattermost-server is the configured max file size, and -// it sometimes isn't small enough to prevent some scenarios. -// -// This is important to prevent huge payloads from being sent -// that could end in a bigger problem. -// -// If an endpoint needs a smaller limit than this one, it could be solved by adding their -// own limit BEFORE reading the request body `r.Body = http.MaxBytesReader(w, r.Body, MaxRequestSize)` -const MaxRequestSize = 5 * 1024 * 1024 // 5MB - -// Handler Root API handler. -type Handler struct { - *ErrorHandler - APIRouter *mux.Router - root *mux.Router - config config.Service -} - -// NewHandler constructs a new handler. -func NewHandler(config config.Service) *Handler { - handler := &Handler{ - ErrorHandler: &ErrorHandler{}, - config: config, - } - - root := mux.NewRouter() - api := root.PathPrefix("/api/v0").Subrouter() - api.Use(LogRequest) - api.Use(MattermostAuthorizationRequired) - - api.Handle("{anything:.*}", http.NotFoundHandler()) - api.NotFoundHandler = http.NotFoundHandler() - - handler.APIRouter = api - handler.root = root - handler.config = config - - return handler -} - -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - r.Body = http.MaxBytesReader(w, r.Body, MaxRequestSize) - h.root.ServeHTTP(w, r) -} - -// handleResponseWithCode logs the internal error and sends the public facing error -// message as JSON in a response with the provided code. -func handleResponseWithCode(w http.ResponseWriter, code int, publicMsg string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - - responseMsg, _ := json.Marshal(struct { - Error string `json:"error"` // A public facing message providing details about the error. - }{ - Error: publicMsg, - }) - _, _ = w.Write(responseMsg) -} - -// HandleErrorWithCode logs the internal error and sends the public facing error -// message as JSON in a response with the provided code. -func HandleErrorWithCode(logger logrus.FieldLogger, w http.ResponseWriter, code int, publicErrorMsg string, internalErr error) { - if internalErr != nil { - logger = logger.WithError(internalErr) - } - - if code >= http.StatusInternalServerError { - logger.Error(publicErrorMsg) - } else { - logger.Warn(publicErrorMsg) - } - - handleResponseWithCode(w, code, publicErrorMsg) -} - -// ReturnJSON writes the given pointerToObject as json with the provided httpStatus -func ReturnJSON(w http.ResponseWriter, pointerToObject interface{}, httpStatus int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(httpStatus) - - if err := json.NewEncoder(w).Encode(pointerToObject); err != nil { - logrus.WithError(err).Warn("Unable to write to http.ResponseWriter") - return - } -} - -// MattermostAuthorizationRequired checks if request is authorized. -func MattermostAuthorizationRequired(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-Id") - if userID != "" { - next.ServeHTTP(w, r) - return - } - - http.Error(w, "Not authorized", http.StatusUnauthorized) - }) -} diff --git a/server/playbooks/server/api/api.yaml b/server/playbooks/server/api/api.yaml deleted file mode 100644 index ab80721e5c6..00000000000 --- a/server/playbooks/server/api/api.yaml +++ /dev/null @@ -1,2053 +0,0 @@ ---- -openapi: 3.0.0 -info: - version: 0.6.0 - title: Playbooks API - contact: - name: Mattermost - url: https://mattermost.com/ - email: support@mattermost.com -servers: - - url: http://localhost:8065/plugins/playbooks/api/v0 -paths: - /plugins/playbooks/api/v0/runs: - get: - summary: List all playbook runs - description: Retrieve a paged list of playbook runs, filtered by team, status, owner, name and/or members, and sorted by ID, name, status, creation date, end date, team or owner ID. - operationId: listPlaybookRuns - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: team_id - in: query - description: ID of the team to filter by. - required: true - example: el3d3t9p55pevvxs2qkdwz334k - schema: - type: string - - name: page - in: query - description: Zero-based index of the page to request. - required: false - example: 3 - schema: - type: integer - format: int32 - default: 0 - - name: per_page - in: query - description: Number of playbook runs to return per page. - required: false - example: 50 - schema: - type: integer - format: int32 - default: 1000 - - name: sort - in: query - description: Field to sort the returned playbook runs by. - required: false - example: end_at - schema: - type: string - default: create_at - enum: - - id - - name - - is_active - - create_at - - end_at - - team_id - - owner_user_id - - name: direction - in: query - description: Direction (ascending or descending) followed by the sorting of the playbook runs. - required: false - example: asc - schema: - type: string - default: desc - enum: - - desc - - asc - - name: statuses - in: query - description: The returned list will contain only the playbook runs with the specified statuses. - required: false - example: InProgress - schema: - type: array - default: - - InProgress - items: - type: string - enum: - - InProgress - - Finished - style: form - explode: true - - name: owner_user_id - in: query - description: The returned list will contain only the playbook runs commanded by this user. Specify "me" for current user. - required: false - example: lpn2ogt9qzkc59lfvvad9t15v4 - schema: - type: string - - name: participant_id - in: query - description: The returned list will contain only the playbook runs for which the given user is a participant. Specify "me" for current user. - required: false - example: bruhg1cs65retdbea798hrml4v - schema: - type: string - - name: search_term - in: query - description: The returned list will contain only the playbook runs whose name contains the search term. - required: false - example: server down - schema: - type: string - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/runs' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: A paged list of playbook runs. - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookRunList" - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - post: - summary: Create a new playbook run - description: Create a new playbook run in a team, using a playbook as template, with a specific name and a specific owner. - operationId: createPlaybookRunFromPost - security: - - BearerAuth: [] - tags: - - PlaybookRuns - requestBody: - description: Playbook run payload. - content: - application/json: - schema: - type: object - required: - - name - - owner_user_id - - team_id - - playbook_id - properties: - name: - type: string - description: The name of the playbook run. - example: Server down in EU cluster - description: - type: string - description: The description of the playbook run. - example: There is one server in the EU cluster that is not responding since April 12. - owner_user_id: - type: string - description: The identifier of the user who is commanding the playbook run. - example: bqnbdf8uc0a8yz4i39qrpgkvtg - team_id: - type: string - description: The identifier of the team where the playbook run's channel is in. - example: 61ji2mpflefup3cnuif80r5rde - post_id: - type: string - description: If the playbook run was created from a post, this field contains the identifier of such post. If not, this field is empty. - example: b2ntfcrl4ujivl456ab4b3aago - playbook_id: - type: string - description: The identifier of the playbook with from which this playbook run was created. - example: 0y4a0ntte97cxvfont8y84wa7x - x-codeSamples: - - lang: curl - source: | - curl -X POST 'http://localhost:8065/plugins/playbooks/api/v0/runs' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -H 'Content-Type: application/json'\ - -d '{"name": "Server down in EU cluster", "description": "There is one server in the EU cluster that is not responding since April 12.", "owner_user_id": "bqnbdf8uc0a8yz4i39qrpgkvtg", "team_id": "61ji2mpflefup3cnuif80r5rde", "playbook_id": "0y4a0ntte97cxvfont8y84wa7x"}' - responses: - 201: - description: Created playbook run. - headers: - Location: - description: Location of the created playbook run. - schema: - type: string - example: /api/v0/runs/nhkx1nbivu45lr84vtxxukp2vr - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookRun" - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/dialog: - post: - summary: Create a new playbook run from dialog - description: This is an internal endpoint to create a playbook run from the submission of an interactive dialog, filled by a user in the webapp. See [Interactive Dialogs](https://docs.mattermost.com/developer/interactive-dialogs.html) for more information. - operationId: createPlaybookRunFromDialog - security: - - BearerAuth: [] - tags: - - Internal - requestBody: - description: Dialog submission payload. - content: - application/json: - schema: - type: object - properties: - type: - type: string - example: dialog_submission - url: - type: string - callback_id: - type: string - description: Callback ID provided by the integration. - state: - type: string - description: Stringified JSON with the post_id and the client_id. - user_id: - type: string - description: ID of the user who submitted the dialog. - channel_id: - type: string - description: ID of the channel the user was in when submitting the dialog. - team_id: - type: string - description: ID of the team the user was on when submitting the dialog. - submission: - type: object - description: Map of the dialog fields to their values - required: - - playbookID - - playbookRunName - properties: - playbookID: - type: string - description: ID of the playbook to create the playbook run from. - example: ahz0s61gh275i7z2ag4g1ntvjm - playbookRunName: - type: string - description: The name of the playbook run to be created. - example: Server down in EU cluster. - playbookRunDescription: - type: string - description: An optional description of the playbook run. - example: There is one server in the EU cluster that is not responding since April 12. - cancelled: - type: boolean - description: If the dialog was cancelled. - responses: - 201: - description: Created playbook run. - headers: - Location: - description: Location of the created playbook run. - schema: - type: string - example: /api/v0/runs/nhkx1nbivu45lr84vtxxukp2vr - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookRun" - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/owners: - get: - summary: Get all owners - description: Get the owners of all playbook runs, filtered by team. - operationId: getOwners - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: team_id - in: query - description: ID of the team to filter by. - required: true - example: el3d3t9p55pevvxs2qkdwz334k - schema: - type: string - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/runs/owners?team_id=ni8duypfe7bamprxqeffd563gy' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: A list of owners. - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/OwnerInfo" - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/channels: - get: - summary: Get playbook run channels - description: Get all channels associated with a playbook run, filtered by team, status, owner, name and/or members, and sorted by ID, name, status, creation date, end date, team, or owner ID. - operationId: getChannels - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: team_id - in: query - description: ID of the team to filter by. - required: true - example: el3d3t9p55pevvxs2qkdwz334k - schema: - type: string - - name: sort - in: query - description: Field to sort the returned channels by, according to their playbook run. - required: false - example: end_at - schema: - type: string - default: create_at - enum: - - id - - name - - create_at - - end_at - - team_id - - owner_user_id - - name: direction - in: query - description: Direction (ascending or descending) followed by the sorting of the playbook runs associated to the channels. - required: false - example: asc - schema: - type: string - default: desc - enum: - - desc - - asc - - name: status - in: query - description: The returned list will contain only the channels whose playbook run has this status. - required: false - example: active - schema: - type: string - default: all - enum: - - all - - InProgress - - Finished - - name: owner_user_id - in: query - description: The returned list will contain only the channels whose playbook run is commanded by this user. - required: false - example: lpn2ogt9qzkc59lfvvad9t15v4 - schema: - type: string - - name: search_term - in: query - description: The returned list will contain only the channels associated to a playbook run whose name contains the search term. - required: false - example: server down - schema: - type: string - - name: participant_id - in: query - description: The returned list will contain only the channels associated to a playbook run for which the given user is a participant. - required: false - example: bruhg1cs65retdbea798hrml4v - schema: - type: string - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/runs/channels?team_id=ni8duypfe7bamprxqeffd563gy' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Channel IDs. - content: - application/json: - schema: - type: array - items: - type: string - description: ID of the channel. - example: v8zdc1893plelmf54vb7f0ramn - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/checklist-autocomplete: - get: - summary: Get autocomplete data for /playbook check - description: This is an internal endpoint used by the autocomplete system to retrieve the data needed to show the list of items that the user can check. - operationId: getChecklistAutocomplete - security: - - BearerAuth: [] - tags: - - Internal - parameters: - - name: channel_ID - in: query - description: ID of the channel the user is in. - required: true - example: r3vk8jdys4rlya46xhdthatoyx - schema: - type: string - responses: - 200: - description: List of autocomplete items for this channel. - content: - application/json: - schema: - type: array - items: - type: object - required: - - item - - hint - - helptext - properties: - item: - type: string - description: A string containing a pair of integers separated by a space. The first integer is the index of the checklist; the second is the index of the item within the checklist. - example: 1 2 - hint: - type: string - description: The title of the corresponding item. - example: Gather information from customer. - helptext: - type: string - description: Always the value "Check/uncheck this item". - example: Check/uncheck this item - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/channel/{channel_id}: - get: - summary: Find playbook run by channel ID - operationId: getPlaybookRunByChannelId - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: channel_id - in: path - required: true - description: ID of the channel associated to the playbook run to retrieve. - schema: - type: string - example: hwrmiyzj3kadcilh3ukfcnsbt6 - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/runs/channel/hwrmiyzj3kadcilh3ukfcnsbt6' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run associated to the channel. - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookRun" - 404: - $ref: "#/components/responses/404" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}: - get: - summary: Get a playbook run - operationId: getPlaybookRun - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to retrieve. - schema: - type: string - example: mx3xyzdojfgyfdx8sc8of1gdme - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/runs/mx3xyzdojfgyfdx8sc8of1gdme' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookRun" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - patch: - summary: Update a playbook run - operationId: updatePlaybookRun - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to retrieve. - schema: - type: string - example: mx3xyzdojfgyfdx8sc8of1gdme - requestBody: - description: Playbook run update payload. - content: - application/json: - schema: - type: object - properties: - active_stage: - type: integer - description: Zero-based index of the stage that will be made active. - example: 2 - x-codeSamples: - - lang: curl - source: | - curl -X PATCH 'http://localhost:8065/plugins/playbooks/api/v0/runs/mx3xyzdojfgyfdx8sc8of1gdme' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -H 'Content-Type: application/json'\ - -d '{"active_stage": 2}' - responses: - 200: - description: Playbook run successfully updated. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/metadata: - get: - summary: Get playbook run metadata - operationId: getPlaybookRunMetadata - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose metadata will be retrieved. - schema: - type: string - example: mx3xyzdojfgyfdx8sc8of1gdme - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/details' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run metadata. - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookRunMetadata" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/end: - put: - summary: End a playbook run - operationId: endPlaybookRun - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to end. - schema: - type: string - example: 1igmynxs77ywmcbwbsujzktter - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/end' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run ended - 500: - $ref: "#/components/responses/500" - post: - summary: End a playbook run from dialog - description: This is an internal endpoint to end a playbook run via a confirmation dialog, submitted by a user in the webapp. - operationId: endPlaybookRunDialog - security: - - BearerAuth: [] - tags: - - Internal - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to end. - schema: - type: string - example: 1igmynxs77ywmcbwbsujzktter - x-codeSamples: - - lang: curl - source: | - curl -X POST 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/end' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run ended - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/restart: - put: - summary: Restart a playbook run - operationId: restartPlaybookRun - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to restart. - schema: - type: string - example: 1igmynxs77ywmcbwbsujzktter - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/end' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run restarted. - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/status: - post: - summary: Update a playbook run's status - operationId: status - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to update. - schema: - type: string - example: 1igmynxs77ywmcbwbsujzktter - requestBody: - description: Payload to change the playbook run's status update message. - content: - application/json: - schema: - type: object - properties: - message: - type: string - description: The status update message. - example: Starting to investigate. - reminder: - type: number - description: The number of seconds until the system will send a reminder to the owner to update the status. No reminder will be scheduled if reminder is 0 or omitted. - example: 600 - required: - - description - - message - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/update-status' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - -d '{"message": "Finishing playbook run because the issue was solved."}' - responses: - 200: - description: Playbook run updated. - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/finish: - put: - summary: Finish a playbook - operationId: finish - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run to finish. - schema: - type: string - example: 1igmynxs77ywmcbwbsujzktter - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/finish' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook run finished. - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/owner: - post: - summary: Update playbook run owner - operationId: changeOwner - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose owner will be changed. - schema: - type: string - example: 1igmynxs77ywmcbwbsujzktter - requestBody: - description: Payload to change the playbook run's owner. - content: - application/json: - schema: - type: object - properties: - owner_id: - type: string - description: The user ID of the new owner. - example: hx7fqtqxp7nn8129t7e505ls6s - required: - - owner_id - x-codeSamples: - - lang: curl - source: | - curl -X POST 'http://localhost:8065/plugins/playbooks/api/v0/runs/1igmynxs77ywmcbwbsujzktter/owner' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -d '{"owner_id": "hx7fqtqxp7nn8129t7e505ls6s"}' - responses: - 200: - description: Owner successfully changed. - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/next-stage-dialog: - post: - summary: Go to next stage from dialog - description: This is an internal endpoint to go to the next stage via a confirmation dialog, submitted by a user in the webapp. - operationId: nextStageDialog - security: - - BearerAuth: [] - tags: - - Internal - parameters: - - in: path - name: id - schema: - type: string - required: true - description: The PlaybookRun ID - requestBody: - description: Dialog submission payload. - content: - application/json: - schema: - type: object - properties: - state: - type: string - description: String representation of the zero-based index of the stage to go to. - example: "3" - responses: - 200: - description: Playbook run stage update. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/add: - post: - summary: Add an item to a playbook run's checklist - description: The most common pattern to add a new item is to only send its title as the request payload. By default, it is an open item, with no assignee and no slash command. - operationId: addChecklistItem - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose checklist will be modified. - example: twcqg0a2m37ydi6ebge3j9ev5z - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist to modify. - example: 1 - schema: - type: integer - requestBody: - description: Checklist item payload. - content: - application/json: - schema: - type: object - required: - - title - properties: - title: - type: string - description: The title of the checklist item. - example: Gather information from customer. - state: - type: string - enum: - - "" - - in_progress - - closed - description: The state of the checklist item. An empty string means that the item is not done. - example: closed - state_modified: - type: integer - format: int64 - description: The timestamp for the latest modification of the item's state, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the item was never modified. - example: 1607774621321 - assignee_id: - type: string - description: The identifier of the user that has been assigned to complete this item. If the item has no assignee, this is an empty string. - example: pisdatkjtdlkdhht2v4inxuzx1 - assignee_modified: - type: integer - format: int64 - description: The timestamp for the latest modification of the item's assignee, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the item never got an assignee. - example: 1608897821125 - command: - type: string - description: The slash command associated with this item. If the item has no slash command associated, this is an empty string - example: /opsgenie on-call - command_last_run: - type: integer - format: int64 - description: The timestamp for the latest execution of the item's command, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the command was never executed. - example: 1608552221019 - description: - type: string - description: A detailed description of the checklist item, formatted with Markdown. - example: Ask the customer for more information in [Zendesk](https://www.zendesk.com/). - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/twcqg0a2m37ydi6ebge3j9ev5z/checklists/1/add' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -d '{"title": "Gather information from customer."}' - responses: - 200: - description: Item successfully added. - default: - description: Error response - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - - /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/reorder: - put: - summary: Reorder an item in a playbook run's checklist - operationId: reoderChecklistItem - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose checklist will be modified. - example: yj74zsk7dvtsv6ndsynsps3g5s - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist to modify. - example: 1 - schema: - type: integer - requestBody: - description: Reorder checklist item payload. - content: - application/json: - schema: - type: object - properties: - item_num: - type: integer - description: Zero-based index of the item to reorder. - example: 2 - new_location: - type: integer - description: Zero-based index of the new place to move the item to. - example: 2 - required: - - item_num - - new_location - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/yj74zsk7dvtsv6ndsynsps3g5s/checklists/1/reorder' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -d '{"item_num": 0, "new_location": 2}' - responses: - 200: - description: Item successfully reordered. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}: - put: - summary: Update an item of a playbook run's checklist - description: Update the title and the slash command of an item in one of the playbook run's checklists. - operationId: itemRename - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose checklist will be modified. - example: 6t7jdgyqr7b5sk24zkauhmrb06 - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist to modify. - example: 1 - schema: - type: integer - - name: item - in: path - required: true - description: Zero-based index of the item to modify. - example: 2 - schema: - type: integer - requestBody: - description: Update checklist item payload. - content: - application/json: - schema: - type: object - properties: - title: - type: string - description: The new title of the item. - example: Gather information from server's logs. - command: - type: string - description: The new slash command of the item. - example: /jira update ticket - required: - - title - - command - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/6t7jdgyqr7b5sk24zkauhmrb06/checklists/1/item/0' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -d '{"title": "Gather information from server's logs.", "command": "/jira update ticket"}' - responses: - 200: - description: Item updated. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - delete: - summary: Delete an item of a playbook run's checklist - operationId: itemDelete - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose checklist will be modified. - example: zjy2q2iy2jafl0lo2oddos5xn7 - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist to modify. - example: 1 - schema: - type: integer - - name: item - in: path - required: true - description: Zero-based index of the item to modify. - example: 2 - schema: - type: integer - x-codeSamples: - - lang: curl - source: | - curl -X DELETE 'http://localhost:8065/plugins/playbooks/api/v0/runs/zjy2q2iy2jafl0lo2oddos5xn7/checklists/1/item/2' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 204: - description: Item successfully deleted. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/state: - put: - summary: Update the state of an item - operationId: itemSetState - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose checklist will be modified. - example: 7l37isroz4e63giev62hs318bn - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist to modify. - example: 1 - schema: - type: integer - - name: item - in: path - required: true - description: Zero-based index of the item to modify. - example: 2 - schema: - type: integer - requestBody: - description: Update checklist item's state payload. - content: - application/json: - schema: - type: object - properties: - new_state: - type: string - description: The new state of the item. - enum: - - "" - - in_progress - - closed - example: closed - default: "" - required: - - new_state - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/7l37isroz4e63giev62hs318bn/checklists/1/item/2/state' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - -d '{"new_state": "closed"}' - responses: - 200: - description: Item's state successfully updated. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/assignee: - put: - summary: Update the assignee of an item - operationId: itemSetAssignee - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose item will get a new assignee. - example: 7l37isroz4e63giev62hs318bn - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist whose item will get a new assignee. - example: 1 - schema: - type: integer - - name: item - in: path - required: true - description: Zero-based index of the item that will get a new assignee. - example: 2 - schema: - type: integer - requestBody: - description: User ID of the new assignee. - content: - application/json: - schema: - type: object - properties: - assignee_id: - type: string - description: The user ID of the new assignee of the item. - example: ruu86intseidqdxjojia41u7l1 - required: - - assignee_id - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/runs/7l37isroz4e63giev62hs318bn/checklists/1/item/2/assignee' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - -d '{"asignee_id": "ruu86intseidqdxjojia41u7l1"}' - responses: - 200: - description: Item's assignee successfully updated. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/checklists/{checklist}/item/{item}/run: - put: - summary: Run an item's slash command - operationId: itemRun - security: - - BearerAuth: [] - tags: - - PlaybookRuns - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose item will be executed. - example: 7l37isroz4e63giev62hs318bn - schema: - type: string - - name: checklist - in: path - required: true - description: Zero-based index of the checklist whose item will be executed. - example: 1 - schema: - type: integer - - name: item - in: path - required: true - description: Zero-based index of the item whose slash command will be executed. - example: 2 - schema: - type: integer - x-codeSamples: - - lang: curl - source: | - curl -X POST 'http://localhost:8065/plugins/playbooks/api/v0/runs/7l37isroz4e63giev62hs318bn/checklists/1/item/2/run' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Item's slash command successfully executed. - content: - application/json: - schema: - $ref: "#/components/schemas/TriggerIdReturn" - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/runs/{id}/timeline/{event_id}/: - delete: - summary: Remove a timeline event from the playbook run - operationId: removeTimelineEvent - security: - - BearerAuth: [] - tags: - - Timeline - parameters: - - name: id - in: path - required: true - description: ID of the playbook run whose timeline event will be modified. - example: zjy2q2iy2jafl0lo2oddos5xn7 - schema: - type: string - - name: event_id - in: path - required: true - description: ID of the timeline event to be deleted - example: craxgf4r4trgzrtues3a1t74ac - schema: - type: string - x-codeSamples: - - lang: curl - source: | - curl -X DELETE 'http://localhost:8065/plugins/playbooks/api/v0/runs/zjy2q2iy2jafl0lo2oddos5xn7/timeline/craxgf4r4trgzrtues3a1t74ac' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 204: - description: Item successfully deleted. - 400: - $ref: "#/components/responses/400" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/playbooks: - get: - summary: List all playbooks - description: Retrieve a paged list of playbooks, filtered by team, and sorted by title, number of stages or number of steps. - operationId: getPlaybooks - security: - - BearerAuth: [] - tags: - - Playbooks - parameters: - - name: team_id - in: query - description: ID of the team to filter by. - required: true - example: 08fmfasq5wit3qyfmq4mjk0rto - schema: - type: string - - name: page - in: query - description: Zero-based index of the page to request. - required: false - example: 3 - schema: - type: integer - format: int32 - default: 0 - - name: per_page - in: query - description: Number of playbooks to return per page. - required: false - example: 50 - schema: - type: integer - format: int32 - default: 1000 - - name: sort - in: query - description: Field to sort the returned playbooks by title, number of stages or total number of steps. - required: false - example: stages - schema: - type: string - default: title - enum: - - title - - stages - - steps - - name: direction - in: query - description: Direction (ascending or descending) followed by the sorting of the playbooks. - required: false - example: asc - schema: - type: string - default: asc - enum: - - desc - - asc - - name: with_archived - in: query - description: Includes archived playbooks in the result. - required: false - example: true - schema: - type: boolean - default: false - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/playbooks?team_id=08fmfasq5wit3qyfmq4mjk0rto&sort=title&direction=asc' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: A paged list of playbooks. - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookList" - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - post: - summary: Create a playbook - operationId: createPlaybook - security: - - BearerAuth: [] - tags: - - Playbooks - requestBody: - description: Playbook - content: - application/json: - schema: - type: object - required: - - title - - team_id - - create_public_playbook_run - - checklists - - member_ids - properties: - title: - type: string - description: The title of the playbook. - example: Cloud PlaybookRuns - description: - type: string - description: The description of the playbook. - example: A playbook to follow when there is a playbook run regarding the availability of the cloud service. - team_id: - type: string - description: The identifier of the team where the playbook is in. - example: p03rbi6viyztysbqnkvcqyel2i - create_public_playbook_run: - type: boolean - description: A boolean indicating whether the playbook runs created from this playbook should be public or private. - example: true - public: - type: boolean - description: A boolean indicating whether the playbook is licensed as public or private. Required 'true' for free tier. - example: true - checklists: - type: array - description: The stages defined by this playbook. - items: - type: object - required: - - title - - items - properties: - title: - type: string - description: The title of the checklist. - example: Triage issue - items: - type: array - description: The list of tasks to do. - items: - type: object - required: - - title - properties: - title: - type: string - description: The title of the checklist item. - example: Gather information from customer. - command: - type: string - description: The slash command associated with this item. If the item has no slash command associated, this is an empty string - example: /opsgenie on-call - description: - type: string - description: A detailed description of the checklist item, formatted with Markdown. - example: Ask the customer for more information in [Zendesk](https://www.zendesk.com/). - member_ids: - description: The identifiers of all the users that are members of this playbook. - type: array - items: - type: string - description: User ID of the playbook member. - example: ilh6s1j4yefbdhxhtlzt179i6m - broadcast_channel_ids: - description: The IDs of the channels where all the status updates will be broadcasted. The team of the broadcast channel must be the same as the playbook's team. - type: array - items: - type: string - description: ID of the broadcast channel. - example: 2zh7rpashwfwapwaqyslmhwbax - invited_user_ids: - description: A list with the IDs of the members to be automatically invited to the playbook run's channel as soon as the playbook run is created. - type: array - items: - type: string - description: User ID of the member to be invited. - example: 01kidjn9iozv7bist427w4gkjo - invite_users_enabled: - description: Boolean that indicates whether the members declared in invited_user_ids will be automatically invited. - type: boolean - example: true - default_owner_id: - description: User ID of the member that will be automatically assigned as owner as soon as the playbook run is created. If the member is not part of the playbook run's channel or is not included in the invited_user_ids list, they will be automatically invited to the channel. - type: string - example: 9dtruav6d9ce3oqnc5pwhtqtfq - default_owner_enabled: - description: Boolean that indicates whether the member declared in default_owner_id will be automatically assigned as owner. - type: string - example: true - announcement_channel_id: - description: ID of the channel where the playbook run will be automatically announced as soon as the playbook run is created. - type: string - example: 8iofau5swv32l6qtk3vlxgobta - announcement_channel_enabled: - description: Boolean that indicates whether the playbook run creation will be announced in the channel declared in announcement_channel_id. - type: boolean - example: true - webhook_on_creation_url: - description: An absolute URL where a POST request will be sent as soon as the playbook run is created. The allowed protocols are HTTP and HTTPS. - type: string - example: https://httpbin.org/post - webhook_on_creation_enabled: - description: Boolean that indicates whether the webhook declared in webhook_on_creation_url will be automatically sent. - type: boolean - example: true - webhook_on_status_update_url: - description: An absolute URL where a POST request will be sent as soon as the playbook run's status is updated. The allowed protocols are HTTP and HTTPS. - type: string - example: https://httpbin.org/post - webhook_on_status_update_enabled: - description: Boolean that indicates whether the webhook declared in webhook_on_status_update_url will be automatically sent. - type: boolean - example: true - x-codeSamples: - - lang: curl - source: | - curl -X POST 'http://localhost:8065/plugins/playbooks/api/v0/playbooks' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -d '{"title": "Cloud PlaybookRuns", "description": "A playbook to follow when there is a playbook run regarding the availability of the cloud service.", "team_id": "p03rbi6viyztysbqnkvcqyel2i","create_public_playbook_run": true,"checklists": [{"title": "Triage issue","items": [{"title": "Gather information from customer."}]}]}' - callbacks: - playbookRunCreation: - "{$request.body#/webhook_on_creation_url}": - post: - summary: PlaybookRun's creation outgoing webhook. - description: When a playbook run is created with this playbook, a POST request is sent to the URL configured in webhook_on_creation_url. The webhook is considered successful if your server returns a response code within the 200-299 range. Otherwise, the webhook is considered failed, and a warning message is posted in the playbook run's channel. No retries are made. - operationId: webhookOncreation - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/WebhookOnCreationPayload" - responses: - "2XX": - description: Your server returns a 2XX code if it successfully received the request. - playbookRunStatusUpdate: - "{$request.body#/webhook_on_status_update_url}": - post: - summary: PlaybookRun's status update outgoing webhook. - description: When a playbook run's status is updated, a POST request is sent to the URL configured in webhook_on_status_update_url. The webhook is considered successful if your server returns a response code within the 200-299 range. Otherwise, the webhook is considered failed, and a warning message is posted in the playbook run's channel. No retries are made. - operationId: webhookOnStatusUpdate - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/WebhookOnStatusUpdatePayload" - responses: - "2XX": - description: Your server returns a 2XX code if it successfully received the request. - responses: - 201: - description: ID of the created playbook. - headers: - Location: - description: Location of the created playbook. - schema: - type: string - example: /api/v0/playbook/cdl5o0tjcp5rqlpjidhobj64nd - content: - application/json: - schema: - type: object - properties: - id: - type: string - example: iz0g457ikesz55dhxcfa0fk9yy - required: - - id - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/playbooks/{id}: - get: - summary: Get a playbook - operationId: getPlaybook - security: - - BearerAuth: [] - tags: - - Playbooks - parameters: - - name: id - in: path - required: true - description: ID of the playbook to retrieve. - schema: - type: string - example: iz0g457ikesz55dhxcfa0fk9yy - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/playbooks/iz0g457ikesz55dhxcfa0fk9yy' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: Playbook. - content: - application/json: - schema: - $ref: "#/components/schemas/Playbook" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - put: - summary: Update a playbook - operationId: updatePlaybook - security: - - BearerAuth: [] - tags: - - Playbooks - parameters: - - name: id - in: path - required: true - description: ID of the playbook to update. - schema: - type: string - example: iz0g457ikesz55dhxcfa0fk9yy - requestBody: - description: Playbook payload - content: - application/json: - schema: - $ref: "#/components/schemas/Playbook" - x-codeSamples: - - lang: curl - source: | - curl -X PUT 'http://localhost:8065/plugins/playbooks/api/v0/playbooks/iz0g457ikesz55dhxcfa0fk9yy' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e'\ - -d '{"title": "Playbook","team_id": "ni8duypfe7bamprxqeffd563gy","create_public_playbook_run": true,"checklists": [{"title": "Title","items": [{"title": "Title"}]}]}' - responses: - 200: - description: Playbook succesfully updated. - 400: - $ref: "#/components/responses/400" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - delete: - summary: Delete a playbook - operationId: deletePlaybook - security: - - BearerAuth: [] - tags: - - Playbooks - parameters: - - name: id - in: path - required: true - description: ID of the playbook to delete. - schema: - type: string - example: iz0g457ikesz55dhxcfa0fk9yy - x-codeSamples: - - lang: curl - source: | - curl -X DELETE 'http://localhost:8065/plugins/playbooks/api/v0/playbooks/iz0g457ikesz55dhxcfa0fk9yy' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 204: - description: Playbook successfully deleted. - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - - /plugins/playbooks/api/v0/playbooks/{id}/autofollows: - get: - summary: Get the list of followers' user IDs of a playbook - operationId: getAutoFollows - security: - - BearerAuth: [] - tags: - - PlaybookAutofollows - parameters: - - name: id - in: path - required: true - description: ID of the playbook to retrieve followers from. - schema: - type: string - example: iz0g457ikesz55dhxcfa0fk9yy - x-codeSamples: - - lang: curl - source: | - curl -X GET 'http://localhost:8065/plugins/playbooks/api/v0/playbooks/iz0g457ikesz55dhxcfa0fk9yy/autofollows' \ - -H 'Authorization: Bearer 9g64ig7q9pds8yjz8rsgd6e36e' - responses: - 200: - description: List of the user IDs who follow the playbook. - content: - application/json: - schema: - $ref: "#/components/schemas/PlaybookAutofollows" - 403: - $ref: "#/components/responses/403" - 500: - $ref: "#/components/responses/500" - -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - responses: - 400: - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: The request is malformed. - 403: - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Access to the resource is forbidden for this user. - 404: - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: Resource requested not found. - 500: - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - description: There was an internal error in the server. - schemas: - PlaybookRun: - type: object - properties: - id: - type: string - description: A unique, 26 characters long, alphanumeric identifier for the playbook run. - example: mx3xyzdojfgyfdx8sc8of1gdme - name: - type: string - description: The name of the playbook run. - example: Server down in EU cluster - description: - type: string - description: The description of the playbook run. - example: There is one server in the EU cluster that is not responding since April 12. - is_active: - type: boolean - description: True if the playbook run is ongoing; false if the playbook run is ended. - example: false - owner_user_id: - type: string - description: The identifier of the user that is commanding the playbook run. - example: bqnbdf8uc0a8yz4i39qrpgkvtg - team_id: - type: string - description: The identifier of the team where the playbook run's channel is in. - example: 61ji2mpflefup3cnuif80r5rde - channel_id: - type: string - description: The identifier of the playbook run's channel. - example: hwrmiyzj3kadcilh3ukfcnsbt6 - create_at: - type: integer - format: int64 - description: The playbook run creation timestamp, formatted as the number of milliseconds since the Unix epoch. - example: 1606807976289 - end_at: - type: integer - format: int64 - description: The playbook run finish timestamp, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the playbook run is not finished. - example: 0 - delete_at: - type: integer - format: int64 - description: The playbook run deletion timestamp, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the playbook run is not deleted. - example: 0 - active_stage: - type: integer - format: int32 - description: Zero-based index of the currently active stage. - example: 1 - active_stage_title: - type: string - description: The title of the currently active stage. - example: Triage issue - post_id: - type: string - description: If the playbook run was created from a post, this field contains the identifier of such post. If not, this field is empty. - example: b2ntfcrl4ujivl456ab4b3aago - playbook_id: - type: string - description: The identifier of the playbook with from which this playbook run was created. - example: 0y4a0ntte97cxvfont8y84wa7x - checklists: - type: array - items: - $ref: "#/components/schemas/Checklist" - PlaybookRunMetadata: - type: object - properties: - channel_name: - type: string - description: Name of the channel associated to the playbook run. - example: server-down-in-eu-cluster - channel_display_name: - type: string - description: Display name of the channel associated to the playbook run. - example: Server down in EU cluster - team_name: - type: string - description: Name of the team the playbook run is in. - example: sre-staff - num_members: - type: integer - format: int64 - description: Number of users that have been members of the playbook run at any point. - example: 25 - total_posts: - type: integer - format: int64 - description: Number of posts in the channel associated to the playbook run. - example: 202 - PlaybookRunList: - type: object - properties: - total_count: - type: integer - description: The total number of playbook runs in the list, regardless of the paging. - format: int32 - example: 305 - page_count: - type: integer - description: The total number of pages. This depends on the total number of playbook runs in the database and the per_page parameter sent with the request. - format: int32 - example: 2 - has_more: - type: boolean - description: A boolean describing whether there are more pages after the currently returned. - example: true - items: - type: array - description: The playbook runs in this page. - items: - $ref: "#/components/schemas/PlaybookRun" - PlaybookAutofollows: - type: object - properties: - total_count: - type: integer - description: The total number of users who marked this playbook to auto-follow runs. - format: int32 - example: 12 - items: - type: array - description: The user IDs of who marked this playbook to auto-follow. - items: - type: string - OwnerInfo: - type: object - required: - - user_id - - username - properties: - user_id: - type: string - description: A unique, 26 characters long, alphanumeric identifier for the owner. - example: ahz0s61gh275i7z2ag4g1ntvjm - username: - type: string - description: Owner's username. - example: aaron.medina - TriggerIdReturn: - type: object - required: - - trigger_id - properties: - trigger_id: - type: string - description: The trigger_id returned by the slash command. - example: ceenjwsg6tgdzjpofxqemy1aio - Playbook: - type: object - properties: - id: - type: string - description: A unique, 26 characters long, alphanumeric identifier for the playbook. - example: iz0g457ikesz55dhxcfa0fk9yy - title: - type: string - description: The title of the playbook. - example: Cloud PlaybookRuns - description: - type: string - description: The description of the playbook. - example: A playbook to follow when there is a playbook run regarding the availability of the cloud service. - team_id: - type: string - description: The identifier of the team where the playbook is in. - example: p03rbi6viyztysbqnkvcqyel2i - create_public_playbook_run: - type: boolean - description: A boolean indicating whether the playbook runs created from this playbook should be public or private. - example: true - create_at: - type: integer - format: int64 - description: The playbook creation timestamp, formatted as the number of milliseconds since the Unix epoch. - example: 1602235338837 - delete_at: - type: integer - format: int64 - description: The playbook deletion timestamp, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the playbook is not deleted. - example: 0 - num_stages: - type: integer - format: int64 - description: The number of stages defined in this playbook. - example: 3 - num_steps: - type: integer - format: int64 - description: The total number of steps from all the stages defined in this playbook. - example: 18 - checklists: - type: array - description: The stages defined in this playbook. - items: - $ref: "#/components/schemas/Checklist" - member_ids: - description: The identifiers of all the users that are members of this playbook. - type: array - items: - type: string - description: User ID of the playbook member. - example: ilh6s1j4yefbdhxhtlzt179i6m - PlaybookList: - type: object - properties: - total_count: - type: integer - description: The total number of playbooks in the list, regardless of the paging. - format: int32 - example: 305 - page_count: - type: integer - description: The total number of pages. This depends on the total number of playbooks in the database and the per_page parameter sent with the request. - format: int32 - example: 2 - has_more: - type: boolean - description: A boolean describing whether there are more pages after the currently returned. - example: true - items: - type: array - description: The playbooks in this page. - items: - $ref: "#/components/schemas/Playbook" - Checklist: - type: object - properties: - id: - type: string - description: A unique, 26 characters long, alphanumeric identifier for the checklist. - example: 6f6nsgxzoq84fqh1dnlyivgafd - title: - type: string - description: The title of the checklist. - example: Triage issue - items: - type: array - description: The list of tasks to do. - items: - $ref: "#/components/schemas/ChecklistItem" - ChecklistItem: - type: object - properties: - id: - type: string - description: A unique, 26 characters long, alphanumeric identifier for the checklist item. - example: 6f6nsgxzoq84fqh1dnlyivgafd - title: - type: string - description: The title of the checklist item. - example: Gather information from customer. - state: - type: string - enum: - - "" - - in_progress - - closed - description: The state of the checklist item. An empty string means that the item is not done. - example: closed - state_modified: - type: integer - format: int64 - description: The timestamp for the latest modification of the item's state, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the item was never modified. - example: 1607774621321 - assignee_id: - type: string - description: The identifier of the user that has been assigned to complete this item. If the item has no assignee, this is an empty string. - example: pisdatkjtdlkdhht2v4inxuzx1 - assignee_modified: - type: integer - format: int64 - description: The timestamp for the latest modification of the item's assignee, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the item never got an assignee. - example: 1608897821125 - command: - type: string - description: The slash command associated with this item. If the item has no slash command associated, this is an empty string - example: /opsgenie on-call - command_last_run: - type: integer - format: int64 - description: The timestamp for the latest execution of the item's command, formatted as the number of milliseconds since the Unix epoch. It equals 0 if the command was never executed. - example: 1608552221019 - description: - type: string - description: A detailed description of the checklist item, formatted with Markdown. - example: Ask the customer for more information in [Zendesk](https://www.zendesk.com/). - Error: - type: object - required: - - error - - details - properties: - error: - type: string - description: A message with the error description. - example: Error retrieving the resource. - details: - type: string - description: Further details on where and why this error happened. - example: Specific details about the error, depending on the case. - WebhookOnCreationPayload: - allOf: - - $ref: "#/components/schemas/PlaybookRun" - - type: object - properties: - channel_url: - type: string - description: Absolute URL to the playbook run's channel. - example: https://example.com/ad-1/channels/channel-name - details_url: - type: string - description: Absolute URL to the playbook run's details. - example: https://example.com/ad-1/playbooks/runs/playbookRunID - WebhookOnStatusUpdatePayload: - allOf: - - $ref: "#/components/schemas/PlaybookRun" - - type: object - properties: - channel_url: - type: string - description: Absolute URL to the playbook run's channel. - example: https://example.com/ad-1/channels/channel-name - details_url: - type: string - description: Absolute URL to the playbook run's details. - example: https://example.com/ad-1/playbooks/runs/playbookRunID diff --git a/server/playbooks/server/api/bot.go b/server/playbooks/server/api/bot.go deleted file mode 100644 index 158247b5515..00000000000 --- a/server/playbooks/server/api/bot.go +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - "strconv" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -type BotHandler struct { - *ErrorHandler - api playbooks.ServicesAPI - poster bot.Poster - config config.Service - playbookRunService app.PlaybookRunService - userInfoStore app.UserInfoStore -} - -func NewBotHandler(router *mux.Router, api playbooks.ServicesAPI, poster bot.Poster, config config.Service, playbookRunService app.PlaybookRunService, userInfoStore app.UserInfoStore) *BotHandler { - handler := &BotHandler{ - ErrorHandler: &ErrorHandler{}, - api: api, - poster: poster, - config: config, - playbookRunService: playbookRunService, - userInfoStore: userInfoStore, - } - - botRouter := router.PathPrefix("/bot").Subrouter() - - notifyAdminsRouter := botRouter.PathPrefix("/notify-admins").Subrouter() - notifyAdminsRouter.HandleFunc("", withContext(handler.notifyAdmins)).Methods(http.MethodPost) - notifyAdminsRouter.HandleFunc("/button-start-trial", withContext(handler.startTrial)).Methods(http.MethodPost) - - botRouter.HandleFunc("/connect", withContext(handler.connect)).Methods(http.MethodGet) - - return handler -} - -type messagePayload struct { - MessageType string `json:"message_type"` -} - -func (h *BotHandler) notifyAdmins(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - var payload messagePayload - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode message", err) - return - } - - if err := h.poster.NotifyAdmins(payload.MessageType, userID, !h.api.IsEnterpriseReady()); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func CanStartTrialLicense(userID string, api playbooks.ServicesAPI) error { - if !api.HasPermissionTo(userID, model.PermissionManageLicenseInformation) { - return errors.Wrap(app.ErrNoPermissions, "no permission to manage license information") - } - - return nil -} - -func (h *BotHandler) startTrial(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - if err := CanStartTrialLicense(userID, h.api); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "no permission to start a trial license", err) - return - } - - var requestData *model.PostActionIntegrationRequest - err := json.NewDecoder(r.Body).Decode(&requestData) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to parse json", err) - return - } - if requestData == nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "missing request data", nil) - return - } - - users, ok := requestData.Context["users"].(float64) - if !ok { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: users is not a number", nil) - return - } - - termsAccepted, ok := requestData.Context["termsAccepted"].(bool) - if !ok { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: termsAccepted is not a boolean", nil) - return - } - - receiveEmailsAccepted, ok := requestData.Context["receiveEmailsAccepted"].(bool) - if !ok { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "malformed context: receiveEmailsAccepted is not a boolean", nil) - return - } - - originalPost, err := h.api.GetPost(requestData.PostId) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - // Modify the button text while the license is downloading - originalAttachments := originalPost.Attachments() -outer: - for _, attachment := range originalAttachments { - for _, action := range attachment.Actions { - if action.Id == "message" { - action.Name = "Requesting trial..." - break outer - } - } - } - model.ParseSlackAttachment(originalPost, originalAttachments) - _, _ = h.api.UpdatePost(originalPost) - - post := &model.Post{ - Id: requestData.PostId, - } - - if err := h.api.RequestTrialLicense(requestData.UserId, int(users), termsAccepted, receiveEmailsAccepted); err != nil { - post.Message = "Trial license could not be retrieved. Visit [https://mattermost.com/trial/](https://mattermost.com/trial/) to request a license." - - if _, postErr := h.api.UpdatePost(post); postErr != nil { - logrus.WithError(postErr).WithField("post_id", post.Id).Error("unable to edit the admin notification post") - } - - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to request the trial license", err) - return - } - - post.Message = "Thank you!" - attachments := []*model.SlackAttachment{ - { - Title: "You’re currently on a free trial of Mattermost Enterprise.", - Text: "Your free trial will expire in **30 days**. Visit our Customer Portal to purchase a license to continue using commercial edition features after your trial ends.\n[Purchase a license](https://customers.mattermost.com/signup)\n[Contact sales](https://mattermost.com/contact-us/)", - }, - } - model.ParseSlackAttachment(post, attachments) - - if _, err := h.api.UpdatePost(post); err != nil { - logrus.WithError(err).WithField("post_id", post.Id).Error("unable to edit the admin notification post") - } - - ReturnJSON(w, post, http.StatusOK) -} - -type DigestSenderParams struct { - isWeekly bool -} - -// connect handles the GET /bot/connect endpoint (a notification sent when the client wakes up or reconnects) -func (h *BotHandler) connect(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - info, err := h.userInfoStore.Get(userID) - if errors.Is(err, app.ErrNotFound) { - info = app.UserInfo{ - ID: userID, - } - } else if err != nil { - h.HandleError(w, c.logger, err) - return - } - - var timezone *time.Location - offset, _ := strconv.Atoi(r.Header.Get("X-Timezone-Offset")) - timezone = time.FixedZone("local", offset*60*60) - - sendRegularDigest := h.createDigestSender(c, w, userID, &info) - - // we want to first try a weekly digest - // if we have already sent it this week, try with a daily one - currentTime := time.UnixMilli(model.GetMillis()).In(timezone) - if app.ShouldSendWeeklyDigestMessage(info, timezone, currentTime) { - sendRegularDigest(DigestSenderParams{isWeekly: true}) - } else if app.ShouldSendDailyDigestMessage(info, timezone, currentTime) { - sendRegularDigest(DigestSenderParams{isWeekly: false}) - } - - w.WriteHeader(http.StatusOK) -} - -func (h *BotHandler) createDigestSender(c *Context, w http.ResponseWriter, userID string, userInfo *app.UserInfo) func(DigestSenderParams) { - return func(params DigestSenderParams) { - now := model.GetMillis() - // record that we're sending a DM now (this will prevent us trying over and over on every - // response if there's a failure later) - userInfo.LastDailyTodoDMAt = now - if err := h.userInfoStore.Upsert(*userInfo); err != nil { - h.HandleError(w, c.logger, err) - return - } - - regulartity := "daily" - if params.isWeekly { - regulartity = "weekly" - } - - if err := h.playbookRunService.DMTodoDigestToUser(userID, false, params.isWeekly); err != nil { - h.HandleError(w, c.logger, errors.Wrapf(err, "failed to send '%s' DMTodoDigest to userID '%s'", regulartity, userID)) - return - } - } -} diff --git a/server/playbooks/server/api/categories.go b/server/playbooks/server/api/categories.go deleted file mode 100644 index b9945baefe3..00000000000 --- a/server/playbooks/server/api/categories.go +++ /dev/null @@ -1,355 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" -) - -const maxItemsInRunsAndPlaybooksCategory = 1000 - -type CategoryHandler struct { - *ErrorHandler - api playbooks.ServicesAPI - categoryService app.CategoryService - playbookService app.PlaybookService - playbookRunService app.PlaybookRunService -} - -func NewCategoryHandler(router *mux.Router, api playbooks.ServicesAPI, categoryService app.CategoryService, playbookService app.PlaybookService, playbookRunService app.PlaybookRunService) *CategoryHandler { - handler := &CategoryHandler{ - ErrorHandler: &ErrorHandler{}, - api: api, - categoryService: categoryService, - playbookService: playbookService, - playbookRunService: playbookRunService, - } - - categoriesRouter := router.PathPrefix("/my_categories").Subrouter() - categoriesRouter.HandleFunc("", withContext(handler.getMyCategories)).Methods(http.MethodGet) - categoriesRouter.HandleFunc("", withContext(handler.createMyCategory)).Methods(http.MethodPost) - categoriesRouter.HandleFunc("/favorites", withContext(handler.isFavorite)).Methods(http.MethodGet) - - categoryRouter := categoriesRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter() - categoryRouter.HandleFunc("", withContext(handler.updateMyCategory)).Methods(http.MethodPut) - categoryRouter.HandleFunc("", withContext(handler.deleteMyCategory)).Methods(http.MethodDelete) - categoryRouter.HandleFunc("/collapse", withContext(handler.collapseMyCategory)).Methods(http.MethodPut) - - return handler -} - -func (h *CategoryHandler) getMyCategories(c *Context, w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - teamID := params.Get("team_id") - userID := r.Header.Get("Mattermost-User-ID") - customCategories, err := h.categoryService.GetCategories(teamID, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - filteredCustomCategories := filterEmptyCategories(customCategories) - - runsCategory, err := h.getRunsCategory(teamID, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - filteredRuns := filterDuplicatesFromCategory(runsCategory, filteredCustomCategories) - allCategories := append([]app.Category{}, customCategories...) - allCategories = append(allCategories, filteredRuns) - - playbooksCategory, err := h.getPlaybooksCategory(teamID, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - filteredPlaybooks := filterDuplicatesFromCategory(playbooksCategory, filteredCustomCategories) - allCategories = append(allCategories, filteredPlaybooks) - - ReturnJSON(w, allCategories, http.StatusOK) -} - -func (h *CategoryHandler) createMyCategory(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - var category app.Category - if err := json.NewDecoder(r.Body).Decode(&category); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode category", err) - return - } - - if category.ID != "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Category given already has ID", nil) - return - } - - // user can only create category for themselves - if category.UserID != userID { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, fmt.Sprintf("userID %s and category userID %s mismatch", userID, category.UserID), nil) - return - } - - createdCategory, err := h.categoryService.Create(category) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, createdCategory, http.StatusOK) -} - -func (h *CategoryHandler) updateMyCategory(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - categoryID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var category app.Category - if err := json.NewDecoder(r.Body).Decode(&category); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode category", err) - return - } - - if categoryID != category.ID { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "categoryID mismatch in patch and body", nil) - return - } - - // user can only update category for themselves - if category.UserID != userID { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "user ID mismatch in session and category", nil) - return - } - - // verify if category belongs to the user - existingCategory, err := h.categoryService.Get(category.ID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err) - return - } - - if existingCategory.DeleteAt != 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil) - return - } - - if existingCategory.UserID != category.UserID { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "UserID mismatch", nil) - return - } - - if err := h.categoryService.Update(category); err != nil { - h.HandleError(w, c.logger, err) - return - } - w.WriteHeader(http.StatusOK) -} - -func (h *CategoryHandler) collapseMyCategory(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - categoryID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var collapsed bool - if err := json.NewDecoder(r.Body).Decode(&collapsed); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode collapsed", err) - return - } - - existingCategory, err := h.categoryService.Get(categoryID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err) - return - } - - if existingCategory.DeleteAt != 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil) - return - } - - // verify if category belongs to the user - if existingCategory.UserID != userID { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "UserID mismatch", nil) - return - } - - if existingCategory.Collapsed == collapsed { - w.WriteHeader(http.StatusOK) - return - } - - patchedCategory := existingCategory - patchedCategory.Collapsed = collapsed - patchedCategory.UpdateAt = model.GetMillis() - - if err := h.categoryService.Update(patchedCategory); err != nil { - h.HandleError(w, c.logger, err) - return - } - w.WriteHeader(http.StatusOK) -} - -func (h *CategoryHandler) deleteMyCategory(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - categoryID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - existingCategory, err := h.categoryService.Get(categoryID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Can't get category", err) - return - } - - // category is already deleted. This avoids - // overriding the original deleted at timestamp - if existingCategory.DeleteAt != 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Category deleted", nil) - return - } - - // verify if category belongs to the user - if existingCategory.UserID != userID { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "UserID mismatch", nil) - return - } - - if err := h.categoryService.Delete(categoryID); err != nil { - h.HandleError(w, c.logger, err) - return - } - w.WriteHeader(http.StatusNoContent) -} - -func (h *CategoryHandler) isFavorite(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - params := r.URL.Query() - teamID := params.Get("team_id") - itemID := params.Get("item_id") - itemType := params.Get("type") - convertedItemType, err := app.StringToItemType(itemType) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - isFavorite, err := h.categoryService.IsItemFavorite(app.CategoryItem{ItemID: itemID, Type: convertedItemType}, teamID, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - ReturnJSON(w, isFavorite, http.StatusOK) -} - -func (h *CategoryHandler) getRunsCategory(teamID, userID string) (app.Category, error) { - runs, err := h.playbookRunService.GetPlaybookRuns( - app.RequesterInfo{ - UserID: userID, - TeamID: teamID, - }, - app.PlaybookRunFilterOptions{ - TeamID: teamID, - ParticipantOrFollowerID: userID, - Statuses: []string{app.StatusInProgress}, - Types: []string{app.RunTypePlaybook}, // only playbook runs can be viewed in Playbook product - Page: 0, - PerPage: maxItemsInRunsAndPlaybooksCategory, - }, - ) - if err != nil { - return app.Category{}, errors.Wrapf(err, "can't get playbook runs") - } - - runCategoryItems := []app.CategoryItem{} - for _, run := range runs.Items { - runCategoryItems = append(runCategoryItems, app.CategoryItem{ - ItemID: run.ID, - Type: app.RunItemType, - Name: run.Name, - }) - } - runCategory := app.Category{ - ID: "runsCategory", - Name: "Runs", - TeamID: teamID, - UserID: userID, - Collapsed: false, - Items: runCategoryItems, - } - return runCategory, nil -} - -func (h *CategoryHandler) getPlaybooksCategory(teamID, userID string) (app.Category, error) { - playbooks, err := h.playbookService.GetPlaybooksForTeam( - app.RequesterInfo{ - TeamID: teamID, - UserID: userID, - }, - teamID, - app.PlaybookFilterOptions{ - Page: 0, - PerPage: maxItemsInRunsAndPlaybooksCategory, - WithMembershipOnly: true, - }, - ) - if err != nil { - return app.Category{}, errors.Wrap(err, "can't get playbooks for team") - } - - playbookCategoryItems := []app.CategoryItem{} - for _, playbook := range playbooks.Items { - playbookCategoryItems = append(playbookCategoryItems, app.CategoryItem{ - ItemID: playbook.ID, - Type: app.PlaybookItemType, - Name: playbook.Title, - Public: playbook.Public, - }) - } - - playbookCategory := app.Category{ - ID: "playbooksCategory", - Name: "Playbooks", - TeamID: teamID, - UserID: userID, - Collapsed: false, - Items: playbookCategoryItems, - } - return playbookCategory, nil -} - -func categoriesContainItem(categories []app.Category, item app.CategoryItem) bool { - for _, category := range categories { - if category.ContainsItem(item) { - return true - } - } - return false -} - -func filterDuplicatesFromCategory(category app.Category, categories []app.Category) app.Category { - newItems := []app.CategoryItem{} - for _, item := range category.Items { - if !categoriesContainItem(categories, item) { - newItems = append(newItems, item) - } - } - category.Items = newItems - return category -} - -func filterEmptyCategories(categories []app.Category) []app.Category { - newCategories := []app.Category{} - for _, category := range categories { - if len(category.Items) > 0 { - newCategories = append(newCategories, category) - } - } - return newCategories -} diff --git a/server/playbooks/server/api/context.go b/server/playbooks/server/api/context.go deleted file mode 100644 index f6f782b089c..00000000000 --- a/server/playbooks/server/api/context.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "net/http" - - "github.com/sirupsen/logrus" -) - -// requestIDContextKeyType ensures requestIDContextKey can never collide with another context key -// having the same value. -type requestIDContextKeyType string - -// requestIDContextKey is the key for the incoming requestID. -var requestIDContextKey = requestIDContextKeyType("requestID") - -// getLogger builds a logger with the requestID attached to the given request. -func getLogger(r *http.Request) logrus.FieldLogger { - var logger logrus.FieldLogger = logrus.StandardLogger() - - requestID, ok := r.Context().Value(requestIDContextKey).(string) - if ok { - logger = logger.WithField("request_id", requestID) - } - - return logger -} - -type Context struct { - logger logrus.FieldLogger -} - -// withContext passes a logger to http handler functions. -func withContext(handler func(c *Context, w http.ResponseWriter, r *http.Request)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - logger := getLogger(r) - handler(&Context{logger}, w, r) - } -} diff --git a/server/playbooks/server/api/error_handler.go b/server/playbooks/server/api/error_handler.go deleted file mode 100644 index 2da64041f15..00000000000 --- a/server/playbooks/server/api/error_handler.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "net/http" - - "github.com/sirupsen/logrus" -) - -type ErrorHandler struct { -} - -// HandleError logs the internal error and sends a generic error as JSON in a 500 response. -func (h *ErrorHandler) HandleError(w http.ResponseWriter, logger logrus.FieldLogger, internalErr error) { - h.HandleErrorWithCode(w, logger, http.StatusInternalServerError, "An internal error has occurred. Check app server logs for details.", internalErr) -} - -// HandleErrorWithCode logs the internal error and sends the public facing error -// message as JSON in a response with the provided code. -func (h *ErrorHandler) HandleErrorWithCode(w http.ResponseWriter, logger logrus.FieldLogger, code int, publicErrorMsg string, internalErr error) { - HandleErrorWithCode(logger, w, code, publicErrorMsg, internalErr) -} - -// PermissionsCheck handles the output of a permission check -// Automatically does the proper error handling. -// Returns true if the check passed and false on failure. Correct use is: if !h.PermissionsCheck(w, check) { return } -func (h *ErrorHandler) PermissionsCheck(w http.ResponseWriter, logger logrus.FieldLogger, checkOutput error) bool { - if checkOutput != nil { - h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", checkOutput) - return false - } - - return true -} diff --git a/server/playbooks/server/api/graph_dataloader.go b/server/playbooks/server/api/graph_dataloader.go deleted file mode 100644 index bc08d885981..00000000000 --- a/server/playbooks/server/api/graph_dataloader.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "github.com/graph-gophers/dataloader/v7" -) - -const loaderBatchCapacity = 200 - -func populateResultWithError[K any](err error, result []*dataloader.Result[K]) []*dataloader.Result[K] { - for i := range result { - result[i] = &dataloader.Result[K]{Error: err} - } - return result -} diff --git a/server/playbooks/server/api/graphql.go b/server/playbooks/server/api/graphql.go deleted file mode 100644 index e67e7253773..00000000000 --- a/server/playbooks/server/api/graphql.go +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - _ "embed" - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - "github.com/graph-gophers/dataloader/v7" - graphql "github.com/graph-gophers/graphql-go" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -type GraphQLHandler struct { - *ErrorHandler - playbookService app.PlaybookService - playbookRunService app.PlaybookRunService - categoryService app.CategoryService - api playbooks.ServicesAPI - config config.Service - permissions *app.PermissionsService - playbookStore app.PlaybookStore - licenceChecker app.LicenseChecker - - schema *graphql.Schema -} - -//go:embed schema.graphqls -var SchemaFile string - -func NewGraphQLHandler( - router *mux.Router, - playbookService app.PlaybookService, - playbookRunService app.PlaybookRunService, - categoryService app.CategoryService, - api playbooks.ServicesAPI, - configService config.Service, - permissions *app.PermissionsService, - playbookStore app.PlaybookStore, - licenceChecker app.LicenseChecker, -) *GraphQLHandler { - handler := &GraphQLHandler{ - ErrorHandler: &ErrorHandler{}, - playbookService: playbookService, - playbookRunService: playbookRunService, - categoryService: categoryService, - api: api, - config: configService, - permissions: permissions, - playbookStore: playbookStore, - licenceChecker: licenceChecker, - } - - opts := []graphql.SchemaOpt{ - graphql.UseFieldResolvers(), - graphql.MaxParallelism(5), - } - - if !configService.IsConfiguredForDevelopmentAndTesting() { - opts = append(opts, - graphql.MaxDepth(8), - graphql.DisableIntrospection(), - ) - } - - root := &RootResolver{} - var err error - handler.schema, err = graphql.ParseSchema(SchemaFile, root, opts...) - if err != nil { - logrus.WithError(err).Error("unable to parse graphql schema") - return nil - } - - router.HandleFunc("/query", withContext(graphiQL)).Methods("GET") - router.HandleFunc("/query", withContext(handler.graphQL)).Methods("POST") - - return handler -} - -type ctxKey struct{} - -type GraphQLContext struct { - r *http.Request - playbookService app.PlaybookService - playbookRunService app.PlaybookRunService - playbookStore app.PlaybookStore - categoryService app.CategoryService - api playbooks.ServicesAPI - logger logrus.FieldLogger - config config.Service - permissions *app.PermissionsService - licenceChecker app.LicenseChecker - favoritesLoader *dataloader.Loader[favoriteInfo, bool] - playbooksLoader *dataloader.Loader[playbookInfo, *app.Playbook] -} - -// When moving over to the multi-product architecture this should be handled by the server. -func (h *GraphQLHandler) graphQL(c *Context, w http.ResponseWriter, r *http.Request) { - // Limit bodies to 300KiB. - r.Body = http.MaxBytesReader(w, r.Body, 300*1024) - - var params struct { - Query string `json:"query"` - OperationName string `json:"operationName"` - Variables map[string]interface{} `json:"variables"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - c.logger.WithError(err).Error("Unable to decode graphql query") - return - } - - if !h.config.IsConfiguredForDevelopmentAndTesting() { - if params.OperationName == "" { - c.logger.Warn("Invalid blank operation name") - return - } - } - - // dataloaders - favoritesLoader := dataloader.NewBatchedLoader(graphQLFavoritesLoader[bool], dataloader.WithBatchCapacity[favoriteInfo, bool](loaderBatchCapacity)) - playbooksLoader := dataloader.NewBatchedLoader(graphQLPlaybooksLoader[*app.Playbook], dataloader.WithBatchCapacity[playbookInfo, *app.Playbook](loaderBatchCapacity)) - - graphQLContext := &GraphQLContext{ - r: r, - playbookService: h.playbookService, - playbookRunService: h.playbookRunService, - categoryService: h.categoryService, - api: h.api, - logger: c.logger, - config: h.config, - permissions: h.permissions, - playbookStore: h.playbookStore, - licenceChecker: h.licenceChecker, - favoritesLoader: favoritesLoader, - playbooksLoader: playbooksLoader, - } - - // Populate the context with required info. - reqCtx := r.Context() - reqCtx = context.WithValue(reqCtx, ctxKey{}, graphQLContext) - - response := h.schema.Exec(reqCtx, - params.Query, - params.OperationName, - params.Variables, - ) - r.Header.Set("X-GQL-Operation", params.OperationName) - - for _, err := range response.Errors { - errLogger := c.logger.WithError(err).WithField("operation", params.OperationName) - - if errors.Is(err, app.ErrNoPermissions) { - errLogger.Warn("Warning executing request") - } else if err.Rule == "FieldsOnCorrectType" { - errLogger.Warn("Query for non existent field") - } else { - errLogger.Error("Error executing request") - } - } - - if err := json.NewEncoder(w).Encode(response); err != nil { - c.logger.WithError(err).Warn("Error while writing response") - } -} - -func getContext(ctx context.Context) (*GraphQLContext, error) { - c, ok := ctx.Value(ctxKey{}).(*GraphQLContext) - if !ok { - return nil, errors.New("custom context not found in context") - } - - return c, nil -} - -// GraphiqlPage is the html base code for the graphiQL query runner -// -//go:embed graphqli.html -var GraphiqlPage []byte - -func graphiQL(c *Context, w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/html") - _, _ = w.Write(GraphiqlPage) -} diff --git a/server/playbooks/server/api/graphql_loader_favorite.go b/server/playbooks/server/api/graphql_loader_favorite.go deleted file mode 100644 index 970152842c4..00000000000 --- a/server/playbooks/server/api/graphql_loader_favorite.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - - "github.com/graph-gophers/dataloader/v7" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" -) - -type favoriteInfo struct { - TeamID string - UserID string - ID string - Type app.CategoryItemType -} - -func graphQLFavoritesLoader[V bool](ctx context.Context, keys []favoriteInfo) []*dataloader.Result[V] { - result := make([]*dataloader.Result[V], len(keys)) - if len(keys) == 0 { - return result - } - - c, err := getContext(ctx) - if err != nil { - for i := range keys { - result[i] = &dataloader.Result[V]{Error: err} - } - return result - } - - // assume all keys are for the same team and user - teamID := keys[0].TeamID - userID := keys[0].UserID - - categoryItems := make([]app.CategoryItem, len(keys)) - for i, favorite := range keys { - categoryItems[i] = app.CategoryItem{ - ItemID: favorite.ID, - Type: favorite.Type, - } - } - - favorites, err := c.categoryService.AreItemsFavorites(categoryItems, teamID, userID) - if err != nil { - populateResultWithError(err, result) - } - - for i, fav := range favorites { - result[i] = &dataloader.Result[V]{Data: V(fav)} - } - - return result -} diff --git a/server/playbooks/server/api/graphql_loader_playbook.go b/server/playbooks/server/api/graphql_loader_playbook.go deleted file mode 100644 index 2259b4ce0a7..00000000000 --- a/server/playbooks/server/api/graphql_loader_playbook.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - - "github.com/graph-gophers/dataloader/v7" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" -) - -type playbookInfo struct { - UserID string - TeamID string - ID string -} - -func graphQLPlaybooksLoader[V *app.Playbook](ctx context.Context, keys []playbookInfo) []*dataloader.Result[V] { - result := make([]*dataloader.Result[V], len(keys)) - - if len(keys) == 0 { - return result - } - - uniquePlaybookIDs := getUniquePlaybookIDs(keys) - - var teamID, userID string = keys[0].TeamID, keys[0].UserID - - c, err := getContext(ctx) - if err != nil { - return populateResultWithError(err, result) - } - - playbookResult, err := c.playbookService.GetPlaybooksForTeam( - app.RequesterInfo{ - UserID: userID, - TeamID: teamID, - }, - teamID, - app.PlaybookFilterOptions{ - PlaybookIDs: uniquePlaybookIDs, - PerPage: loaderBatchCapacity, - }, - ) - if err != nil { - return populateResultWithError(err, result) - } - playbooksByID := make(map[string]*app.Playbook) - for i := range playbookResult.Items { - playbooksByID[playbookResult.Items[i].ID] = &playbookResult.Items[i] - } - - for i, playbookInfo := range keys { - playbook, ok := playbooksByID[playbookInfo.ID] - if !ok { - result[i] = &dataloader.Result[V]{Data: nil} - continue - } - result[i] = &dataloader.Result[V]{ - Data: V(playbook), - } - } - return result -} - -func getUniquePlaybookIDs(playbooks []playbookInfo) []string { - playbookByID := make(map[string]bool) - - for _, playbook := range playbooks { - playbookByID[playbook.ID] = true - } - - result := make([]string, 0, len(playbookByID)) - for playbookID := range playbookByID { - result = append(result, playbookID) - } - return result -} diff --git a/server/playbooks/server/api/graphql_playbook.go b/server/playbooks/server/api/graphql_playbook.go deleted file mode 100644 index 8b2d01b3392..00000000000 --- a/server/playbooks/server/api/graphql_playbook.go +++ /dev/null @@ -1,239 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - "fmt" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/sirupsen/logrus" -) - -type PlaybookResolver struct { - app.Playbook -} - -func (r *PlaybookResolver) ChannelMode(ctx context.Context) string { - return fmt.Sprint(r.Playbook.ChannelMode) -} - -func (r *PlaybookResolver) IsFavorite(ctx context.Context) (bool, error) { - c, err := getContext(ctx) - if err != nil { - return false, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - thunk := c.favoritesLoader.Load(ctx, favoriteInfo{ - TeamID: r.TeamID, - UserID: userID, - Type: app.PlaybookItemType, - ID: r.ID, - }) - - result, err := thunk() - if err != nil { - return false, err - } - return result, nil -} - -func (r *PlaybookResolver) DeleteAt() float64 { - return float64(r.Playbook.DeleteAt) -} - -func (r *PlaybookResolver) LastRunAt() float64 { - return float64(r.Playbook.LastRunAt) -} - -func (r *PlaybookResolver) NumRuns() int32 { - return int32(r.Playbook.NumRuns) -} - -func (r *PlaybookResolver) ActiveRuns() int32 { - return int32(r.Playbook.ActiveRuns) -} - -func (r *PlaybookResolver) RetrospectiveReminderIntervalSeconds() float64 { - return float64(r.Playbook.RetrospectiveReminderIntervalSeconds) -} - -func (r *PlaybookResolver) ReminderTimerDefaultSeconds() float64 { - return float64(r.Playbook.ReminderTimerDefaultSeconds) -} - -func (r *PlaybookResolver) Metrics() []*MetricConfigResolver { - metricConfigResolvers := make([]*MetricConfigResolver, 0, len(r.Playbook.Metrics)) - for _, metricConfig := range r.Playbook.Metrics { - metricConfigResolvers = append(metricConfigResolvers, &MetricConfigResolver{metricConfig}) - } - - return metricConfigResolvers -} - -type MetricConfigResolver struct { - app.PlaybookMetricConfig -} - -func (r *MetricConfigResolver) Target() *int32 { - if r.PlaybookMetricConfig.Target.Valid { - intvalue := int32(r.PlaybookMetricConfig.Target.ValueOrZero()) - return &intvalue - } - return nil -} - -func (r *PlaybookResolver) Checklists() []*ChecklistResolver { - checklistResolvers := make([]*ChecklistResolver, 0, len(r.Playbook.Checklists)) - for _, checklist := range r.Playbook.Checklists { - checklistResolvers = append(checklistResolvers, &ChecklistResolver{checklist}) - } - - return checklistResolvers -} - -type ChecklistResolver struct { - app.Checklist -} - -func (r *ChecklistResolver) Items() []*ChecklistItemResolver { - checklistItemResolvers := make([]*ChecklistItemResolver, 0, len(r.Checklist.Items)) - for _, items := range r.Checklist.Items { - checklistItemResolvers = append(checklistItemResolvers, &ChecklistItemResolver{items}) - } - - return checklistItemResolvers -} - -type ChecklistItemResolver struct { - app.ChecklistItem -} - -func (r *ChecklistItemResolver) StateModified() float64 { - return float64(r.ChecklistItem.StateModified) -} - -func (r *ChecklistItemResolver) AssigneeModified() float64 { - return float64(r.ChecklistItem.AssigneeModified) -} - -func (r *ChecklistItemResolver) CommandLastRun() float64 { - return float64(r.ChecklistItem.CommandLastRun) -} - -func (r *ChecklistItemResolver) DueDate() float64 { - return float64(r.ChecklistItem.DueDate) -} - -func (r *ChecklistItemResolver) TaskActions() []*TaskActionResolver { - taskActionsResolvers := make([]*TaskActionResolver, 0, len(r.ChecklistItem.TaskActions)) - for _, taskAction := range r.ChecklistItem.TaskActions { - taskActionsResolvers = append(taskActionsResolvers, &TaskActionResolver{taskAction}) - } - - return taskActionsResolvers -} - -type TaskActionResolver struct { - app.TaskAction -} - -func (r *TaskActionResolver) Trigger() *TriggerResolver { - return &TriggerResolver{r.TaskAction.Trigger} -} - -func (r *TaskActionResolver) Actions() []*ActionResolver { - actionsResolvers := make([]*ActionResolver, 0, len(r.TaskAction.Actions)) - for _, action := range r.TaskAction.Actions { - actionsResolvers = append(actionsResolvers, &ActionResolver{action}) - } - return actionsResolvers -} - -type ActionResolver struct { - app.Action -} - -func (r *ActionResolver) Type() string { - return string(r.Action.Type) -} - -func (r *ActionResolver) Payload() string { - var payload string - switch r.Action.Type { - case app.MarkItemAsDoneActionType: - payload = r.Action.Payload - default: - logrus.WithField("task_action_type", r.Action.Type).Error("Unknown trigger type") - payload = "" - } - return payload -} - -type TriggerResolver struct { - app.Trigger -} - -func (r *TriggerResolver) Type() string { - return string(r.Trigger.Type) -} - -func (r *TriggerResolver) Payload() string { - var payload string - switch r.Trigger.Type { - case app.KeywordsByUsersTriggerType: - payload = r.Trigger.Payload - default: - logrus.WithField("task_trigger_type", r.Trigger.Type).Error("Unknown trigger type") - payload = "" - } - return payload -} - -type UpdateChecklist struct { - Title string `json:"title"` - Items []UpdateChecklistItem `json:"items"` -} - -func (c UpdateChecklist) GetItems() []app.ChecklistItemCommon { - items := make([]app.ChecklistItemCommon, len(c.Items)) - for i := range c.Items { - items[i] = &c.Items[i] - } - return items -} - -type UpdateChecklistItem struct { - Title string `json:"title"` - State string `json:"state"` - StateModified float64 `json:"state_modified"` - AssigneeID string `json:"assignee_id"` - AssigneeModified float64 `json:"assignee_modified"` - Command string `json:"command"` - CommandLastRun float64 `json:"command_last_run"` - Description string `json:"description"` - LastSkipped float64 `json:"delete_at"` - DueDate float64 `json:"due_date"` - TaskActions *[]app.TaskAction `json:"task_actions"` -} - -func (ci *UpdateChecklistItem) GetAssigneeID() string { - return ci.AssigneeID -} - -func (ci *UpdateChecklistItem) SetAssigneeModified(modified int64) { - ci.AssigneeModified = float64(modified) -} - -func (ci *UpdateChecklistItem) SetState(state string) { - ci.State = state -} - -func (ci *UpdateChecklistItem) SetStateModified(modified int64) { - ci.StateModified = float64(modified) -} - -func (ci *UpdateChecklistItem) SetCommandLastRun(lastRun int64) { - ci.CommandLastRun = float64(lastRun) -} diff --git a/server/playbooks/server/api/graphql_root.go b/server/playbooks/server/api/graphql_root.go deleted file mode 100644 index d5dd8cbd562..00000000000 --- a/server/playbooks/server/api/graphql_root.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "strings" -) - -type RootResolver struct { - RunRootResolver - PlaybookRootResolver -} - -func addToSetmap[T any](setmap map[string]interface{}, name string, value *T) { - if value != nil { - setmap[name] = *value - } -} - -func addConcatToSetmap(setmap map[string]interface{}, name string, value *[]string) { - if value != nil { - setmap[name] = strings.Join(*value, ",") - } -} diff --git a/server/playbooks/server/api/graphql_root_playbook.go b/server/playbooks/server/api/graphql_root_playbook.go deleted file mode 100644 index 60f3b816692..00000000000 --- a/server/playbooks/server/api/graphql_root_playbook.go +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - "encoding/json" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" - "gopkg.in/guregu/null.v4" -) - -// RunMutationCollection hold all mutation functions for a playbookRun -type PlaybookRootResolver struct { -} - -func getGraphqlPlaybook(ctx context.Context, playbookID string) (*PlaybookResolver, error) { - c, err := getContext(ctx) - if err != nil { - return nil, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - if err = c.permissions.PlaybookView(userID, playbookID); err != nil { - return nil, err - } - - playbook, err := c.playbookService.Get(playbookID) - if err != nil { - return nil, err - } - - return &PlaybookResolver{playbook}, nil -} - -func (r *PlaybookRootResolver) Playbook(ctx context.Context, args struct { - ID string -}) (*PlaybookResolver, error) { - playbookID := args.ID - return getGraphqlPlaybook(ctx, playbookID) -} - -func (r *PlaybookRootResolver) Playbooks(ctx context.Context, args struct { - TeamID string - Sort string - Direction string - SearchTerm string - WithMembershipOnly bool - WithArchived bool -}) ([]*PlaybookResolver, error) { - c, err := getContext(ctx) - if err != nil { - return nil, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - if args.TeamID != "" { - if err = c.permissions.PlaybookList(userID, args.TeamID); err != nil { - return nil, err - } - } - - isGuest, err := app.IsGuest(userID, c.api) - if err != nil { - return nil, err - } - - requesterInfo := app.RequesterInfo{ - UserID: userID, - TeamID: args.TeamID, - IsAdmin: app.IsSystemAdmin(userID, c.api), - } - - opts := app.PlaybookFilterOptions{ - Sort: app.SortField(args.Sort), - Direction: app.SortDirection(args.Direction), - SearchTerm: args.SearchTerm, - WithArchived: args.WithArchived, - WithMembershipOnly: isGuest || args.WithMembershipOnly, // Guests can only see playbooks if they are invited to them - Page: 0, - PerPage: 10000, - } - - playbookResults, err := c.playbookService.GetPlaybooksForTeam(requesterInfo, args.TeamID, opts) - if err != nil { - return nil, err - } - - ret := make([]*PlaybookResolver, 0, len(playbookResults.Items)) - for _, pb := range playbookResults.Items { - ret = append(ret, &PlaybookResolver{pb}) - } - - return ret, nil -} - -func (r *RunRootResolver) UpdatePlaybookFavorite(ctx context.Context, args struct { - ID string - Favorite bool -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - - userID := c.r.Header.Get("Mattermost-User-ID") - - if err = c.permissions.PlaybookView(userID, args.ID); err != nil { - return "", err - } - - currentPlaybook, err := c.playbookService.Get(args.ID) - if err != nil { - return "", err - } - - if currentPlaybook.DeleteAt != 0 { - return "", errors.New("archived playbooks can not be modified") - } - - if args.Favorite { - if err := c.categoryService.AddFavorite( - app.CategoryItem{ - ItemID: currentPlaybook.ID, - Type: app.PlaybookItemType, - }, - currentPlaybook.TeamID, - userID, - ); err != nil { - return "", err - } - } else { - if err := c.categoryService.DeleteFavorite( - app.CategoryItem{ - ItemID: currentPlaybook.ID, - Type: app.PlaybookItemType, - }, - currentPlaybook.TeamID, - userID, - ); err != nil { - return "", err - } - } - - return currentPlaybook.ID, nil -} - -func (r *PlaybookRootResolver) UpdatePlaybook(ctx context.Context, args struct { - ID string - Updates struct { - Title *string - Description *string - Public *bool - CreatePublicPlaybookRun *bool - ReminderMessageTemplate *string - ReminderTimerDefaultSeconds *float64 - StatusUpdateEnabled *bool - InvitedUserIDs *[]string - InvitedGroupIDs *[]string - InviteUsersEnabled *bool - DefaultOwnerID *string - DefaultOwnerEnabled *bool - BroadcastChannelIDs *[]string - BroadcastEnabled *bool - WebhookOnCreationURLs *[]string - WebhookOnCreationEnabled *bool - MessageOnJoin *string - MessageOnJoinEnabled *bool - RetrospectiveReminderIntervalSeconds *float64 - RetrospectiveTemplate *string - RetrospectiveEnabled *bool - WebhookOnStatusUpdateURLs *[]string - WebhookOnStatusUpdateEnabled *bool - SignalAnyKeywords *[]string - SignalAnyKeywordsEnabled *bool - CategorizeChannelEnabled *bool - CategoryName *string - RunSummaryTemplateEnabled *bool - RunSummaryTemplate *string - ChannelNameTemplate *string - Checklists *[]UpdateChecklist - CreateChannelMemberOnNewParticipant *bool - RemoveChannelMemberOnRemovedParticipant *bool - ChannelID *string - ChannelMode *string - } -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - currentPlaybook, err := c.playbookService.Get(args.ID) - if err != nil { - return "", err - } - - if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil { - return "", err - } - - if currentPlaybook.DeleteAt != 0 { - return "", errors.New("archived playbooks can not be modified") - } - - setmap := map[string]interface{}{} - addToSetmap(setmap, "Title", args.Updates.Title) - addToSetmap(setmap, "Description", args.Updates.Description) - if args.Updates.Public != nil { - if *args.Updates.Public { - if err := c.permissions.PlaybookMakePublic(userID, currentPlaybook); err != nil { - return "", err - } - } else { - if err := c.permissions.PlaybookMakePrivate(userID, currentPlaybook); err != nil { - return "", err - } - } - if !c.licenceChecker.PlaybookAllowed(*args.Updates.Public) { - return "", errors.Wrapf(app.ErrLicensedFeature, "the playbook is not valid with the current license") - } - addToSetmap(setmap, "Public", args.Updates.Public) - } - addToSetmap(setmap, "CreatePublicIncident", args.Updates.CreatePublicPlaybookRun) - addToSetmap(setmap, "ReminderMessageTemplate", args.Updates.ReminderMessageTemplate) - addToSetmap(setmap, "ReminderTimerDefaultSeconds", args.Updates.ReminderTimerDefaultSeconds) - addToSetmap(setmap, "StatusUpdateEnabled", args.Updates.StatusUpdateEnabled) - addToSetmap(setmap, "CreateChannelMemberOnNewParticipant", args.Updates.CreateChannelMemberOnNewParticipant) - addToSetmap(setmap, "RemoveChannelMemberOnRemovedParticipant", args.Updates.RemoveChannelMemberOnRemovedParticipant) - - if args.Updates.InvitedUserIDs != nil { - filteredInvitedUserIDs := c.permissions.FilterInvitedUserIDs(*args.Updates.InvitedUserIDs, currentPlaybook.TeamID) - addConcatToSetmap(setmap, "ConcatenatedInvitedUserIDs", &filteredInvitedUserIDs) - } - - if args.Updates.InvitedGroupIDs != nil { - filteredInvitedGroupIDs := c.permissions.FilterInvitedGroupIDs(*args.Updates.InvitedGroupIDs) - addConcatToSetmap(setmap, "ConcatenatedInvitedGroupIDs", &filteredInvitedGroupIDs) - } - - addToSetmap(setmap, "InviteUsersEnabled", args.Updates.InviteUsersEnabled) - if args.Updates.DefaultOwnerID != nil { - if !c.api.HasPermissionToTeam(*args.Updates.DefaultOwnerID, currentPlaybook.TeamID, model.PermissionViewTeam) { - return "", errors.Wrap(app.ErrNoPermissions, "default owner can't view team") - } - addToSetmap(setmap, "DefaultCommanderID", args.Updates.DefaultOwnerID) - } - addToSetmap(setmap, "DefaultCommanderEnabled", args.Updates.DefaultOwnerEnabled) - - if args.Updates.BroadcastChannelIDs != nil { - if err := c.permissions.NoAddedBroadcastChannelsWithoutPermission(userID, *args.Updates.BroadcastChannelIDs, currentPlaybook.BroadcastChannelIDs); err != nil { - return "", err - } - addConcatToSetmap(setmap, "ConcatenatedBroadcastChannelIDs", args.Updates.BroadcastChannelIDs) - } - - addToSetmap(setmap, "BroadcastEnabled", args.Updates.BroadcastEnabled) - if args.Updates.WebhookOnCreationURLs != nil { - if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnCreationURLs); err != nil { - return "", err - } - addConcatToSetmap(setmap, "ConcatenatedWebhookOnCreationURLs", args.Updates.WebhookOnCreationURLs) - } - addToSetmap(setmap, "WebhookOnCreationEnabled", args.Updates.WebhookOnCreationEnabled) - addToSetmap(setmap, "MessageOnJoin", args.Updates.MessageOnJoin) - addToSetmap(setmap, "MessageOnJoinEnabled", args.Updates.MessageOnJoinEnabled) - addToSetmap(setmap, "RetrospectiveReminderIntervalSeconds", args.Updates.RetrospectiveReminderIntervalSeconds) - addToSetmap(setmap, "RetrospectiveTemplate", args.Updates.RetrospectiveTemplate) - addToSetmap(setmap, "RetrospectiveEnabled", args.Updates.RetrospectiveEnabled) - if args.Updates.WebhookOnStatusUpdateURLs != nil { - if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnStatusUpdateURLs); err != nil { - return "", err - } - addConcatToSetmap(setmap, "ConcatenatedWebhookOnStatusUpdateURLs", args.Updates.WebhookOnStatusUpdateURLs) - } - addToSetmap(setmap, "WebhookOnStatusUpdateEnabled", args.Updates.WebhookOnStatusUpdateEnabled) - if args.Updates.SignalAnyKeywords != nil { - validSignalAnyKeywords := app.ProcessSignalAnyKeywords(*args.Updates.SignalAnyKeywords) - addConcatToSetmap(setmap, "ConcatenatedSignalAnyKeywords", &validSignalAnyKeywords) - } - addToSetmap(setmap, "SignalAnyKeywordsEnabled", args.Updates.SignalAnyKeywordsEnabled) - addToSetmap(setmap, "CategorizeChannelEnabled", args.Updates.CategorizeChannelEnabled) - if args.Updates.CategoryName != nil { - if err := app.ValidateCategoryName(*args.Updates.CategoryName); err != nil { - return "", err - } - addToSetmap(setmap, "CategoryName", args.Updates.CategoryName) - } - addToSetmap(setmap, "RunSummaryTemplateEnabled", args.Updates.RunSummaryTemplateEnabled) - addToSetmap(setmap, "RunSummaryTemplate", args.Updates.RunSummaryTemplate) - addToSetmap(setmap, "ChannelNameTemplate", args.Updates.ChannelNameTemplate) - addToSetmap(setmap, "ChannelID", args.Updates.ChannelID) - addToSetmap(setmap, "ChannelMode", args.Updates.ChannelMode) - - // Not optimal graphql. Stopgap measure. Should be updated separately. - if args.Updates.Checklists != nil { - app.CleanUpChecklists(*args.Updates.Checklists) - if err := validateUpdateTaskActions(*args.Updates.Checklists); err != nil { - return "", errors.Wrapf(err, "failed to validate task actions in graphql json for playbook id: '%s'", args.ID) - } - checklistsJSON, err := json.Marshal(args.Updates.Checklists) - if err != nil { - return "", errors.Wrapf(err, "failed to marshal checklist in graphql json for playbook id: '%s'", args.ID) - } - setmap["ChecklistsJSON"] = checklistsJSON - } - - if args.Updates.Checklists != nil || args.Updates.InvitedUserIDs != nil || args.Updates.InviteUsersEnabled != nil { - if err := validatePreAssignmentUpdate(currentPlaybook, args.Updates.Checklists, args.Updates.InvitedUserIDs, args.Updates.InviteUsersEnabled); err != nil { - return "", errors.Wrapf(err, "invalid user pre-assignment for playbook id: '%s'", args.ID) - } - } - - if len(setmap) > 0 { - if err := c.playbookStore.GraphqlUpdate(args.ID, setmap); err != nil { - return "", err - } - } - - return args.ID, nil -} - -func (r *PlaybookRootResolver) AddPlaybookMember(ctx context.Context, args struct { - PlaybookID string - UserID string -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - currentPlaybook, err := c.playbookService.Get(args.PlaybookID) - if err != nil { - return "", err - } - - if err := c.permissions.PlaybookManageMembers(userID, currentPlaybook); err != nil { - return "", err - } - - if currentPlaybook.DeleteAt != 0 { - return "", errors.New("archived playbooks can not be modified") - } - - if err := c.playbookStore.AddPlaybookMember(args.PlaybookID, args.UserID); err != nil { - return "", errors.Wrap(err, "unable to add playbook member") - } - - return "", nil -} - -func (r *PlaybookRootResolver) RemovePlaybookMember(ctx context.Context, args struct { - PlaybookID string - UserID string -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - currentPlaybook, err := c.playbookService.Get(args.PlaybookID) - if err != nil { - return "", err - } - - if currentPlaybook.DeleteAt != 0 { - return "", errors.New("archived playbooks can not be modified") - } - - // do not require manageMembers permission if the user want to leave playbook - if userID != args.UserID { - if err := c.permissions.PlaybookManageMembers(userID, currentPlaybook); err != nil { - return "", err - } - } - - if err := c.playbookStore.RemovePlaybookMember(args.PlaybookID, args.UserID); err != nil { - return "", errors.Wrap(err, "unable to remove playbook member") - } - - return "", nil -} - -func (r *PlaybookRootResolver) AddMetric(ctx context.Context, args struct { - PlaybookID string - Title string - Description string - Type string - Target *float64 -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - currentPlaybook, err := c.playbookService.Get(args.PlaybookID) - if err != nil { - return "", err - } - - if currentPlaybook.DeleteAt != 0 { - return "", errors.New("archived playbooks can not be modified") - } - - if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil { - return "", err - } - - var target null.Int - if args.Target == nil { - target = null.NewInt(0, false) - } else { - target = null.IntFrom(int64(*args.Target)) - } - - if err := c.playbookStore.AddMetric(args.PlaybookID, app.PlaybookMetricConfig{ - Title: args.Title, - Description: args.Description, - Type: args.Type, - Target: target, - }); err != nil { - return "", err - } - - return args.PlaybookID, nil -} - -func (r *PlaybookRootResolver) UpdateMetric(ctx context.Context, args struct { - ID string - Title *string - Description *string - Target *float64 -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - currentMetric, err := c.playbookStore.GetMetric(args.ID) - if err != nil { - return "", err - } - - currentPlaybook, err := c.playbookService.Get(currentMetric.PlaybookID) - if err != nil { - return "", err - } - - if currentPlaybook.DeleteAt != 0 { - return "", errors.New("archived playbooks can not be modified") - } - - if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil { - return "", err - } - - setmap := map[string]interface{}{} - addToSetmap(setmap, "Title", args.Title) - addToSetmap(setmap, "Description", args.Description) - if args.Target != nil { - setmap["Target"] = null.IntFrom(int64(*args.Target)) - } - if len(setmap) > 0 { - if err := c.playbookStore.UpdateMetric(args.ID, setmap); err != nil { - return "", err - } - } - - return args.ID, nil -} - -func (r *PlaybookRootResolver) DeleteMetric(ctx context.Context, args struct { - ID string -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - currentMetric, err := c.playbookStore.GetMetric(args.ID) - if err != nil { - return "", err - } - - currentPlaybook, err := c.playbookService.Get(currentMetric.PlaybookID) - if err != nil { - return "", err - } - - if err := c.permissions.PlaybookManageProperties(userID, currentPlaybook); err != nil { - return "", err - } - - if err := c.playbookStore.DeleteMetric(args.ID); err != nil { - return "", err - } - - return args.ID, nil -} - -func validatePreAssignmentUpdate[T app.ChecklistCommon](pb app.Playbook, newChecklists *[]T, newInvitedUsers *[]string, newInviteUsersEnabled *bool) error { - assignees := app.GetDistinctAssignees(pb.Checklists) - if newChecklists != nil { - assignees = app.GetDistinctAssignees(*newChecklists) - } - - invitedUsers := pb.InvitedUserIDs - if newInvitedUsers != nil { - invitedUsers = *newInvitedUsers - } - - inviteUsersEnabled := pb.InviteUsersEnabled - if newInviteUsersEnabled != nil { - inviteUsersEnabled = *newInviteUsersEnabled - } - - return app.ValidatePreAssignment(assignees, invitedUsers, inviteUsersEnabled) -} - -// validateUpdateTaskActions validates the taskactions in the given checklist -// NOTE: Any changes to this function must be made to function 'validateTaskActions' for the REST endpoint. -func validateUpdateTaskActions(checklists []UpdateChecklist) error { - for _, checklist := range checklists { - for _, item := range checklist.Items { - if taskActions := item.TaskActions; taskActions != nil { - for _, ta := range *taskActions { - if err := app.ValidateTrigger(ta.Trigger); err != nil { - return err - } - for _, a := range ta.Actions { - if err := app.ValidateAction(a); err != nil { - return err - } - } - } - } - } - } - return nil -} diff --git a/server/playbooks/server/api/graphql_root_run.go b/server/playbooks/server/api/graphql_root_run.go deleted file mode 100644 index 7c4b590915d..00000000000 --- a/server/playbooks/server/api/graphql_root_run.go +++ /dev/null @@ -1,327 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -// RunRootResolver hold all queries and mutations for a playbookRun -type RunRootResolver struct { -} - -func (r *RunRootResolver) Run(ctx context.Context, args struct { - ID string `url:"id,omitempty"` -}) (*RunResolver, error) { - c, err := getContext(ctx) - if err != nil { - return nil, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - if err = c.permissions.RunView(userID, args.ID); err != nil { - return nil, err - } - - run, err := c.playbookRunService.GetPlaybookRun(args.ID) - if err != nil { - return nil, err - } - - return &RunResolver{*run}, nil -} - -func (r *RunRootResolver) Runs(ctx context.Context, args struct { - TeamID string - Sort string - Direction string - Statuses []string - ParticipantOrFollowerID string - ChannelID string - First *int32 - After *string - Types []string -}) (*RunConnectionResolver, error) { - c, err := getContext(ctx) - if err != nil { - return nil, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - requesterInfo := app.RequesterInfo{ - UserID: userID, - TeamID: args.TeamID, - IsAdmin: app.IsSystemAdmin(userID, c.api), - } - - if args.ParticipantOrFollowerID == client.Me { - args.ParticipantOrFollowerID = userID - } - - perPage := 10000 // If paging not specified, get "everything" - if args.First != nil { - perPage = int(*args.First) - } - - page := 0 - if args.After != nil { - page, err = decodeRunConnectionCursor(*args.After) - if err != nil { - return nil, err - } - } - - filterOptions := app.PlaybookRunFilterOptions{ - Sort: app.SortField(args.Sort), - Direction: app.SortDirection(args.Direction), - TeamID: args.TeamID, - Statuses: args.Statuses, - ParticipantOrFollowerID: args.ParticipantOrFollowerID, - ChannelID: args.ChannelID, - IncludeFavorites: true, - Types: args.Types, - Page: page, - PerPage: perPage, - } - - runResults, err := c.playbookRunService.GetPlaybookRuns(requesterInfo, filterOptions) - if err != nil { - return nil, err - } - - return &RunConnectionResolver{results: *runResults, page: page}, nil -} - -func (r *RunRootResolver) SetRunFavorite(ctx context.Context, args struct { - ID string - Fav bool -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - if err = c.permissions.RunView(userID, args.ID); err != nil { - return "", err - } - - playbookRun, err := c.playbookRunService.GetPlaybookRun(args.ID) - if err != nil { - return "", err - } - - if args.Fav { - if err := c.categoryService.AddFavorite( - app.CategoryItem{ - ItemID: playbookRun.ID, - Type: app.RunItemType, - }, - playbookRun.TeamID, - userID, - ); err != nil { - return "", err - } - } else { - if err := c.categoryService.DeleteFavorite( - app.CategoryItem{ - ItemID: playbookRun.ID, - Type: app.RunItemType, - }, - playbookRun.TeamID, - userID, - ); err != nil { - return "", err - } - } - - return playbookRun.ID, nil -} - -type RunUpdates struct { - Name *string - Summary *string - ChannelID *string - CreateChannelMemberOnNewParticipant *bool - RemoveChannelMemberOnRemovedParticipant *bool - StatusUpdateBroadcastChannelsEnabled *bool - StatusUpdateBroadcastWebhooksEnabled *bool - BroadcastChannelIDs *[]string - WebhookOnStatusUpdateURLs *[]string -} - -func (r *RunRootResolver) UpdateRun(ctx context.Context, args struct { - ID string - Updates RunUpdates -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - if err = c.permissions.RunManageProperties(userID, args.ID); err != nil { - return "", err - } - - playbookRun, err := c.playbookRunService.GetPlaybookRun(args.ID) - if err != nil { - return "", err - } - - now := model.GetMillis() - - // scalar updates - setmap := map[string]interface{}{} - addToSetmap(setmap, "Name", args.Updates.Name) - addToSetmap(setmap, "Description", args.Updates.Summary) - addToSetmap(setmap, "ChannelID", args.Updates.ChannelID) - addToSetmap(setmap, "CreateChannelMemberOnNewParticipant", args.Updates.CreateChannelMemberOnNewParticipant) - addToSetmap(setmap, "RemoveChannelMemberOnRemovedParticipant", args.Updates.RemoveChannelMemberOnRemovedParticipant) - addToSetmap(setmap, "StatusUpdateBroadcastChannelsEnabled", args.Updates.StatusUpdateBroadcastChannelsEnabled) - addToSetmap(setmap, "StatusUpdateBroadcastWebhooksEnabled", args.Updates.StatusUpdateBroadcastWebhooksEnabled) - - if args.Updates.Summary != nil { - addToSetmap(setmap, "SummaryModifiedAt", &now) - } - - if args.Updates.BroadcastChannelIDs != nil { - if err := c.permissions.NoAddedBroadcastChannelsWithoutPermission(userID, *args.Updates.BroadcastChannelIDs, playbookRun.BroadcastChannelIDs); err != nil { - return "", err - } - addConcatToSetmap(setmap, "ConcatenatedBroadcastChannelIDs", args.Updates.BroadcastChannelIDs) - } - - if args.Updates.WebhookOnStatusUpdateURLs != nil { - if err := app.ValidateWebhookURLs(*args.Updates.WebhookOnStatusUpdateURLs); err != nil { - return "", err - } - addConcatToSetmap(setmap, "ConcatenatedWebhookOnStatusUpdateURLs", args.Updates.WebhookOnStatusUpdateURLs) - } - - if err := c.playbookRunService.GraphqlUpdate(args.ID, setmap); err != nil { - return "", err - } - - return playbookRun.ID, nil -} - -func (r *RunRootResolver) AddRunParticipants(ctx context.Context, args struct { - RunID string - UserIDs []string - ForceAddToChannel bool -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - // When user is joining run RunView permission is enough, otherwise user need manage permissions - if updatesOnlyRequesterMembership(userID, args.UserIDs) { - if err := c.permissions.RunView(userID, args.RunID); err != nil { - return "", errors.Wrap(err, "attempted to join run without permissions") - } - } else { - if err := c.permissions.RunManageProperties(userID, args.RunID); err != nil { - return "", errors.Wrap(err, "attempted to modify participants without permissions") - } - } - - if err := c.playbookRunService.AddParticipants(args.RunID, args.UserIDs, userID, args.ForceAddToChannel); err != nil { - return "", errors.Wrap(err, "failed to add participant from run") - } - - return "", nil -} - -func (r *RunRootResolver) RemoveRunParticipants(ctx context.Context, args struct { - RunID string - UserIDs []string -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - // When user is leaving run RunView permission is enough, otherwise user need manage permissions - if updatesOnlyRequesterMembership(userID, args.UserIDs) { - if err := c.permissions.RunView(userID, args.RunID); err != nil { - return "", errors.Wrap(err, "attempted to modify participants without permissions") - } - } else { - if err := c.permissions.RunManageProperties(userID, args.RunID); err != nil { - return "", errors.Wrap(err, "attempted to modify participants without permissions") - } - } - - if err := c.playbookRunService.RemoveParticipants(args.RunID, args.UserIDs, userID); err != nil { - return "", errors.Wrap(err, "failed to remove participant from run") - } - - for _, userID := range args.UserIDs { - if err := c.playbookRunService.Unfollow(args.RunID, userID); err != nil { - return "", errors.Wrap(err, "failed to make participant to unfollow run") - } - } - - return "", nil -} - -func updatesOnlyRequesterMembership(requesterUserID string, userIDs []string) bool { - return len(userIDs) == 1 && userIDs[0] == requesterUserID -} - -func (r *RunRootResolver) ChangeRunOwner(ctx context.Context, args struct { - RunID string - OwnerID string -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - requesterID := c.r.Header.Get("Mattermost-User-ID") - - if err := c.permissions.RunManageProperties(requesterID, args.RunID); err != nil { - return "", errors.Wrap(err, "attempted to modify the run owner without permissions") - } - - if err := c.playbookRunService.ChangeOwner(args.RunID, requesterID, args.OwnerID); err != nil { - return "", errors.Wrap(err, "failed to change the run owner") - } - - return "", nil -} - -func (r *RunRootResolver) UpdateRunTaskActions(ctx context.Context, args struct { - RunID string - ChecklistNum float64 - ItemNum float64 - TaskActions *[]app.TaskAction -}) (string, error) { - c, err := getContext(ctx) - if err != nil { - return "", err - } - if args.TaskActions == nil { - return "", err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - if err := validateTaskActions(*args.TaskActions); err != nil { - return "", err - } - - if err := c.playbookRunService.SetTaskActionsToChecklistItem(args.RunID, userID, int(args.ChecklistNum), int(args.ItemNum), *args.TaskActions); err != nil { - return "", err - } - - return "", nil -} diff --git a/server/playbooks/server/api/graphql_run.go b/server/playbooks/server/api/graphql_run.go deleted file mode 100644 index 80cb46d0d09..00000000000 --- a/server/playbooks/server/api/graphql_run.go +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - "strconv" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -type RunResolver struct { - app.PlaybookRun -} - -// NumTasks is a computed attribute (not stored in database) which -// returns the number of total tasks in a playbook run: -func (r *RunResolver) NumTasks() int32 { - total := 0 - for _, checklist := range r.PlaybookRun.Checklists { - total += len(checklist.Items) - } - return int32(total) -} - -// NumTasksClosed is a computed attribute (not stored in database) which -// returns the number of tasks closed in a playbook run: -func (r *RunResolver) NumTasksClosed() int32 { - closed := 0 - for _, checklist := range r.PlaybookRun.Checklists { - for _, item := range checklist.Items { - if item.State == app.ChecklistItemStateClosed || item.State == app.ChecklistItemStateSkipped { - closed++ - } - } - } - return int32(closed) -} - -func (r *RunResolver) Type() string { - return r.PlaybookRun.Type -} - -func (r *RunResolver) CreateAt() float64 { - return float64(r.PlaybookRun.CreateAt) -} - -func (r *RunResolver) EndAt() float64 { - return float64(r.PlaybookRun.EndAt) -} - -func (r *RunResolver) SummaryModifiedAt() float64 { - return float64(r.PlaybookRun.SummaryModifiedAt) -} -func (r *RunResolver) LastStatusUpdateAt() float64 { - return float64(r.PlaybookRun.LastStatusUpdateAt) -} - -func (r *RunResolver) RetrospectivePublishedAt() float64 { - return float64(r.PlaybookRun.RetrospectivePublishedAt) -} - -func (r *RunResolver) ReminderTimerDefaultSeconds() float64 { - return float64(r.PlaybookRun.ReminderTimerDefaultSeconds) -} - -func (r *RunResolver) PreviousReminder() float64 { - return float64(r.PlaybookRun.PreviousReminder) -} - -func (r *RunResolver) RetrospectiveReminderIntervalSeconds() float64 { - return float64(r.PlaybookRun.RetrospectiveReminderIntervalSeconds) -} - -func (r *RunResolver) Checklists() []*ChecklistResolver { - checklistResolvers := make([]*ChecklistResolver, 0, len(r.PlaybookRun.Checklists)) - for _, checklist := range r.PlaybookRun.Checklists { - checklistResolvers = append(checklistResolvers, &ChecklistResolver{checklist}) - } - - return checklistResolvers -} - -func (r *RunResolver) StatusPosts() []*StatusPostResolver { - statusPostResolvers := make([]*StatusPostResolver, 0, len(r.PlaybookRun.StatusPosts)) - for _, statusPost := range r.PlaybookRun.StatusPosts { - statusPostResolvers = append(statusPostResolvers, &StatusPostResolver{statusPost}) - } - - return statusPostResolvers -} - -func (r *RunResolver) TimelineEvents() []*TimelineEventResolver { - timelineEventResolvers := make([]*TimelineEventResolver, 0, len(r.PlaybookRun.StatusPosts)) - for _, event := range r.PlaybookRun.TimelineEvents { - timelineEventResolvers = append(timelineEventResolvers, &TimelineEventResolver{event}) - } - - return timelineEventResolvers -} - -func (r *RunResolver) IsFavorite(ctx context.Context) (bool, error) { - c, err := getContext(ctx) - if err != nil { - return false, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - thunk := c.favoritesLoader.Load(ctx, favoriteInfo{ - TeamID: r.TeamID, - UserID: userID, - Type: app.RunItemType, - ID: r.ID, - }) - - result, err := thunk() - if err != nil { - return false, err - } - - return result, nil -} - -type StatusPostResolver struct { - app.StatusPost -} - -func (r *StatusPostResolver) CreateAt() float64 { - return float64(r.StatusPost.CreateAt) -} - -func (r *StatusPostResolver) DeleteAt() float64 { - return float64(r.StatusPost.DeleteAt) -} - -type TimelineEventResolver struct { - app.TimelineEvent -} - -func (r *TimelineEventResolver) CreateAt() float64 { - return float64(r.TimelineEvent.CreateAt) -} - -func (r *TimelineEventResolver) EventType() string { - return string(r.TimelineEvent.EventType) -} - -func (r *TimelineEventResolver) DeleteAt() float64 { - return float64(r.TimelineEvent.DeleteAt) -} - -func (r *RunResolver) Followers(ctx context.Context) ([]string, error) { - c, err := getContext(ctx) - if err != nil { - return nil, err - } - - metadata, err := c.playbookRunService.GetPlaybookRunMetadata(r.ID) - if err != nil { - return nil, errors.Wrap(err, "can't get metadata") - } - - return metadata.Followers, nil -} - -func (r *RunResolver) Playbook(ctx context.Context) (*PlaybookResolver, error) { - c, err := getContext(ctx) - if err != nil { - return nil, err - } - userID := c.r.Header.Get("Mattermost-User-ID") - - thunk := c.playbooksLoader.Load(ctx, playbookInfo{ - UserID: userID, - ID: r.PlaybookID, - TeamID: r.TeamID, - }) - - result, err := thunk() - if err != nil { - return nil, err - } - - if result == nil { - return nil, nil - } - - return &PlaybookResolver{*result}, nil -} - -func (r *RunResolver) LastUpdatedAt(ctx context.Context) float64 { - if len(r.PlaybookRun.TimelineEvents) < 1 { - return float64(r.PlaybookRun.CreateAt) - } - return float64(r.PlaybookRun.TimelineEvents[len(r.PlaybookRun.TimelineEvents)-1].EventAt) -} - -type RunConnectionResolver struct { - results app.GetPlaybookRunsResults - page int -} - -func (r *RunConnectionResolver) TotalCount() int32 { - return int32(r.results.TotalCount) -} - -func (r *RunConnectionResolver) Edges() []*RunEdgeResolver { - ret := make([]*RunEdgeResolver, 0, len(r.results.Items)) - // Cursor is just the end cursor for the page for now - cursor := r.results.PageCount - for _, run := range r.results.Items { - ret = append(ret, &RunEdgeResolver{run, cursor}) - } - - return ret -} - -func (r *RunConnectionResolver) PageInfo() *PageInfoResolver { - startCursor := "" - endCursor := "" - - if len(r.results.Items) > 0 { - // "Cursors" are just the page numbers - startCursor = encodeRunConnectionCursor(r.page) - endCursor = encodeRunConnectionCursor(r.page + 1) - } - - return &PageInfoResolver{ - HasNextPage: r.results.HasMore, - StartCursor: startCursor, - EndCursor: endCursor, - } -} - -func encodeRunConnectionCursor(cursor int) string { - return strconv.Itoa(cursor) -} - -func decodeRunConnectionCursor(cursor string) (int, error) { - num, err := strconv.Atoi(cursor) - if err != nil { - return 0, errors.Wrap(err, "unable to decode cursor") - } - return num, nil -} - -type RunEdgeResolver struct { - run app.PlaybookRun - cursor int -} - -func (r *RunEdgeResolver) Node() *RunResolver { - return &RunResolver{r.run} -} - -func (r *RunEdgeResolver) Cursor() string { - return encodeRunConnectionCursor(r.cursor) -} - -type PageInfoResolver struct { - HasNextPage bool - StartCursor string - EndCursor string -} diff --git a/server/playbooks/server/api/graphqli.html b/server/playbooks/server/api/graphqli.html deleted file mode 100644 index 6671a98e922..00000000000 --- a/server/playbooks/server/api/graphqli.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - GraphiQL editor | Mattermost - - - - - - - - -
Loading...
- - - diff --git a/server/playbooks/server/api/logger.go b/server/playbooks/server/api/logger.go deleted file mode 100644 index 9f5cec008ff..00000000000 --- a/server/playbooks/server/api/logger.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "context" - "net/http" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/sirupsen/logrus" -) - -// statusRecorder intercepts and saves the status code written to an http.ResponseWriter. -type statusRecorder struct { - http.ResponseWriter - statusCode int -} - -func (r *statusRecorder) WriteHeader(code int) { - // Forward the write - r.ResponseWriter.WriteHeader(code) - - // Save the status code - r.statusCode = code -} - -// LogRequest logs each request, attaching a unique request_id to the request context to trace -// logs throughout the request lifecycle. -func LogRequest(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - recorder := statusRecorder{w, 200} - requestID := model.NewId() - - startMilis := time.Now().UnixNano() / int64(time.Millisecond) - - logger := logrus.WithFields(logrus.Fields{ - "method": r.Method, - "url": r.URL.String(), - "user_id": r.Header.Get("Mattermost-User-Id"), - "request_id": requestID, - "user_agent": r.Header.Get("User-Agent"), - }) - r = r.WithContext(context.WithValue(r.Context(), requestIDContextKey, requestID)) - - logger.Debug("Received HTTP request") - - next.ServeHTTP(&recorder, r) - - gqlOp := r.Header.Get("X-GQL-Operation") - if gqlOp != "" { - logger = logger.WithField("gql_operation", gqlOp) - } - - endMilis := time.Now().UnixNano() / int64(time.Millisecond) - logger.WithFields(logrus.Fields{ - "time": endMilis - startMilis, - "status": recorder.statusCode, - }).Debug("Handled HTTP request") - }) -} diff --git a/server/playbooks/server/api/playbook_runs.go b/server/playbooks/server/api/playbook_runs.go deleted file mode 100644 index 05319027f01..00000000000 --- a/server/playbooks/server/api/playbook_runs.go +++ /dev/null @@ -1,1888 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -// PlaybookRunHandler is the API handler. -type PlaybookRunHandler struct { - *ErrorHandler - config config.Service - playbookRunService app.PlaybookRunService - playbookService app.PlaybookService - permissions *app.PermissionsService - licenseChecker app.LicenseChecker - api playbooks.ServicesAPI - poster bot.Poster -} - -// NewPlaybookRunHandler Creates a new Plugin API handler. -func NewPlaybookRunHandler( - router *mux.Router, - playbookRunService app.PlaybookRunService, - playbookService app.PlaybookService, - permissions *app.PermissionsService, - licenseChecker app.LicenseChecker, - api playbooks.ServicesAPI, - poster bot.Poster, - configService config.Service, -) *PlaybookRunHandler { - handler := &PlaybookRunHandler{ - ErrorHandler: &ErrorHandler{}, - playbookRunService: playbookRunService, - playbookService: playbookService, - api: api, - poster: poster, - config: configService, - permissions: permissions, - licenseChecker: licenseChecker, - } - - playbookRunsRouter := router.PathPrefix("/runs").Subrouter() - playbookRunsRouter.HandleFunc("", withContext(handler.getPlaybookRuns)).Methods(http.MethodGet) - playbookRunsRouter.HandleFunc("", withContext(handler.createPlaybookRunFromPost)).Methods(http.MethodPost) - - playbookRunsRouter.HandleFunc("/dialog", withContext(handler.createPlaybookRunFromDialog)).Methods(http.MethodPost) - playbookRunsRouter.HandleFunc("/add-to-timeline-dialog", withContext(handler.addToTimelineDialog)).Methods(http.MethodPost) - playbookRunsRouter.HandleFunc("/owners", withContext(handler.getOwners)).Methods(http.MethodGet) - playbookRunsRouter.HandleFunc("/channels", withContext(handler.getChannels)).Methods(http.MethodGet) - playbookRunsRouter.HandleFunc("/checklist-autocomplete", withContext(handler.getChecklistAutocomplete)).Methods(http.MethodGet) - playbookRunsRouter.HandleFunc("/checklist-autocomplete-item", withContext(handler.getChecklistAutocompleteItem)).Methods(http.MethodGet) - playbookRunsRouter.HandleFunc("/runs-autocomplete", withContext(handler.getChannelRunsAutocomplete)).Methods(http.MethodGet) - - playbookRunRouter := playbookRunsRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter() - playbookRunRouter.HandleFunc("", withContext(handler.getPlaybookRun)).Methods(http.MethodGet) - playbookRunRouter.HandleFunc("/metadata", withContext(handler.getPlaybookRunMetadata)).Methods(http.MethodGet) - playbookRunRouter.HandleFunc("/status-updates", withContext(handler.getStatusUpdates)).Methods(http.MethodGet) - playbookRunRouter.HandleFunc("/request-update", withContext(handler.requestUpdate)).Methods(http.MethodPost) - playbookRunRouter.HandleFunc("/request-join-channel", withContext(handler.requestJoinChannel)).Methods(http.MethodPost) - - playbookRunRouterAuthorized := playbookRunRouter.PathPrefix("").Subrouter() - playbookRunRouterAuthorized.Use(handler.checkEditPermissions) - playbookRunRouterAuthorized.HandleFunc("", withContext(handler.updatePlaybookRun)).Methods(http.MethodPatch) - playbookRunRouterAuthorized.HandleFunc("/owner", withContext(handler.changeOwner)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/status", withContext(handler.status)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/finish", withContext(handler.finish)).Methods(http.MethodPut) - playbookRunRouterAuthorized.HandleFunc("/finish-dialog", withContext(handler.finishDialog)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/update-status-dialog", withContext(handler.updateStatusDialog)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/reminder/button-update", withContext(handler.reminderButtonUpdate)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/reminder", withContext(handler.reminderReset)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/no-retrospective-button", withContext(handler.noRetrospectiveButton)).Methods(http.MethodPost) - playbookRunRouterAuthorized.HandleFunc("/timeline/{eventID:[A-Za-z0-9]+}", withContext(handler.removeTimelineEvent)).Methods(http.MethodDelete) - playbookRunRouterAuthorized.HandleFunc("/restore", withContext(handler.restore)).Methods(http.MethodPut) - playbookRunRouterAuthorized.HandleFunc("/status-update-enabled", withContext(handler.toggleStatusUpdates)).Methods(http.MethodPut) - - channelRouter := playbookRunsRouter.PathPrefix("/channel/{channel_id:[A-Za-z0-9]+}").Subrouter() - channelRouter.HandleFunc("", withContext(handler.getPlaybookRunByChannel)).Methods(http.MethodGet) - channelRouter.HandleFunc("/runs", withContext(handler.getPlaybookRunsForChannelByUser)).Methods(http.MethodGet) - - checklistsRouter := playbookRunRouterAuthorized.PathPrefix("/checklists").Subrouter() - checklistsRouter.HandleFunc("", withContext(handler.addChecklist)).Methods(http.MethodPost) - checklistsRouter.HandleFunc("/move", withContext(handler.moveChecklist)).Methods(http.MethodPost) - checklistsRouter.HandleFunc("/move-item", withContext(handler.moveChecklistItem)).Methods(http.MethodPost) - - checklistRouter := checklistsRouter.PathPrefix("/{checklist:[0-9]+}").Subrouter() - checklistRouter.HandleFunc("", withContext(handler.removeChecklist)).Methods(http.MethodDelete) - checklistRouter.HandleFunc("/add", withContext(handler.addChecklistItem)).Methods(http.MethodPost) - checklistRouter.HandleFunc("/rename", withContext(handler.renameChecklist)).Methods(http.MethodPut) - checklistRouter.HandleFunc("/add-dialog", withContext(handler.addChecklistItemDialog)).Methods(http.MethodPost) - checklistRouter.HandleFunc("/skip", withContext(handler.checklistSkip)).Methods(http.MethodPut) - checklistRouter.HandleFunc("/restore", withContext(handler.checklistRestore)).Methods(http.MethodPut) - checklistRouter.HandleFunc("/duplicate", withContext(handler.duplicateChecklist)).Methods(http.MethodPost) - - checklistItem := checklistRouter.PathPrefix("/item/{item:[0-9]+}").Subrouter() - checklistItem.HandleFunc("", withContext(handler.itemDelete)).Methods(http.MethodDelete) - checklistItem.HandleFunc("", withContext(handler.itemEdit)).Methods(http.MethodPut) - checklistItem.HandleFunc("/skip", withContext(handler.itemSkip)).Methods(http.MethodPut) - checklistItem.HandleFunc("/restore", withContext(handler.itemRestore)).Methods(http.MethodPut) - checklistItem.HandleFunc("/state", withContext(handler.itemSetState)).Methods(http.MethodPut) - checklistItem.HandleFunc("/assignee", withContext(handler.itemSetAssignee)).Methods(http.MethodPut) - checklistItem.HandleFunc("/command", withContext(handler.itemSetCommand)).Methods(http.MethodPut) - checklistItem.HandleFunc("/run", withContext(handler.itemRun)).Methods(http.MethodPost) - checklistItem.HandleFunc("/duplicate", withContext(handler.itemDuplicate)).Methods(http.MethodPost) - checklistItem.HandleFunc("/duedate", withContext(handler.itemSetDueDate)).Methods(http.MethodPut) - - retrospectiveRouter := playbookRunRouterAuthorized.PathPrefix("/retrospective").Subrouter() - retrospectiveRouter.HandleFunc("", withContext(handler.updateRetrospective)).Methods(http.MethodPost) - retrospectiveRouter.HandleFunc("/publish", withContext(handler.publishRetrospective)).Methods(http.MethodPost) - - followersRouter := playbookRunRouter.PathPrefix("/followers").Subrouter() - followersRouter.HandleFunc("", withContext(handler.follow)).Methods(http.MethodPut) - followersRouter.HandleFunc("", withContext(handler.unfollow)).Methods(http.MethodDelete) - followersRouter.HandleFunc("", withContext(handler.getFollowers)).Methods(http.MethodGet) - - return handler -} - -func (h *PlaybookRunHandler) checkEditPermissions(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger := getLogger(r) - vars := mux.Vars(r) - userID := r.Header.Get("Mattermost-User-ID") - - playbookRun, err := h.playbookRunService.GetPlaybookRun(vars["id"]) - if err != nil { - h.HandleError(w, logger, err) - return - } - - if !h.PermissionsCheck(w, logger, h.permissions.RunManageProperties(userID, playbookRun.ID)) { - return - } - - next.ServeHTTP(w, r) - }) -} - -// createPlaybookRunFromPost handles the POST /runs endpoint -func (h *PlaybookRunHandler) createPlaybookRunFromPost(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - var playbookRunCreateOptions client.PlaybookRunCreateOptions - if err := json.NewDecoder(r.Body).Decode(&playbookRunCreateOptions); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook run create options", err) - return - } - - playbookRun, err := h.createPlaybookRun( - app.PlaybookRun{ - OwnerUserID: playbookRunCreateOptions.OwnerUserID, - TeamID: playbookRunCreateOptions.TeamID, - ChannelID: playbookRunCreateOptions.ChannelID, - Name: playbookRunCreateOptions.Name, - Summary: playbookRunCreateOptions.Description, - PostID: playbookRunCreateOptions.PostID, - PlaybookID: playbookRunCreateOptions.PlaybookID, - Type: playbookRunCreateOptions.Type, - }, - userID, - playbookRunCreateOptions.CreatePublicRun, - app.RunSourcePost, - ) - if errors.Is(err, app.ErrNoPermissions) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "unable to create playbook run", err) - return - } - - if errors.Is(err, app.ErrMalformedPlaybookRun) { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to create playbook run", err) - return - } - - if err != nil { - h.HandleError(w, c.logger, errors.Wrapf(err, "unable to create playbook run")) - return - } - - h.poster.PublishWebsocketEventToUser(app.PlaybookRunCreatedWSEvent, map[string]interface{}{ - "playbook_run": playbookRun, - }, userID) - - w.Header().Add("Location", fmt.Sprintf("/api/v0/runs/%s", playbookRun.ID)) - ReturnJSON(w, &playbookRun, http.StatusCreated) -} - -// Note that this currently does nothing. This is temporary given the removal of stages. Will be used by status. -func (h *PlaybookRunHandler) updatePlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - - oldPlaybookRun, err := h.playbookRunService.GetPlaybookRun(playbookRunID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - var updates app.UpdateOptions - if err = json.NewDecoder(r.Body).Decode(&updates); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode payload", err) - return - } - - updatedPlaybookRun := oldPlaybookRun - - ReturnJSON(w, updatedPlaybookRun, http.StatusOK) -} - -// createPlaybookRunFromDialog handles the interactive dialog submission when a user presses confirm on -// the create playbook run dialog. -func (h *PlaybookRunHandler) createPlaybookRunFromDialog(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - var request *model.SubmitDialogRequest - err := json.NewDecoder(r.Body).Decode(&request) - if err != nil || request == nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err) - return - } - - if userID != request.UserId { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "interactive dialog's userID must be the same as the requester's userID", nil) - return - } - - var state app.DialogState - err = json.Unmarshal([]byte(request.State), &state) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal dialog state", err) - return - } - - var playbookID, name string - if rawPlaybookID, ok := request.Submission[app.DialogFieldPlaybookIDKey].(string); ok { - playbookID = rawPlaybookID - } - if rawName, ok := request.Submission[app.DialogFieldNameKey].(string); ok { - name = rawName - } - - playbook, err := h.playbookService.Get(playbookID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to get playbook", err) - return - } - - playbookRun, err := h.createPlaybookRun( - app.PlaybookRun{ - OwnerUserID: request.UserId, - TeamID: request.TeamId, - ChannelID: playbook.GetRunChannelID(), - Name: name, - PostID: state.PostID, - PlaybookID: playbookID, - Type: app.RunTypePlaybook, - }, - request.UserId, - nil, - app.RunSourceDialog, - ) - if err != nil { - if errors.Is(err, app.ErrMalformedPlaybookRun) { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to create playbook run", err) - return - } - - if errors.Is(err, app.ErrNoPermissions) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to make runs from this playbook", err) - return - } - - var msg string - - if errors.Is(err, app.ErrChannelDisplayNameInvalid) { - msg = "The name is invalid or too long. Please use a valid name with fewer than 64 characters." - } - - if msg != "" { - resp := &model.SubmitDialogResponse{ - Errors: map[string]string{ - app.DialogFieldNameKey: msg, - }, - } - respBytes, _ := json.Marshal(resp) - _, _ = w.Write(respBytes) - return - } - - h.HandleError(w, c.logger, err) - return - } - - channel, err := h.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to get new channel", err) - return - } - - // Delay sending the websocket message because the front end may try to change to the newly created - // channel, and the server may respond with a "channel not found" error. This happens in e2e tests, - // and possibly in the wild. - go func() { - time.Sleep(1 * time.Second) // arbitrary 1 second magic number - - h.poster.PublishWebsocketEventToUser(app.PlaybookRunCreatedWSEvent, map[string]interface{}{ - "client_id": state.ClientID, - "playbook_run": playbookRun, - "channel_name": channel.Name, - }, request.UserId) - }() - - if err := h.postPlaybookRunCreatedMessage(playbookRun, request.ChannelId); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.Header().Add("Location", fmt.Sprintf("/api/v0/runs/%s", playbookRun.ID)) - w.WriteHeader(http.StatusCreated) -} - -// addToTimelineDialog handles the interactive dialog submission when a user clicks the -// corresponding post action. -func (h *PlaybookRunHandler) addToTimelineDialog(c *Context, w http.ResponseWriter, r *http.Request) { - if !h.licenseChecker.TimelineAllowed() { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "timeline feature is not covered by current server license", nil) - return - } - - userID := r.Header.Get("Mattermost-User-ID") - - var request *model.SubmitDialogRequest - err := json.NewDecoder(r.Body).Decode(&request) - if err != nil || request == nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err) - return - } - - if userID != request.UserId { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "interactive dialog's userID must be the same as the requester's userID", nil) - return - } - - var playbookRunID, summary string - if rawPlaybookRunID, ok := request.Submission[app.DialogFieldPlaybookRunKey].(string); ok { - playbookRunID = rawPlaybookRunID - } - if rawSummary, ok := request.Submission[app.DialogFieldSummary].(string); ok { - summary = rawSummary - } - - playbookRun, incErr := h.playbookRunService.GetPlaybookRun(playbookRunID) - if incErr != nil { - h.HandleError(w, c.logger, incErr) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRun.ID)) { - return - } - - var state app.DialogStateAddToTimeline - err = json.Unmarshal([]byte(request.State), &state) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal dialog state", err) - return - } - - if err = h.playbookRunService.AddPostToTimeline(playbookRunID, userID, state.PostID, summary); err != nil { - h.HandleError(w, c.logger, errors.Wrap(err, "failed to add post to timeline")) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) createPlaybookRun(playbookRun app.PlaybookRun, userID string, createPublicRun *bool, source string) (*app.PlaybookRun, error) { - // Validate initial data - if playbookRun.ID != "" { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "playbook run already has an id") - } - - if playbookRun.CreateAt != 0 { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "playbook run channel already has created at date") - } - - if playbookRun.TeamID == "" && playbookRun.ChannelID == "" { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "must provide team or channel to create playbook run") - } - - if playbookRun.OwnerUserID == "" { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "missing owner user id of playbook run") - } - - if strings.TrimSpace(playbookRun.Name) == "" && playbookRun.ChannelID == "" { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "missing name of playbook run") - } - - // Retrieve channel if needed and validate it - // If a channel is specified, ensure it's from the given team (if one provided), or - // just grab the team for that channel. - var channel *model.Channel - var err error - if playbookRun.ChannelID != "" { - channel, err = h.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get channel") - } - - if playbookRun.TeamID == "" { - playbookRun.TeamID = channel.TeamId - } else if channel.TeamId != playbookRun.TeamID { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "channel not in given team") - } - } - - // Copy data from playbook if needed - public := true - if createPublicRun != nil { - public = *createPublicRun - } - - var playbook *app.Playbook - if playbookRun.PlaybookID != "" { - var pb app.Playbook - pb, err = h.playbookService.Get(playbookRun.PlaybookID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get playbook") - } - playbook = &pb - - if playbook.DeleteAt != 0 { - return nil, errors.New("playbook is archived, cannot create a new run using an archived playbook") - } - - if err = h.permissions.RunCreate(userID, *playbook); err != nil { - return nil, err - } - - if source == "dialog" && playbook.ChannelMode == app.PlaybookRunLinkExistingChannel && playbookRun.ChannelID == "" { - return nil, errors.Wrap(app.ErrMalformedPlaybookRun, "playbook is configured to be linked to existing channel but no channel is configured. Run can not be created from dialog") - } - - if createPublicRun == nil { - public = pb.CreatePublicPlaybookRun - } - - playbookRun.SetChecklistFromPlaybook(*playbook) - playbookRun.SetConfigurationFromPlaybook(*playbook, source) - } - - // Check the permissions on the channel: the user must be able to create it or, - // if one's already provided, they need to be able to manage it. - if channel == nil { - permission := model.PermissionCreatePrivateChannel - permissionMessage := "You are not able to create a private channel" - if public { - permission = model.PermissionCreatePublicChannel - permissionMessage = "You are not able to create a public channel" - } - if !h.api.HasPermissionToTeam(userID, playbookRun.TeamID, permission) { - return nil, errors.Wrap(app.ErrNoPermissions, permissionMessage) - } - } else { - permission := model.PermissionManagePublicChannelProperties - permissionMessage := "You are not able to manage public channel properties" - if channel.Type == model.ChannelTypePrivate { - permission = model.PermissionManagePrivateChannelProperties - permissionMessage = "You are not able to manage private channel properties" - } else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup { - permission = model.PermissionReadChannel - permissionMessage = "You do not have access to this channel" - } - - if !h.api.HasPermissionToChannel(userID, channel.Id, permission) { - return nil, errors.Wrap(app.ErrNoPermissions, permissionMessage) - } - } - - // Check the permissions on the provided post: the user must have access to the post's channel - if playbookRun.PostID != "" { - var post *model.Post - post, err = h.api.GetPost(playbookRun.PostID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get playbook run original post") - } - if !h.api.HasPermissionToChannel(userID, post.ChannelId, model.PermissionReadChannel) { - return nil, errors.New("user does not have access to the channel containing the playbook run's original post") - } - } - - playbookRunReturned, err := h.playbookRunService.CreatePlaybookRun(&playbookRun, playbook, userID, public) - if err != nil { - return nil, err - } - - // force database retrieval to ensure all data is processed correctly (i.e participantIds) - return h.playbookRunService.GetPlaybookRun(playbookRunReturned.ID) - -} - -func (h *PlaybookRunHandler) getRequesterInfo(userID string) (app.RequesterInfo, error) { - return app.GetRequesterInfo(userID, h.api) -} - -// getPlaybookRuns handles the GET /runs endpoint. -func (h *PlaybookRunHandler) getPlaybookRuns(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - filterOptions, err := parsePlaybookRunsFilterOptions(r.URL, userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad parameter", err) - return - } - - requesterInfo, err := h.getRequesterInfo(userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - results, err := h.playbookRunService.GetPlaybookRuns(requesterInfo, *filterOptions) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, results, http.StatusOK) -} - -// getPlaybookRun handles the /runs/{id} endpoint. -func (h *PlaybookRunHandler) getPlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) { - return - } - - playbookRunToGet, err := h.playbookRunService.GetPlaybookRun(playbookRunID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, playbookRunToGet, http.StatusOK) -} - -// getPlaybookRunMetadata handles the /runs/{id}/metadata endpoint. -func (h *PlaybookRunHandler) getPlaybookRunMetadata(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) { - return - } - - playbookRunMetadata, err := h.playbookRunService.GetPlaybookRunMetadata(playbookRunID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, playbookRunMetadata, http.StatusOK) -} - -// getPlaybookRunByChannel handles the /runs/channel/{channel_id} endpoint. -// Notice that it returns both playbook runs as well as channel checklists -func (h *PlaybookRunHandler) getPlaybookRunByChannel(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - channelID := vars["channel_id"] - userID := r.Header.Get("Mattermost-User-ID") - - // get playbook runs for the specific channel and user - playbookRunsResult, err := h.playbookRunService.GetPlaybookRuns( - app.RequesterInfo{ - UserID: userID, - }, - - app.PlaybookRunFilterOptions{ - ChannelID: channelID, - Page: 0, - PerPage: 2, - }, - ) - - if err != nil { - h.HandleError(w, c.logger, err) - return - } - playbookRuns := playbookRunsResult.Items - if len(playbookRuns) == 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found", - errors.Errorf("playbook run for channel id %s not found", channelID)) - return - } - - if len(playbookRuns) > 1 { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "multiple runs in the channel", nil) - return - } - - playbookRun := playbookRuns[0] - ReturnJSON(w, &playbookRun, http.StatusOK) -} - -// getOwners handles the /runs/owners api endpoint. -func (h *PlaybookRunHandler) getOwners(c *Context, w http.ResponseWriter, r *http.Request) { - teamID := r.URL.Query().Get("team_id") - - userID := r.Header.Get("Mattermost-User-ID") - options := app.PlaybookRunFilterOptions{ - TeamID: teamID, - } - - requesterInfo, err := h.getRequesterInfo(userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - owners, err := h.playbookRunService.GetOwners(requesterInfo, options) - if err != nil { - h.HandleError(w, c.logger, errors.Wrapf(err, "failed to get owners")) - return - } - - if owners == nil { - owners = []app.OwnerInfo{} - } - - ReturnJSON(w, owners, http.StatusOK) -} - -func (h *PlaybookRunHandler) getChannels(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - filterOptions, err := parsePlaybookRunsFilterOptions(r.URL, userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad parameter", err) - return - } - - requesterInfo, err := h.getRequesterInfo(userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - playbookRuns, err := h.playbookRunService.GetPlaybookRuns(requesterInfo, *filterOptions) - if err != nil { - h.HandleError(w, c.logger, errors.Wrapf(err, "failed to get playbookRuns")) - return - } - - channelIds := make([]string, 0, len(playbookRuns.Items)) - for _, playbookRun := range playbookRuns.Items { - channelIds = append(channelIds, playbookRun.ChannelID) - } - - ReturnJSON(w, channelIds, http.StatusOK) -} - -// changeOwner handles the /runs/{id}/change-owner api endpoint. -func (h *PlaybookRunHandler) changeOwner(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - OwnerID string `json:"owner_id"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "could not decode request body", err) - return - } - - if err := h.playbookRunService.ChangeOwner(vars["id"], userID, params.OwnerID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{}, http.StatusOK) -} - -// updateStatusD handles the POST /runs/{id}/status endpoint, user has edit permissions -func (h *PlaybookRunHandler) status(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var options app.StatusUpdateOptions - if err := json.NewDecoder(r.Body).Decode(&options); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode body into StatusUpdateOptions", err) - return - } - - if publicMsg, internalErr := h.updateStatus(playbookRunID, userID, options); internalErr != nil { - if errors.Is(internalErr, app.ErrNoPermissions) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, publicMsg, internalErr) - } else { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, publicMsg, internalErr) - } - return - } - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"status":"OK"}`)) -} - -// updateStatus returns a publicMessage and an internal error -func (h *PlaybookRunHandler) updateStatus(playbookRunID, userID string, options app.StatusUpdateOptions) (string, error) { - - // user must be a participant to be able to post an update - if err := h.permissions.RunManageProperties(userID, playbookRunID); err != nil { - return "Not authorized", err - } - - options.Message = strings.TrimSpace(options.Message) - if options.Message == "" { - return "message must not be empty", errors.New("message field empty") - } - - if options.Reminder <= 0 && !options.FinishRun { - return "the reminder must be set and not 0", errors.New("reminder was 0") - } - if options.Reminder < 0 || options.FinishRun { - options.Reminder = 0 - } - options.Reminder = options.Reminder * time.Second - - if err := h.playbookRunService.UpdateStatus(playbookRunID, userID, options); err != nil { - return "An internal error has occurred. Check app server logs for details.", err - } - - if options.FinishRun { - if err := h.playbookRunService.FinishPlaybookRun(playbookRunID, userID); err != nil { - return "An internal error has occurred. Check app server logs for details.", err - } - } - - return "", nil -} - -// updateStatusD handles the POST /runs/{id}/finish endpoint, user has edit permissions -func (h *PlaybookRunHandler) finish(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.FinishPlaybookRun(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"status":"OK"}`)) -} - -// getStatusUpdates handles the GET /runs/{id}/status endpoint -// -// Our goal is to deliver status updates to any user (when playbook is public) or -// any playbook member (when playbook is private). To do that we need to bypass the -// permissions system and avoid checking channel membership. -// -// This approach will be deprecated as a step towards channel-playbook decoupling. -func (h *PlaybookRunHandler) getStatusUpdates(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to get status updates", nil) - return - } - - playbookRun, err := h.playbookRunService.GetPlaybookRun(playbookRunID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - posts := make([]*app.StatusPostComplete, 0) - for _, p := range playbookRun.StatusPosts { - post, err := h.api.GetPost(p.ID) - if err != nil { - c.logger.WithError(err).WithField("post_id", p.ID).Error("statusUpdates: can not retrieve post") - continue - } - - // Given the fact that we are bypassing some permissions, - // an additional check is added to limit the risk - if post.Type == "custom_run_update" { - posts = append(posts, app.NewStatusPostComplete(post)) - } - } - - // sort by creation date, so that the first element is the newest post - sort.Slice(posts, func(i, j int) bool { - return posts[i].CreateAt > posts[j].CreateAt - }) - - ReturnJSON(w, posts, http.StatusOK) -} - -// restore "un-finishes" a playbook run -func (h *PlaybookRunHandler) restore(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.RestorePlaybookRun(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"status":"OK"}`)) -} - -// requestUpdate posts a status update request message in the run's channel -func (h *PlaybookRunHandler) requestUpdate(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to post update request", nil) - return - } - - if err := h.playbookRunService.RequestUpdate(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } -} - -// requestJoinChannel posts a channel-join request message in the run's channel -func (h *PlaybookRunHandler) requestJoinChannel(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - // user must be a participant to be able to request to join the channel - if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRunID)) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "not authorized to request join channel", nil) - return - } - - if err := h.playbookRunService.RequestJoinChannel(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } -} - -// updateStatusDialog handles the POST /runs/{id}/finish-dialog endpoint, called when a -// user submits the Finish Run dialog. -func (h *PlaybookRunHandler) finishDialog(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbookRun, incErr := h.playbookRunService.GetPlaybookRun(playbookRunID) - if incErr != nil { - h.HandleError(w, c.logger, incErr) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRun.ID)) { - return - } - - if err := h.playbookRunService.FinishPlaybookRun(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } -} - -func (h *PlaybookRunHandler) toggleStatusUpdates(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var payload struct { - StatusEnabled bool `json:"status_enabled"` - } - - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - h.HandleError(w, c.logger, err) - return - } - - if err := h.playbookRunService.ToggleStatusUpdates(playbookRunID, userID, payload.StatusEnabled); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{"success": true}, http.StatusOK) - -} - -// updateStatusDialog handles the POST /runs/{id}/update-status-dialog endpoint, called when a -// user submits the Update Status dialog. -func (h *PlaybookRunHandler) updateStatusDialog(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var request *model.SubmitDialogRequest - err := json.NewDecoder(r.Body).Decode(&request) - if err != nil || request == nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err) - return - } - - var options app.StatusUpdateOptions - if message, ok := request.Submission[app.DialogFieldMessageKey]; ok { - options.Message = message.(string) - } - - if reminderI, ok := request.Submission[app.DialogFieldReminderInSecondsKey]; ok { - var reminder int - reminder, err = strconv.Atoi(reminderI.(string)) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - options.Reminder = time.Duration(reminder) - } - - if finishB, ok := request.Submission[app.DialogFieldFinishRun]; ok { - var finish bool - if finish, ok = finishB.(bool); ok { - options.FinishRun = finish - } - } - - if publicMsg, internalErr := h.updateStatus(playbookRunID, userID, options); internalErr != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, publicMsg, internalErr) - return - } - - w.WriteHeader(http.StatusOK) -} - -// reminderButtonUpdate handles the POST /runs/{id}/reminder/button-update endpoint, called when a -// user clicks on the reminder interactive button -func (h *PlaybookRunHandler) reminderButtonUpdate(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - var requestData *model.PostActionIntegrationRequest - err := json.NewDecoder(r.Body).Decode(&requestData) - if err != nil || requestData == nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "missing request data", nil) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(requestData.UserId, playbookRunID)) { - return - } - - if err = h.playbookRunService.OpenUpdateStatusDialog(playbookRunID, requestData.UserId, requestData.TriggerId); err != nil { - h.HandleError(w, c.logger, errors.New("reminderButtonUpdate failed to open update status dialog")) - return - } - - ReturnJSON(w, nil, http.StatusOK) -} - -// reminderReset handles the POST /runs/{id}/reminder endpoint, called when a -// user clicks on the reminder custom_update_status time selector -func (h *PlaybookRunHandler) reminderReset(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - var payload struct { - NewReminderSeconds int `json:"new_reminder_seconds"` - } - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - h.HandleError(w, c.logger, err) - return - } - - if payload.NewReminderSeconds <= 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "new_reminder_seconds must be > 0", errors.New("new_reminder_seconds was <= 0")) - return - } - - storedPlaybookRun, err := h.playbookRunService.GetPlaybookRun(playbookRunID) - if err != nil { - err = errors.Wrapf(err, "reminderReset: no playbook run for path's playbookRunID: %s", playbookRunID) - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "no playbook run for path's playbookRunID", err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, storedPlaybookRun.ID)) { - return - } - - if err = h.playbookRunService.ResetReminder(playbookRunID, time.Duration(payload.NewReminderSeconds)*time.Second); err != nil { - err = errors.Wrapf(err, "reminderReset: error setting new reminder for playbookRunID %s", playbookRunID) - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error removing reminder post", err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) noRetrospectiveButton(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbookRunToCancelRetro, err := h.playbookRunService.GetPlaybookRun(playbookRunID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunManageProperties(userID, playbookRunToCancelRetro.ID)) { - return - } - - if err := h.playbookRunService.CancelRetrospective(playbookRunToCancelRetro.ID, userID); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to cancel retrospective", err) - return - } - - ReturnJSON(w, nil, http.StatusOK) -} - -// removeTimelineEvent handles the DELETE /runs/{id}/timeline/{eventID} endpoint. -// User has been authenticated to edit the playbook run. -func (h *PlaybookRunHandler) removeTimelineEvent(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - eventID := vars["eventID"] - - if err := h.playbookRunService.RemoveTimelineEvent(id, userID, eventID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) getChecklistAutocompleteItem(c *Context, w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - channelID := query.Get("channel_id") - userID := r.Header.Get("Mattermost-User-ID") - - playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, - fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err) - return - } - if len(playbookRuns) == 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found", - errors.Errorf("playbook run for channel id %s not found", channelID)) - return - } - - data, err := h.playbookRunService.GetChecklistItemAutocomplete(playbookRuns) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, data, http.StatusOK) -} - -func (h *PlaybookRunHandler) getChecklistAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - channelID := query.Get("channel_id") - userID := r.Header.Get("Mattermost-User-ID") - - playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, - fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err) - return - } - if len(playbookRuns) == 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found", - errors.Errorf("playbook run for channel id %s not found", channelID)) - return - } - - data, err := h.playbookRunService.GetChecklistAutocomplete(playbookRuns) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, data, http.StatusOK) -} - -func (h *PlaybookRunHandler) getChannelRunsAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - channelID := query.Get("channel_id") - userID := r.Header.Get("Mattermost-User-ID") - - playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, - fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err) - return - } - if len(playbookRuns) == 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusNotFound, "Not found", - errors.Errorf("playbook run for channel id %s not found", channelID)) - return - } - - data, err := h.playbookRunService.GetRunsAutocomplete(playbookRuns) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, data, http.StatusOK) -} - -func (h *PlaybookRunHandler) getPlaybookRunsForChannelByUser(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - channelID := vars["channel_id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbookRuns, err := h.playbookRunService.GetPlaybookRunsForChannelByUser(channelID, userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, - fmt.Sprintf("unable to retrieve runs for channel id %s", channelID), err) - return - } - - ReturnJSON(w, playbookRuns, http.StatusOK) -} - -func (h *PlaybookRunHandler) itemSetState(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - NewState string `json:"new_state"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err) - return - } - - if !app.IsValidChecklistItemState(params.NewState) { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter new state", nil) - return - } - - if err := h.playbookRunService.ModifyCheckedState(id, userID, params.NewState, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{}, http.StatusOK) -} - -func (h *PlaybookRunHandler) itemSetAssignee(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - AssigneeID string `json:"assignee_id"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err) - return - } - - if err := h.playbookRunService.SetAssignee(id, userID, params.AssigneeID, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{}, http.StatusOK) -} - -func (h *PlaybookRunHandler) itemSetDueDate(c *Context, w http.ResponseWriter, r *http.Request) { - if !h.licenseChecker.ChecklistItemDueDateAllowed() { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "checklist item due date feature is not covered by current server license", nil) - return - } - - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - DueDate int64 `json:"due_date"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err) - return - } - - if err := h.playbookRunService.SetDueDate(id, userID, params.DueDate, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{}, http.StatusOK) -} - -func (h *PlaybookRunHandler) itemSetCommand(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - Command string `json:"command"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal", err) - return - } - - if err := h.playbookRunService.SetCommandToChecklistItem(id, userID, checklistNum, itemNum, params.Command); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{}, http.StatusOK) -} - -func (h *PlaybookRunHandler) itemRun(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - triggerID, err := h.playbookRunService.RunChecklistItemSlashCommand(playbookRunID, userID, checklistNum, itemNum) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, map[string]interface{}{"trigger_id": triggerID}, http.StatusOK) -} - -func (h *PlaybookRunHandler) itemDuplicate(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.DuplicateChecklistItem(playbookRunID, userID, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusCreated) -} - -func (h *PlaybookRunHandler) addChecklist(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var checklist app.Checklist - if err := json.NewDecoder(r.Body).Decode(&checklist); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode Checklist", err) - return - } - - checklist.Title = strings.TrimSpace(checklist.Title) - if checklist.Title == "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist title", - errors.New("checklist title must not be blank")) - return - } - - if err := h.playbookRunService.AddChecklist(id, userID, checklist); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusCreated) -} - -func (h *PlaybookRunHandler) removeChecklist(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.RemoveChecklist(id, userID, checklistNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusCreated) -} - -func (h *PlaybookRunHandler) duplicateChecklist(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.DuplicateChecklist(playbookRunID, userID, checklistNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusCreated) -} - -func (h *PlaybookRunHandler) addChecklistItem(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var checklistItem app.ChecklistItem - if err := json.NewDecoder(r.Body).Decode(&checklistItem); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode ChecklistItem", err) - return - } - - checklistItem.Title = strings.TrimSpace(checklistItem.Title) - if checklistItem.Title == "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist item title", - errors.New("checklist item title must not be blank")) - return - } - - if err := h.playbookRunService.AddChecklistItem(id, userID, checklistNum, checklistItem); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusCreated) -} - -// addChecklistItemDialog handles the interactive dialog submission when a user clicks add new task -func (h *PlaybookRunHandler) addChecklistItemDialog(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - vars := mux.Vars(r) - playbookRunID := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - - var request *model.SubmitDialogRequest - err = json.NewDecoder(r.Body).Decode(&request) - if err != nil || request == nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to decode SubmitDialogRequest", err) - return - } - - if userID != request.UserId { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "interactive dialog's userID must be the same as the requester's userID", nil) - return - } - - var name, description string - if rawName, ok := request.Submission[app.DialogFieldItemNameKey].(string); ok { - name = rawName - } - if rawDescription, ok := request.Submission[app.DialogFieldItemDescriptionKey].(string); ok { - description = rawDescription - } - - checklistItem := app.ChecklistItem{ - Title: name, - Description: description, - } - - checklistItem.Title = strings.TrimSpace(checklistItem.Title) - if checklistItem.Title == "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist item title", - errors.New("checklist item title must not be blank")) - return - } - - if err := h.playbookRunService.AddChecklistItem(playbookRunID, userID, checklistNum, checklistItem); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) - -} - -func (h *PlaybookRunHandler) itemDelete(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.RemoveChecklistItem(id, userID, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) checklistSkip(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.SkipChecklist(id, userID, checklistNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) checklistRestore(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.RestoreChecklist(id, userID, checklistNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) itemSkip(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.SkipChecklistItem(id, userID, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) itemRestore(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.RestoreChecklistItem(id, userID, checklistNum, itemNum); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookRunHandler) itemEdit(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - itemNum, err := strconv.Atoi(vars["item"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse item", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - Title string `json:"title"` - Command string `json:"command"` - Description string `json:"description"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal edit params state", err) - return - } - - if err := h.playbookRunService.EditChecklistItem(id, userID, checklistNum, itemNum, params.Title, params.Command, params.Description); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) renameChecklist(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - checklistNum, err := strconv.Atoi(vars["checklist"]) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to parse checklist", err) - return - } - userID := r.Header.Get("Mattermost-User-ID") - - var modificationParams struct { - NewTitle string `json:"title"` - } - if err := json.NewDecoder(r.Body).Decode(&modificationParams); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal new title", err) - return - } - - if modificationParams.NewTitle == "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "bad parameter: checklist title", - errors.New("checklist title must not be blank")) - return - } - - if err := h.playbookRunService.RenameChecklist(id, userID, checklistNum, modificationParams.NewTitle); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) moveChecklist(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - SourceChecklistIdx int `json:"source_checklist_idx"` - DestChecklistIdx int `json:"dest_checklist_idx"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal edit params", err) - return - } - - if err := h.playbookRunService.MoveChecklist(id, userID, params.SourceChecklistIdx, params.DestChecklistIdx); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) moveChecklistItem(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - SourceChecklistIdx int `json:"source_checklist_idx"` - SourceItemIdx int `json:"source_item_idx"` - DestChecklistIdx int `json:"dest_checklist_idx"` - DestItemIdx int `json:"dest_item_idx"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "failed to unmarshal edit params", err) - return - } - - if err := h.playbookRunService.MoveChecklistItem(id, userID, params.SourceChecklistIdx, params.SourceItemIdx, params.DestChecklistIdx, params.DestItemIdx); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) postPlaybookRunCreatedMessage(playbookRun *app.PlaybookRun, channelID string) error { - channel, err := h.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - return err - } - - post := &model.Post{ - Message: fmt.Sprintf("Playbook run %s started in ~%s", playbookRun.Name, channel.Name), - } - h.poster.EphemeralPost(playbookRun.OwnerUserID, channelID, post) - - return nil -} - -func (h *PlaybookRunHandler) updateRetrospective(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var retroUpdate app.RetrospectiveUpdate - - if err := json.NewDecoder(r.Body).Decode(&retroUpdate); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode payload", err) - return - } - - if err := h.playbookRunService.UpdateRetrospective(playbookRunID, userID, retroUpdate); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to update retrospective", err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) publishRetrospective(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookRunID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var retroUpdate app.RetrospectiveUpdate - - if err := json.NewDecoder(r.Body).Decode(&retroUpdate); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode payload", err) - return - } - - if err := h.playbookRunService.PublishRetrospective(playbookRunID, userID, retroUpdate); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusInternalServerError, "unable to publish retrospective", err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) follow(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) { - return - } - - if err := h.playbookRunService.Follow(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) unfollow(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if err := h.playbookRunService.Unfollow(playbookRunID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookRunHandler) getFollowers(c *Context, w http.ResponseWriter, r *http.Request) { - playbookRunID := mux.Vars(r)["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.RunView(userID, playbookRunID)) { - return - } - - var followers []string - var err error - if followers, err = h.playbookRunService.GetFollowers(playbookRunID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, followers, http.StatusOK) -} - -// parsePlaybookRunsFilterOptions is only for parsing. Put validation logic in app.validateOptions. -func parsePlaybookRunsFilterOptions(u *url.URL, currentUserID string) (*app.PlaybookRunFilterOptions, error) { - teamID := u.Query().Get("team_id") - - pageParam := u.Query().Get("page") - if pageParam == "" { - pageParam = "0" - } - page, err := strconv.Atoi(pageParam) - if err != nil { - return nil, errors.Wrapf(err, "bad parameter 'page'") - } - - perPageParam := u.Query().Get("per_page") - if perPageParam == "" { - perPageParam = "0" - } - perPage, err := strconv.Atoi(perPageParam) - if err != nil { - return nil, errors.Wrapf(err, "bad parameter 'per_page'") - } - - sort := u.Query().Get("sort") - direction := u.Query().Get("direction") - - // Parse statuses= query string parameters as an array. - statuses := u.Query()["statuses"] - - ownerID := u.Query().Get("owner_user_id") - if ownerID == client.Me { - ownerID = currentUserID - } - - searchTerm := u.Query().Get("search_term") - - participantID := u.Query().Get("participant_id") - if participantID == client.Me { - participantID = currentUserID - } - - participantOrFollowerID := u.Query().Get("participant_or_follower_id") - if participantOrFollowerID == client.Me { - participantOrFollowerID = currentUserID - } - - playbookID := u.Query().Get("playbook_id") - - activeGTEParam := u.Query().Get("active_gte") - if activeGTEParam == "" { - activeGTEParam = "0" - } - activeGTE, _ := strconv.ParseInt(activeGTEParam, 10, 64) - - activeLTParam := u.Query().Get("active_lt") - if activeLTParam == "" { - activeLTParam = "0" - } - activeLT, _ := strconv.ParseInt(activeLTParam, 10, 64) - - startedGTEParam := u.Query().Get("started_gte") - if startedGTEParam == "" { - startedGTEParam = "0" - } - startedGTE, _ := strconv.ParseInt(startedGTEParam, 10, 64) - - startedLTParam := u.Query().Get("started_lt") - if startedLTParam == "" { - startedLTParam = "0" - } - startedLT, _ := strconv.ParseInt(startedLTParam, 10, 64) - - // Parse types= query string parameters as an array. - types := u.Query()["types"] - - options := app.PlaybookRunFilterOptions{ - TeamID: teamID, - Page: page, - PerPage: perPage, - Sort: app.SortField(sort), - Direction: app.SortDirection(direction), - Statuses: statuses, - OwnerID: ownerID, - SearchTerm: searchTerm, - ParticipantID: participantID, - ParticipantOrFollowerID: participantOrFollowerID, - PlaybookID: playbookID, - ActiveGTE: activeGTE, - ActiveLT: activeLT, - StartedGTE: startedGTE, - StartedLT: startedLT, - Types: types, - } - - options, err = options.Validate() - if err != nil { - return nil, err - } - - return &options, nil -} diff --git a/server/playbooks/server/api/playbooks.go b/server/playbooks/server/api/playbooks.go deleted file mode 100644 index f23fddbfefe..00000000000 --- a/server/playbooks/server/api/playbooks.go +++ /dev/null @@ -1,774 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// PlaybookHandler is the API handler. -type PlaybookHandler struct { - *ErrorHandler - playbookService app.PlaybookService - api playbooks.ServicesAPI - config config.Service - permissions *app.PermissionsService -} - -const SettingsKey = "global_settings" -const maxPlaybooksToAutocomplete = 15 - -// NewPlaybookHandler returns a new playbook api handler -func NewPlaybookHandler(router *mux.Router, playbookService app.PlaybookService, api playbooks.ServicesAPI, configService config.Service, permissions *app.PermissionsService) *PlaybookHandler { - handler := &PlaybookHandler{ - ErrorHandler: &ErrorHandler{}, - playbookService: playbookService, - api: api, - config: configService, - permissions: permissions, - } - - playbooksRouter := router.PathPrefix("/playbooks").Subrouter() - - playbooksRouter.HandleFunc("", withContext(handler.createPlaybook)).Methods(http.MethodPost) - - playbooksRouter.HandleFunc("", withContext(handler.getPlaybooks)).Methods(http.MethodGet) - playbooksRouter.HandleFunc("/autocomplete", withContext(handler.getPlaybooksAutoComplete)).Methods(http.MethodGet) - playbooksRouter.HandleFunc("/import", withContext(handler.importPlaybook)).Methods(http.MethodPost) - - playbookRouter := playbooksRouter.PathPrefix("/{id:[A-Za-z0-9]+}").Subrouter() - playbookRouter.HandleFunc("", withContext(handler.getPlaybook)).Methods(http.MethodGet) - playbookRouter.HandleFunc("", withContext(handler.updatePlaybook)).Methods(http.MethodPut) - playbookRouter.HandleFunc("", withContext(handler.archivePlaybook)).Methods(http.MethodDelete) - playbookRouter.HandleFunc("/restore", withContext(handler.restorePlaybook)).Methods(http.MethodPut) - playbookRouter.HandleFunc("/export", withContext(handler.exportPlaybook)).Methods(http.MethodGet) - playbookRouter.HandleFunc("/duplicate", withContext(handler.duplicatePlaybook)).Methods(http.MethodPost) - - autoFollowsRouter := playbookRouter.PathPrefix("/autofollows").Subrouter() - autoFollowsRouter.HandleFunc("", withContext(handler.getAutoFollows)).Methods(http.MethodGet) - autoFollowRouter := autoFollowsRouter.PathPrefix("/{userID:[A-Za-z0-9]+}").Subrouter() - autoFollowRouter.HandleFunc("", withContext(handler.autoFollow)).Methods(http.MethodPut) - autoFollowRouter.HandleFunc("", withContext(handler.autoUnfollow)).Methods(http.MethodDelete) - - insightsRouter := playbooksRouter.PathPrefix("/insights").Subrouter() - insightsRouter.HandleFunc("/user/me", withContext(handler.getTopPlaybooksForUser)).Methods(http.MethodGet) - insightsRouter.HandleFunc("/teams/{teamID}", withContext(handler.getTopPlaybooksForTeam)).Methods(http.MethodGet) - - return handler -} - -func (h *PlaybookHandler) validPlaybook(w http.ResponseWriter, logger logrus.FieldLogger, playbook *app.Playbook) bool { - if playbook.WebhookOnCreationEnabled { - if err := app.ValidateWebhookURLs(playbook.WebhookOnCreationURLs); err != nil { - h.HandleErrorWithCode(w, logger, http.StatusBadRequest, err.Error(), err) - return false - } - } - - if playbook.WebhookOnStatusUpdateEnabled { - if err := app.ValidateWebhookURLs(playbook.WebhookOnStatusUpdateURLs); err != nil { - h.HandleErrorWithCode(w, logger, http.StatusBadRequest, err.Error(), err) - return false - } - } - - if playbook.CategorizeChannelEnabled { - if err := app.ValidateCategoryName(playbook.CategoryName); err != nil { - h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "invalid category name", err) - return false - } - } - - if len(playbook.SignalAnyKeywords) != 0 { - playbook.SignalAnyKeywords = app.ProcessSignalAnyKeywords(playbook.SignalAnyKeywords) - } - - if playbook.BroadcastEnabled { //nolint - for _, channelID := range playbook.BroadcastChannelIDs { - channel, err := h.api.GetChannelByID(channelID) - if err != nil { - h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "broadcasting to invalid channel ID", err) - return false - } - // check if channel is archived - if channel.DeleteAt != 0 { - h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "broadcasting to archived channel", err) - return false - } - } - } - for listIndex := range playbook.Checklists { - for itemIndex := range playbook.Checklists[listIndex].Items { - if err := validateTaskActions(playbook.Checklists[listIndex].Items[itemIndex].TaskActions); err != nil { - h.HandleErrorWithCode(w, logger, http.StatusBadRequest, "invalid task actions", err) - return false - } - } - } - - return true -} - -func (h *PlaybookHandler) createPlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - var playbook app.Playbook - if err := json.NewDecoder(r.Body).Decode(&playbook); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook", err) - return - } - - if playbook.ID != "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Playbook given already has ID", nil) - return - } - - if playbook.ReminderTimerDefaultSeconds <= 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook ReminderTimerDefaultSeconds must be > 0", nil) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) { - return - } - - // If not specified make the creator the sole admin - if len(playbook.Members) == 0 { - playbook.Members = []app.PlaybookMember{ - { - UserID: userID, - Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin}, - }, - } - } - - if !h.validPlaybook(w, c.logger, &playbook) { - return - } - - if err := h.validateMetrics(playbook); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid metrics configs", err) - return - } - - app.CleanUpChecklists(playbook.Checklists) - - if err := validatePreAssignment(playbook); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Invalid pre-assignment", err) - return - } - - id, err := h.playbookService.Create(playbook, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - result := struct { - ID string `json:"id"` - }{ - ID: id, - } - w.Header().Add("Location", makeAPIURL(h.api, "playbooks/%s", id)) - - ReturnJSON(w, &result, http.StatusCreated) -} - -func (h *PlaybookHandler) getPlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) { - return - } - - playbook, err := h.playbookService.Get(playbookID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, &playbook, http.StatusOK) -} - -func (h *PlaybookHandler) updatePlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - userID := r.Header.Get("Mattermost-User-ID") - var playbook app.Playbook - if err := json.NewDecoder(r.Body).Decode(&playbook); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook", err) - return - } - - // Force parsed playbook id to be URL parameter id - playbook.ID = vars["id"] - oldPlaybook, err := h.playbookService.Get(playbook.ID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - if err = h.validateMetrics(playbook); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid metrics configs", err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookModifyWithFixes(userID, &playbook, oldPlaybook)) { - return - } - - if oldPlaybook.DeleteAt != 0 { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Playbook cannot be modified", fmt.Errorf("playbook with id '%s' cannot be modified because it is archived", playbook.ID)) - return - } - - if !h.validPlaybook(w, c.logger, &playbook) { - return - } - - app.CleanUpChecklists(playbook.Checklists) - - if err = validatePreAssignment(playbook); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Invalid user pre-assignment", err) - return - } - - err = h.playbookService.Update(playbook, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func validatePreAssignment(pb app.Playbook) error { - assignees := app.GetDistinctAssignees(pb.Checklists) - return app.ValidatePreAssignment(assignees, pb.InvitedUserIDs, pb.InviteUsersEnabled) -} - -// validateTaskActions validates the taskactions in the given checklist -// NOTE: Any changes to this function must be made to function 'validateUpdateTaskActions' for the GraphQL endpoint. -func validateTaskActions(taskActions []app.TaskAction) error { - for _, ta := range taskActions { - if err := app.ValidateTrigger(ta.Trigger); err != nil { - return err - } - for _, a := range ta.Actions { - if err := app.ValidateAction(a); err != nil { - return err - } - } - } - return nil -} - -func (h *PlaybookHandler) archivePlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbookToArchive, err := h.playbookService.Get(playbookID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.DeletePlaybook(userID, playbookToArchive)) { - return - } - - err = h.playbookService.Archive(playbookToArchive, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookHandler) restorePlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbookToRestore, err := h.playbookService.Get(playbookID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.DeletePlaybook(userID, playbookToRestore)) { - return - } - - err = h.playbookService.Restore(playbookToRestore, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *PlaybookHandler) getPlaybooks(c *Context, w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - teamID := params.Get("team_id") - userID := r.Header.Get("Mattermost-User-ID") - opts, err := parseGetPlaybooksOptions(r.URL) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, fmt.Sprintf("failed to get playbooks: %s", err.Error()), nil) - return - } - - if teamID != "" && !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) { - return - } - - requesterInfo := app.RequesterInfo{ - UserID: userID, - TeamID: teamID, - IsAdmin: app.IsSystemAdmin(userID, h.api), - } - - isGuest, err := app.IsGuest(userID, h.api) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "", err) - return - } - - if isGuest { - opts.WithMembershipOnly = true - } - - playbookResults, err := h.playbookService.GetPlaybooksForTeam(requesterInfo, teamID, opts) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - ReturnJSON(w, playbookResults, http.StatusOK) -} - -func (h *PlaybookHandler) getPlaybooksAutoComplete(c *Context, w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - teamID := query.Get("team_id") - userID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) { - return - } - - requesterInfo := app.RequesterInfo{ - UserID: userID, - TeamID: teamID, - IsAdmin: app.IsSystemAdmin(userID, h.api), - } - - playbooksResult, err := h.playbookService.GetPlaybooksForTeam(requesterInfo, teamID, app.PlaybookFilterOptions{ - Page: 0, - PerPage: maxPlaybooksToAutocomplete, - WithArchived: query.Get("with_archived") == "true", - }) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - list := make([]model.AutocompleteListItem, 0) - - for _, playbook := range playbooksResult.Items { - list = append(list, model.AutocompleteListItem{ - Item: playbook.ID, - HelpText: playbook.Title, - }) - } - - ReturnJSON(w, list, http.StatusOK) -} - -func parseGetPlaybooksOptions(u *url.URL) (app.PlaybookFilterOptions, error) { - params := u.Query() - - var sortField app.SortField - param := strings.ToLower(params.Get("sort")) - switch param { - case "title", "": - sortField = app.SortByTitle - case "stages": - sortField = app.SortByStages - case "steps": - sortField = app.SortBySteps - case "runs": - sortField = app.SortByRuns - case "last_run_at": - sortField = app.SortByLastRunAt - case "active_runs": - sortField = app.SortByActiveRuns - default: - return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'sort' (%s): it should be empty or one of 'title', 'stages', 'steps', 'runs', 'last_run_at'", param) - } - - var sortDirection app.SortDirection - param = strings.ToLower(params.Get("direction")) - switch param { - case "asc", "": - sortDirection = app.DirectionAsc - case "desc": - sortDirection = app.DirectionDesc - default: - return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'direction' (%s): it should be empty or one of 'asc' or 'desc'", param) - } - - pageParam := params.Get("page") - if pageParam == "" { - pageParam = "0" - } - page, err := strconv.Atoi(pageParam) - if err != nil { - return app.PlaybookFilterOptions{}, errors.Wrapf(err, "bad parameter 'page': it should be a number") - } - if page < 0 { - return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'page': it should be a positive number") - } - - perPageParam := params.Get("per_page") - if perPageParam == "" || perPageParam == "0" { - perPageParam = "1000" - } - perPage, err := strconv.Atoi(perPageParam) - if err != nil { - return app.PlaybookFilterOptions{}, errors.Wrapf(err, "bad parameter 'per_page': it should be a number") - } - if perPage < 0 { - return app.PlaybookFilterOptions{}, errors.Errorf("bad parameter 'per_page': it should be a positive number") - } - - searchTerm := u.Query().Get("search_term") - - withArchived, _ := strconv.ParseBool(u.Query().Get("with_archived")) - - return app.PlaybookFilterOptions{ - Sort: sortField, - Direction: sortDirection, - Page: page, - PerPage: perPage, - SearchTerm: searchTerm, - WithArchived: withArchived, - }, nil -} - -func (h *PlaybookHandler) autoFollow(c *Context, w http.ResponseWriter, r *http.Request) { - playbookID := mux.Vars(r)["id"] - currentUserID := r.Header.Get("Mattermost-User-ID") - userID := mux.Vars(r)["userID"] - - if currentUserID != userID && !app.IsSystemAdmin(currentUserID, h.api) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "User doesn't have permissions to make another user autofollow the playbook.", nil) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) { - return - } - - if err := h.playbookService.AutoFollow(playbookID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -func (h *PlaybookHandler) autoUnfollow(c *Context, w http.ResponseWriter, r *http.Request) { - playbookID := mux.Vars(r)["id"] - currentUserID := r.Header.Get("Mattermost-User-ID") - userID := mux.Vars(r)["userID"] - - if currentUserID != userID && !app.IsSystemAdmin(currentUserID, h.api) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "User doesn't have permissions to make another user autofollow the playbook.", nil) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, playbookID)) { - return - } - - if err := h.playbookService.AutoUnfollow(playbookID, userID); err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.WriteHeader(http.StatusOK) -} - -// getAutoFollows returns the list of users that have marked this playbook for auto-following runs -func (h *PlaybookHandler) getAutoFollows(c *Context, w http.ResponseWriter, r *http.Request) { - playbookID := mux.Vars(r)["id"] - currentUserID := r.Header.Get("Mattermost-User-ID") - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(currentUserID, playbookID)) { - return - } - - autoFollowers, err := h.playbookService.GetAutoFollows(playbookID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - ReturnJSON(w, autoFollowers, http.StatusOK) -} - -func (h *PlaybookHandler) exportPlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbook, err := h.playbookService.Get(playbookID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookViewWithPlaybook(userID, playbook)) { - return - } - - export, err := app.GeneratePlaybookExport(playbook) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write(export) -} - -func (h *PlaybookHandler) duplicatePlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - playbookID := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - playbook, err := h.playbookService.Get(playbookID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookViewWithPlaybook(userID, playbook)) { - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) { - return - } - - newPlaybookID, err := h.playbookService.Duplicate(playbook, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - result := struct { - ID string `json:"id"` - }{ - ID: newPlaybookID, - } - ReturnJSON(w, &result, http.StatusCreated) -} - -func (h *PlaybookHandler) importPlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - params := r.URL.Query() - teamID := params.Get("team_id") - userID := r.Header.Get("Mattermost-User-ID") - var importBlock struct { - app.Playbook - Version int `json:"version"` - } - if err := json.NewDecoder(r.Body).Decode(&importBlock); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode playbook import", err) - return - } - playbook := importBlock.Playbook - - if playbook.ID != "" { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "playbook import should not have ID field", nil) - return - } - - if importBlock.Version != app.CurrentPlaybookExportVersion { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Unsupported import version", nil) - return - } - - // Make the importer the sole admin of the playbook. - playbook.Members = []app.PlaybookMember{ - { - UserID: userID, - Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin}, - }, - } - - // Force the imported playbook to be public to avoid licencing issues - playbook.Public = true - - if teamID != "" { - playbook.TeamID = teamID - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookCreate(userID, playbook)) { - return - } - - if !h.validPlaybook(w, c.logger, &playbook) { - return - } - - id, err := h.playbookService.Import(playbook, userID) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - result := struct { - ID string `json:"id"` - }{ - ID: id, - } - w.Header().Add("Location", makeAPIURL(h.api, "playbooks/%s", id)) - - ReturnJSON(w, &result, http.StatusCreated) -} - -func (h *PlaybookHandler) validateMetrics(pb app.Playbook) error { - if len(pb.Metrics) > app.MaxMetricsPerPlaybook { - return errors.Errorf(fmt.Sprintf("playbook cannot have more than %d key metrics", app.MaxMetricsPerPlaybook)) - } - - //check if titles are unique - titles := make(map[string]bool) - for _, m := range pb.Metrics { - if titles[m.Title] { - return errors.Errorf("metrics names must be unique") - } - titles[m.Title] = true - } - return nil -} - -func (h *PlaybookHandler) getTopPlaybooksForUser(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - params := r.URL.Query() - timeRange := params.Get("time_range") - teamID := params.Get("team_id") - if teamID == "" { - h.HandleErrorWithCode(w, c.logger, http.StatusNotImplemented, "invalid team_id parameter", errors.New("teamID cannot be empty")) - return - } - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) { - return - } - - page, err := strconv.Atoi(params.Get("page")) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting page parameter to integer", err) - return - } - perPage, err := strconv.Atoi(params.Get("per_page")) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting per_page parameter to integer", err) - return - } - - // setting startTime as per user's location - user, err := h.api.GetUserByID(userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err) - return - } - timezone := user.GetTimezoneLocation() - - // get unix time for duration - startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone) - if appErr != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid time parameter", appErr) - return - } - - topPlaybooks, err := h.playbookService.GetTopPlaybooksForUser(teamID, userID, &model.InsightsOpts{ - StartUnixMilli: model.GetMillisForTime(*startTime), - Page: page, - PerPage: perPage, - }) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - ReturnJSON(w, &topPlaybooks, http.StatusOK) -} - -func (h *PlaybookHandler) getTopPlaybooksForTeam(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - teamID := vars["teamID"] - userID := r.Header.Get("Mattermost-User-ID") - params := r.URL.Query() - timeRange := params.Get("time_range") - if teamID == "" { - h.HandleErrorWithCode(w, c.logger, http.StatusNotImplemented, "invalid team_id parameter", errors.New("teamID cannot be empty")) - return - } - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookList(userID, teamID)) { - return - } - page, err := strconv.Atoi(params.Get("page")) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting page parameter to integer", err) - return - } - perPage, err := strconv.Atoi(params.Get("per_page")) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "error converting per_page parameter to integer", err) - return - } - - // setting startTime as per user's location - user, err := h.api.GetUserByID(userID) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to get user", err) - return - } - timezone := user.GetTimezoneLocation() - - // get unix time for duration - startTime, appErr := model.GetStartOfDayForTimeRange(timeRange, timezone) - if appErr != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid time parameter", appErr) - return - } - - topPlaybooks, err := h.playbookService.GetTopPlaybooksForTeam(teamID, userID, &model.InsightsOpts{ - StartUnixMilli: model.GetMillisForTime(*startTime), - Page: page, - PerPage: perPage, - }) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - ReturnJSON(w, &topPlaybooks, http.StatusOK) -} diff --git a/server/playbooks/server/api/schema.graphqls b/server/playbooks/server/api/schema.graphqls deleted file mode 100644 index 1d70dba1991..00000000000 --- a/server/playbooks/server/api/schema.graphqls +++ /dev/null @@ -1,324 +0,0 @@ -type Query { - playbook(id: String!): Playbook - playbooks( - teamID: String = "", - sort: String = "title", - direction: String = "ASC", - searchTerm: String = "", - withArchived: Boolean = false, - withMembershipOnly: Boolean = false, - ): [Playbook!]! - - run(id: String!): Run - runs( - teamID: String = "", - sort: String = "", - direction: String = "", - statuses: [String!] = [], - participantOrFollowerID: String = "", - channelID: String = "", - first: Int, - after: String, - types: [PlaybookRunType!] = [], - ): RunConnection! -} - -type Mutation { - updatePlaybookFavorite(id: String!, favorite: Boolean!): String! - updatePlaybook(id: String!, updates: PlaybookUpdates!): String! - - addMetric(playbookID: String!, title: String!, description: String!, type: String!, target: Int): String! - updateMetric(id: String!, title: String, description: String, target: Int): String! - deleteMetric(id: String!): String! - - addPlaybookMember(playbookID: String!, userID: String!): String! - removePlaybookMember(playbookID: String!, userID: String!): String! - - setRunFavorite(id: String!, fav: Boolean!): String! - updateRun(id: String!, updates: RunUpdates!): String! - addRunParticipants(runID: String!, userIDs: [String!]!, forceAddToChannel: Boolean = false): String! - removeRunParticipants(runID: String!, userIDs: [String!]!): String! - changeRunOwner(runID: String!, ownerID: String!): String! - updateRunTaskActions(runID: String!, checklistNum: Float!, itemNum: Float!, taskActions: [TaskActionUpdates!]): String! -} - -type PageInfo { - hasNextPage: Boolean! - startCursor: String! - endCursor: String! -} - -input PlaybookUpdates { - title: String - description: String - public: Boolean - createPublicPlaybookRun: Boolean - reminderMessageTemplate: String - reminderTimerDefaultSeconds: Float - statusUpdateEnabled: Boolean - invitedUserIDs: [String!] - invitedGroupIDs: [String!] - inviteUsersEnabled: Boolean - defaultOwnerID: String - defaultOwnerEnabled: Boolean - broadcastChannelIDs: [String!] - broadcastEnabled: Boolean - webhookOnCreationURLs: [String!] - webhookOnCreationEnabled: Boolean - messageOnJoin: String - messageOnJoinEnabled: Boolean - retrospectiveReminderIntervalSeconds: Float - retrospectiveTemplate: String - retrospectiveEnabled: Boolean - webhookOnStatusUpdateURLs: [String!] - webhookOnStatusUpdateEnabled: Boolean - signalAnyKeywords: [String!] - signalAnyKeywordsEnabled: Boolean - categorizeChannelEnabled: Boolean - categoryName: String - runSummaryTemplateEnabled: Boolean - runSummaryTemplate: String - channelNameTemplate: String - checklists: [ChecklistUpdates!] - createChannelMemberOnNewParticipant: Boolean - removeChannelMemberOnRemovedParticipant: Boolean - channelId: String - channelMode: String -} - -input ChecklistUpdates { - title: String! - items: [ChecklistItemUpdates!]! -} - -input ChecklistItemUpdates { - title: String! - description: String! - state: String! - stateModified: Float! - assigneeID: String! - assigneeModified: Float! - command: String! - commandLastRun: Float! - dueDate: Float! - taskActions: [TaskActionUpdates!] -} - -input TaskActionUpdates { - trigger: TriggerUpdates! - actions: [ActionUpdates!]! -} - -input TriggerUpdates { - type: String! - payload: String! -} - -input ActionUpdates { - type: String! - payload: String! -} - -type Playbook { - id: String! - title: String! - description: String! - teamID: String! - createPublicPlaybookRun: Boolean! - deleteAt: Float! - lastRunAt: Float! - numRuns: Int! - activeRuns: Int! - runSummaryTemplateEnabled: Boolean! - defaultPlaybookMemberRole: String! - public: Boolean! - checklists: [Checklist!]! - members: [Member!]! - reminderMessageTemplate: String! - reminderTimerDefaultSeconds: Float! - statusUpdateEnabled: Boolean! - invitedUserIDs: [String!]! - invitedGroupIDs: [String!]! - inviteUsersEnabled: Boolean! - defaultOwnerID: String! - defaultOwnerEnabled: Boolean! - broadcastChannelIDs: [String!]! - broadcastEnabled: Boolean! - webhookOnCreationURLs: [String!]! - webhookOnCreationEnabled: Boolean! - messageOnJoin: String! - messageOnJoinEnabled: Boolean! - retrospectiveReminderIntervalSeconds: Float! - retrospectiveTemplate: String! - retrospectiveEnabled: Boolean! - webhookOnStatusUpdateURLs: [String!]! - webhookOnStatusUpdateEnabled: Boolean! - signalAnyKeywords: [String!]! - signalAnyKeywordsEnabled: Boolean! - categorizeChannelEnabled: Boolean! - categoryName: String! - runSummaryTemplateEnabled: Boolean! - runSummaryTemplate: String! - channelNameTemplate: String! - defaultPlaybookAdminRole: String! - defaultPlaybookMemberRole: String! - defaultRunAdminRole: String! - defaultRunMemberRole: String! - metrics: [PlaybookMetricConfig!]! - isFavorite: Boolean! - createChannelMemberOnNewParticipant: Boolean! - removeChannelMemberOnRemovedParticipant: Boolean! - channelID: String! - channelMode: String! -} - -type Checklist { - title: String! - items: [ChecklistItem!]! -} - -type Member { - userID: String! - roles: [String!]! - schemeRoles: [String!]! -} - -type ChecklistItem { - title: String! - description: String! - state: String! - stateModified: Float! - assigneeID: String! - assigneeModified: Float! - command: String! - commandLastRun: Float! - dueDate: Float! - taskActions: [TaskAction!]! -} - -type TaskAction { - trigger: Trigger! - actions: [Action!]! -} - -type Trigger { - type: String! - payload: String! -} - -type Action { - type: String! - payload: String! -} - -enum MetricType { - metric_duration - metric_currency - metric_integer -} - -type PlaybookMetricConfig { - id: String! - title: String! - description: String! - type: MetricType! - target: Int -} - -enum PlaybookRunType { - playbook - channelChecklist -} - -type Run { - id: String! - playbookID: String! - playbook: Playbook - name: String! - ownerUserID: String! - channelID: String! - postID: String! - teamID: String! - isFavorite: Boolean! - currentStatus: String! - createAt: Float! - endAt: Float! - participantIDs: [String!]! - - summary: String! - summaryModifiedAt: Float! - checklists: [Checklist!]! - - retrospective: String! - retrospectivePublishedAt: Float! - retrospectiveReminderIntervalSeconds: Float! - retrospectiveEnabled: Boolean! - retrospectiveWasCanceled: Boolean! - - statusUpdateEnabled: Boolean! - statusUpdateBroadcastWebhooksEnabled: Boolean! - lastStatusUpdateAt: Float! - statusPosts: [StatusPost!]! - reminderPostId: String! - reminderMessageTemplate: String! - reminderTimerDefaultSeconds: Float! - previousReminder: Float! - - statusUpdateBroadcastChannelsEnabled: Boolean! - statusUpdateBroadcastWebhooksEnabled: Boolean! - broadcastChannelIDs: [String!]! - webhookOnStatusUpdateURLs: [String!]! - createChannelMemberOnNewParticipant: Boolean! - removeChannelMemberOnRemovedParticipant: Boolean! - - lastUpdatedAt: Float! - - timelineEvents: [TimelineEvent!]! - followers: [String!]! - - numTasks: Int! - numTasksClosed: Int! - - type: PlaybookRunType! -} - -type RunConnection { - totalCount: Int! - edges: [RunEdge!]! - pageInfo: PageInfo! -} - -type RunEdge { - cursor: String! - node: Run! -} - -type StatusPost { - id: String! - createAt: Float! - deleteAt: Float! -} - -type TimelineEvent { - id: String! - createAt: Float! - deleteAt: Float! - eventType: String! - details: String! - postID: String! - summary: String! - subjectUserID: String! - creatorUserID: String! -} - -input RunUpdates { - name: String - summary: String - createChannelMemberOnNewParticipant: Boolean - removeChannelMemberOnRemovedParticipant: Boolean - statusUpdateBroadcastChannelsEnabled: Boolean - statusUpdateBroadcastWebhooksEnabled: Boolean - broadcastChannelIDs: [String!] - webhookOnStatusUpdateURLs: [String!] - channelID: String -} diff --git a/server/playbooks/server/api/settings.go b/server/playbooks/server/api/settings.go deleted file mode 100644 index c6e787c664d..00000000000 --- a/server/playbooks/server/api/settings.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "net/http" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -// SettingsHandler is the API handler. -type SettingsHandler struct { - *ErrorHandler - api playbooks.ServicesAPI - config config.Service -} - -// NewSettingsHandler returns a new settings api handler -func NewSettingsHandler(router *mux.Router, api playbooks.ServicesAPI, configService config.Service) *SettingsHandler { - handler := &SettingsHandler{ - ErrorHandler: &ErrorHandler{}, - api: api, - config: configService, - } - - settingsRouter := router.PathPrefix("/settings").Subrouter() - settingsRouter.HandleFunc("", handler.getSettings).Methods(http.MethodGet) - - return handler -} - -func (h *SettingsHandler) getSettings(w http.ResponseWriter, r *http.Request) { - cfg := h.config.GetConfiguration() - settings := client.GlobalSettings{ - EnableExperimentalFeatures: cfg.EnableExperimentalFeatures, - } - - ReturnJSON(w, &settings, http.StatusOK) -} diff --git a/server/playbooks/server/api/signal.go b/server/playbooks/server/api/signal.go deleted file mode 100644 index f6afa135050..00000000000 --- a/server/playbooks/server/api/signal.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -type SignalHandler struct { - *ErrorHandler - api playbooks.ServicesAPI - playbookRunService app.PlaybookRunService - playbookService app.PlaybookService - keywordsThreadIgnorer app.KeywordsThreadIgnorer -} - -func NewSignalHandler(router *mux.Router, api playbooks.ServicesAPI, playbookRunService app.PlaybookRunService, playbookService app.PlaybookService, keywordsThreadIgnorer app.KeywordsThreadIgnorer) *SignalHandler { - handler := &SignalHandler{ - ErrorHandler: &ErrorHandler{}, - api: api, - playbookRunService: playbookRunService, - playbookService: playbookService, - keywordsThreadIgnorer: keywordsThreadIgnorer, - } - - signalRouter := router.PathPrefix("/signal").Subrouter() - - keywordsRouter := signalRouter.PathPrefix("/keywords").Subrouter() - keywordsRouter.HandleFunc("/run-playbook", withContext(handler.playbookRun)).Methods(http.MethodPost) - keywordsRouter.HandleFunc("/ignore-thread", withContext(handler.ignoreKeywords)).Methods(http.MethodPost) - - return handler -} - -func (h *SignalHandler) playbookRun(c *Context, w http.ResponseWriter, r *http.Request) { - publicErrorMessage := "unable to decode post action integration request" - - var req *model.PostActionIntegrationRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - h.returnError(publicErrorMessage, err, c.logger, w) - return - } - if req == nil { - h.returnError(publicErrorMessage, errors.New("nil request"), c.logger, w) - return - } - - id, err := getStringField("selected_option", req.Context, w) - if err != nil { - h.returnError(publicErrorMessage, err, c.logger, w) - return - } - - pbook, err := h.playbookService.Get(id) - if err != nil { - h.returnError("can't get chosen playbook", errors.Wrapf(err, "can't get chosen playbook, id - %s", id), c.logger, w) - return - } - - if err := h.playbookRunService.OpenCreatePlaybookRunDialog(req.TeamId, req.UserId, req.TriggerId, "", "", []app.Playbook{pbook}); err != nil { - h.returnError("can't open dialog", errors.Wrap(err, "can't open a dialog"), c.logger, w) - return - } - - ReturnJSON(w, &model.PostActionIntegrationResponse{}, http.StatusOK) - if _, err := h.api.DeletePost(req.PostId); err != nil { - h.returnError("unable to delete original post", err, c.logger, w) - return - } -} - -func (h *SignalHandler) ignoreKeywords(c *Context, w http.ResponseWriter, r *http.Request) { - publicErrorMessage := "unable to decode post action integration request" - - var req *model.PostActionIntegrationRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil || req == nil { - h.returnError(publicErrorMessage, err, c.logger, w) - return - } - - postID, err := getStringField("postID", req.Context, w) - if err != nil { - h.returnError(publicErrorMessage, err, c.logger, w) - return - } - post, err := h.api.GetPost(postID) - if err != nil { - h.returnError(publicErrorMessage, err, c.logger, w) - return - } - - h.keywordsThreadIgnorer.Ignore(postID, post.UserId) - if post.RootId != "" { - h.keywordsThreadIgnorer.Ignore(post.RootId, post.UserId) - } - - ReturnJSON(w, &model.PostActionIntegrationResponse{}, http.StatusOK) - if _, err := h.api.DeletePost(req.PostId); err != nil { - h.returnError("unable to delete original post", err, c.logger, w) - return - } -} - -func (h *SignalHandler) returnError(returnMessage string, err error, logger logrus.FieldLogger, w http.ResponseWriter) { - resp := model.PostActionIntegrationResponse{ - EphemeralText: fmt.Sprintf("Error: %s", returnMessage), - } - logger.WithError(err).Warn(returnMessage) - ReturnJSON(w, &resp, http.StatusOK) -} - -func getStringField(field string, context map[string]interface{}, w http.ResponseWriter) (string, error) { - fieldInt, ok := context[field] - if !ok { - return "", errors.Errorf("no %s field in the request context", field) - } - fieldValue, ok := fieldInt.(string) - if !ok { - return "", errors.Errorf("%s field is not a string", field) - } - return fieldValue, nil -} diff --git a/server/playbooks/server/api/stats.go b/server/playbooks/server/api/stats.go deleted file mode 100644 index b6c343b0d36..00000000000 --- a/server/playbooks/server/api/stats.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "math" - "net/http" - "net/url" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "gopkg.in/guregu/null.v4" - - "github.com/gorilla/mux" - "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore" - "github.com/pkg/errors" -) - -type StatsHandler struct { - *ErrorHandler - api playbooks.ServicesAPI - statsStore *sqlstore.StatsStore - playbookService app.PlaybookService - permissions *app.PermissionsService - licenseChecker app.LicenseChecker -} - -func NewStatsHandler(router *mux.Router, api playbooks.ServicesAPI, statsStore *sqlstore.StatsStore, playbookService app.PlaybookService, permissions *app.PermissionsService, licenseChecker app.LicenseChecker) *StatsHandler { - handler := &StatsHandler{ - ErrorHandler: &ErrorHandler{}, - api: api, - statsStore: statsStore, - playbookService: playbookService, - permissions: permissions, - licenseChecker: licenseChecker, - } - - statsRouter := router.PathPrefix("/stats").Subrouter() - statsRouter.HandleFunc("/site", withContext(handler.playbookSiteStats)).Methods(http.MethodGet) - statsRouter.HandleFunc("/playbook", withContext(handler.playbookStats)).Methods(http.MethodGet) - - return handler -} - -type PlaybookStats struct { - RunsInProgress int `json:"runs_in_progress"` - ParticipantsActive int `json:"participants_active"` - RunsFinishedPrev30Days int `json:"runs_finished_prev_30_days"` - RunsFinishedPercentageChange int `json:"runs_finished_percentage_change"` - RunsStartedPerWeek []int `json:"runs_started_per_week"` - RunsStartedPerWeekTimes [][]int64 `json:"runs_started_per_week_times"` - ActiveRunsPerDay []int `json:"active_runs_per_day"` - ActiveRunsPerDayTimes [][]int64 `json:"active_runs_per_day_times"` - ActiveParticipantsPerDay []int `json:"active_participants_per_day"` - ActiveParticipantsPerDayTimes [][]int64 `json:"active_participants_per_day_times"` - MetricOverallAverage []null.Int `json:"metric_overall_average"` - MetricRollingAverage []null.Int `json:"metric_rolling_average"` - MetricRollingAverageChange []null.Int `json:"metric_rolling_average_change"` - MetricValueRange [][]int64 `json:"metric_value_range"` - MetricRollingValues [][]int64 `json:"metric_rolling_values"` - LastXRunNames []string `json:"last_x_run_names"` -} - -const ( - MetricChartPeriod = 10 - MetricRollingAveragePeriod = 10 -) - -func parsePlaybookStatsFilters(u *url.URL) (*sqlstore.StatsFilters, error) { - playbookID := u.Query().Get("playbook_id") - if playbookID == "" { - return nil, errors.New("bad parameter 'playbook_id'; 'playbook_id' is required") - } - - return &sqlstore.StatsFilters{ - PlaybookID: playbookID, - }, nil -} - -// playbookStats handles the internal plugin stats -func (h *StatsHandler) playbookStats(c *Context, w http.ResponseWriter, r *http.Request) { - if !h.licenseChecker.StatsAllowed() { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "timeline feature is not covered by current server license", nil) - return - } - - userID := r.Header.Get("Mattermost-User-ID") - - filters, err := parsePlaybookStatsFilters(r.URL) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "Bad filters", err) - return - } - - if !h.PermissionsCheck(w, c.logger, h.permissions.PlaybookView(userID, filters.PlaybookID)) { - return - } - - runsFinishedLast30Days := h.statsStore.RunsFinishedBetweenDays(filters, 30, 0) - runsFinishedBetween60and30DaysAgo := h.statsStore.RunsFinishedBetweenDays(filters, 60, 31) - var percentageChange int - if runsFinishedBetween60and30DaysAgo == 0 { - percentageChange = 99999999 - } else { - percentageChange = int(math.Floor(float64((runsFinishedLast30Days-runsFinishedBetween60and30DaysAgo)/runsFinishedBetween60and30DaysAgo) * 100)) - } - runsStartedPerWeek, runsStartedPerWeekTimes := h.statsStore.RunsStartedPerWeekLastXWeeks(12, filters) - activeRunsPerDay, activeRunsPerDayTimes := h.statsStore.ActiveRunsPerDayLastXDays(14, filters) - activeParticipantsPerDay, activeParticipantsPerDayTimes := h.statsStore.ActiveParticipantsPerDayLastXDays(14, filters) - - metricOverallAverage := h.statsStore.MetricOverallAverage(*filters) - metricRollingValues, lastXRunNames := h.statsStore.MetricRollingValuesLastXRuns(MetricChartPeriod, 0, *filters) - metricRollingAverage, metricRollingAverageChange := h.statsStore.MetricRollingAverageAndChange(MetricRollingAveragePeriod, *filters) - metricValueRange := h.statsStore.MetricValueRange(*filters) - - ReturnJSON(w, &PlaybookStats{ - RunsInProgress: h.statsStore.TotalInProgressPlaybookRuns(filters), - ParticipantsActive: h.statsStore.TotalActiveParticipants(filters), - RunsFinishedPrev30Days: runsFinishedLast30Days, - RunsFinishedPercentageChange: percentageChange, - RunsStartedPerWeek: runsStartedPerWeek, - RunsStartedPerWeekTimes: runsStartedPerWeekTimes, - ActiveRunsPerDay: activeRunsPerDay, - ActiveRunsPerDayTimes: activeRunsPerDayTimes, - ActiveParticipantsPerDay: activeParticipantsPerDay, - ActiveParticipantsPerDayTimes: activeParticipantsPerDayTimes, - MetricOverallAverage: metricOverallAverage, - MetricRollingValues: metricRollingValues, - MetricValueRange: metricValueRange, - MetricRollingAverage: metricRollingAverage, - MetricRollingAverageChange: metricRollingAverageChange, - LastXRunNames: lastXRunNames, - }, http.StatusOK) -} - -type PlaybookSiteStats struct { - TotalPlaybooks int `json:"total_playbooks"` - TotalPlaybookRuns int `json:"total_playbook_runs"` -} - -// playbooSitekStats collects and sends the stats used for system-console > statistics -// -// Response 200: PlaybookSiteStats -// Response 401: when user is not authenticated -// Response 403: when user has no permissions to see stats -func (h *StatsHandler) playbookSiteStats(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - // user must have right to access analytics - if !h.api.HasPermissionTo(userID, model.PermissionGetAnalytics) { - h.HandleErrorWithCode(w, c.logger, http.StatusForbidden, "user is not allowed to get site stats", nil) - return - } - totalPlaybooks, err := h.statsStore.TotalPlaybooks() - if err != nil { - c.logger.WithError(err).Warn("playbookSiteStats failed fetching total playbooks") - } - totalRuns, err := h.statsStore.TotalPlaybookRuns() - if err != nil { - c.logger.WithError(err).Warn("playbookSiteStats failed fetching total playbook runs") - } - ReturnJSON(w, &PlaybookSiteStats{ - TotalPlaybooks: totalPlaybooks, - TotalPlaybookRuns: totalRuns, - }, http.StatusOK) -} diff --git a/server/playbooks/server/api/telemetry.go b/server/playbooks/server/api/telemetry.go deleted file mode 100644 index 6f979e3001f..00000000000 --- a/server/playbooks/server/api/telemetry.go +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "encoding/json" - "net/http" - - "github.com/gorilla/mux" - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -// TelemetryHandler is the API handler. -type TelemetryHandler struct { - *ErrorHandler - playbookRunService app.PlaybookRunService - playbookRunTelemetry app.PlaybookRunTelemetry - playbookService app.PlaybookService - permissions *app.PermissionsService - playbookTelemetry app.PlaybookTelemetry - genericTelemetry app.GenericTelemetry - botTelemetry bot.Telemetry - api playbooks.ServicesAPI -} - -// NewTelemetryHandler Creates a new Plugin API handler. -func NewTelemetryHandler( - router *mux.Router, - playbookRunService app.PlaybookRunService, - api playbooks.ServicesAPI, - playbookRunTelemetry app.PlaybookRunTelemetry, - playbookService app.PlaybookService, - playbookTelemetry app.PlaybookTelemetry, - genericTelemetry app.GenericTelemetry, - botTelemetry bot.Telemetry, - permissions *app.PermissionsService, -) *TelemetryHandler { - handler := &TelemetryHandler{ - ErrorHandler: &ErrorHandler{}, - playbookRunService: playbookRunService, - playbookRunTelemetry: playbookRunTelemetry, - playbookService: playbookService, - playbookTelemetry: playbookTelemetry, - genericTelemetry: genericTelemetry, - botTelemetry: botTelemetry, - api: api, - permissions: permissions, - } - - telemetryRouter := router.PathPrefix("/telemetry").Subrouter() - telemetryRouter.HandleFunc("", withContext(handler.createEvent)).Methods(http.MethodPost) - - startTrialRouter := telemetryRouter.PathPrefix("/start-trial").Subrouter() - startTrialRouter.HandleFunc("", withContext(handler.startTrial)).Methods(http.MethodPost) - - playbookRunTelemetryRouterAuthorized := telemetryRouter.PathPrefix("/run").Subrouter() - playbookRunTelemetryRouterAuthorized.Use(handler.checkPlaybookRunViewPermissions) - playbookRunTelemetryRouterAuthorized.HandleFunc("/{id:[A-Za-z0-9]+}", withContext(handler.telemetryForPlaybookRun)).Methods(http.MethodPost) - - playbookTelemetryRouterAuthorized := telemetryRouter.PathPrefix("/playbook").Subrouter() - playbookTelemetryRouterAuthorized.Use(handler.checkPlaybookViewPermissions) - playbookTelemetryRouterAuthorized.HandleFunc("/{id:[A-Za-z0-9]+}", withContext(handler.telemetryForPlaybook)).Methods(http.MethodPost) - - templateRouter := telemetryRouter.PathPrefix("/template").Subrouter() - templateRouter.HandleFunc("", withContext(handler.telemetryForTemplate)) - - return handler -} - -type EventData struct { - Name string - Type app.TelemetryType - Properties map[string]interface{} -} - -func (h *TelemetryHandler) createEvent(c *Context, w http.ResponseWriter, r *http.Request) { - var event EventData - if err := json.NewDecoder(r.Body).Decode(&event); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err) - return - } - - if event.Properties == nil { - event.Properties = map[string]interface{}{} - } - event.Properties["UserActualID"] = r.Header.Get("Mattermost-User-ID") - - switch event.Type { - case app.TelemetryTypePage: - name, err := app.NewTelemetryPage(event.Name) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid page tracking", err) - return - } - h.genericTelemetry.Page(*name, event.Properties) - case app.TelemetryTypeTrack: - name, err := app.NewTelemetryTrack(event.Name) - if err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid event tracking", err) - return - } - h.genericTelemetry.Track(*name, event.Properties) - default: - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "invalid type to be tracked", nil) - return - } - - w.WriteHeader(http.StatusNoContent) -} - -func (h *TelemetryHandler) checkPlaybookRunViewPermissions(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - userID := r.Header.Get("Mattermost-User-ID") - runID := vars["id"] - - if err := h.permissions.RunView(userID, runID); err != nil { - logger := getLogger(r) - if errors.Is(err, app.ErrNoPermissions) { - h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", err) - return - } - h.HandleError(w, logger, err) - return - } - - next.ServeHTTP(w, r) - }) -} - -func (h *TelemetryHandler) checkPlaybookViewPermissions(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - userID := r.Header.Get("Mattermost-User-ID") - playbookID := vars["id"] - - if err := h.permissions.PlaybookView(userID, playbookID); err != nil { - logger := getLogger(r) - if errors.Is(err, app.ErrNoPermissions) { - h.HandleErrorWithCode(w, logger, http.StatusForbidden, "Not authorized", err) - return - } - h.HandleError(w, logger, err) - return - } - - next.ServeHTTP(w, r) - }) -} - -type TrackerPayload struct { - Action string `json:"action"` -} - -// telemetryForPlaybookRun handles the /telemetry/run/{id}?action=the_action endpoint. The frontend -// can use this endpoint to track events that occur in the context of a playbook run. -func (h *TelemetryHandler) telemetryForPlaybookRun(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var params TrackerPayload - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err) - return - } - - if params.Action == "" { - h.HandleError(w, c.logger, errors.New("must provide action")) - return - } - - playbookRun, err := h.playbookRunService.GetPlaybookRun(id) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - h.playbookRunTelemetry.FrontendTelemetryForPlaybookRun(playbookRun, userID, params.Action) - - w.WriteHeader(http.StatusNoContent) -} - -func (h *TelemetryHandler) startTrial(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - var params TrackerPayload - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err) - return - } - - h.botTelemetry.StartTrial(userID, params.Action) - - w.WriteHeader(http.StatusNoContent) -} - -// telemetryForPlaybook handles the /telemetry/playbook/{id}?action=the_action endpoint. The frontend -// can use this endpoint to track events that occur in the context of a playbook. -func (h *TelemetryHandler) telemetryForPlaybook(c *Context, w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - id := vars["id"] - userID := r.Header.Get("Mattermost-User-ID") - - var params TrackerPayload - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err) - return - } - - if params.Action == "" { - h.HandleError(w, c.logger, errors.New("must provide action")) - return - } - - playbook, err := h.playbookService.Get(id) - if err != nil { - h.HandleError(w, c.logger, err) - return - } - - h.playbookTelemetry.FrontendTelemetryForPlaybook(playbook, userID, params.Action) - - w.WriteHeader(http.StatusNoContent) -} - -func (h *TelemetryHandler) telemetryForTemplate(c *Context, w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get("Mattermost-User-ID") - - var params struct { - TemplateName string `json:"template_name"` - Action string `json:"action"` - } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - h.HandleErrorWithCode(w, c.logger, http.StatusBadRequest, "unable to decode post body", err) - return - } - - if params.TemplateName == "" { - h.HandleError(w, c.logger, errors.New("must provide template_name")) - return - } - if params.Action == "" { - h.HandleError(w, c.logger, errors.New("must provide action")) - return - } - - h.playbookTelemetry.FrontendTelemetryForPlaybookTemplate(params.TemplateName, userID, params.Action) - - w.WriteHeader(http.StatusNoContent) -} diff --git a/server/playbooks/server/api/urls.go b/server/playbooks/server/api/urls.go deleted file mode 100644 index aee71ec815a..00000000000 --- a/server/playbooks/server/api/urls.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package api - -import ( - "fmt" - "net/url" - "path" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const defaultBaseAPIURL = "plugins/playbooks/api/v0" - -func getAPIBaseURL(api playbooks.ServicesAPI) (string, error) { - siteURL := model.ServiceSettingsDefaultSiteURL - if api.GetConfig().ServiceSettings.SiteURL != nil { - siteURL = *api.GetConfig().ServiceSettings.SiteURL - } - - parsedSiteURL, err := url.Parse(siteURL) - if err != nil { - return "", errors.Wrapf(err, "failed to parse siteURL %s", siteURL) - } - - return path.Join(parsedSiteURL.Path, defaultBaseAPIURL), nil -} - -func makeAPIURL(api playbooks.ServicesAPI, apiPath string, args ...interface{}) string { - apiBaseURL, err := getAPIBaseURL(api) - if err != nil { - logrus.WithError(err).Error("failed to build api base url") - apiBaseURL = defaultBaseAPIURL - } - - return path.Join("/", apiBaseURL, fmt.Sprintf(apiPath, args...)) -} diff --git a/server/playbooks/server/api_actions_test.go b/server/playbooks/server/api_actions_test.go deleted file mode 100644 index 8ef8f87d835..00000000000 --- a/server/playbooks/server/api_actions_test.go +++ /dev/null @@ -1,457 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "net/http" - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mitchellh/mapstructure" - "github.com/stretchr/testify/assert" -) - -func TestActionCreation(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - createNewChannel := func(t *testing.T, name string) *model.Channel { - t.Helper() - - pubChannel, _, err := e.ServerAdminClient.CreateChannel(context.Background(), &model.Channel{ - DisplayName: name, - Name: name, - Type: model.ChannelTypeOpen, - TeamId: e.BasicTeam.Id, - }) - assert.NoError(t, err) - - _, _, err = e.ServerAdminClient.AddChannelMember(context.Background(), pubChannel.Id, e.RegularUser.Id) - assert.NoError(t, err) - - return pubChannel - } - - t.Run("create valid action", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-valid-action") - - // Create a valid action - actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: client.ActionTypeWelcomeMessage, - TriggerType: client.TriggerTypeNewMemberJoins, - Payload: client.WelcomeMessagePayload{ - Message: "Hello!", - }, - }) - - // Verify that the API succeeds - assert.NoError(t, err) - assert.NotEmpty(t, actionID) - }) - - t.Run("create valid partial action", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-valid-partial-action") - - // Create an action with only keywords, but no playbook ID - actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: client.ActionTypePromptRunPlaybook, - TriggerType: client.TriggerTypeKeywordsPosted, - Payload: client.PromptRunPlaybookFromKeywordsPayload{ - Keywords: []string{"one"}, - }, - }) - - // Verify that the API succeeds - assert.NoError(t, err) - assert.NotEmpty(t, actionID) - }) - - t.Run("create invalid action - duplicate action and trigger types", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-invalid-action-duplicate") - - // Define an action - action := client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: client.ActionTypeCategorizeChannel, - TriggerType: client.TriggerTypeNewMemberJoins, - Payload: client.CategorizeChannelPayload{ - CategoryName: "category", - }, - } - - // Create a valid action - actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, action) - - // Verify that the API succeeds - assert.NoError(t, err) - assert.NotEmpty(t, actionID) - - // Try to create the same action again - _, err = e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, action) - - // Verify that the API fails with a 500 error - requireErrorWithStatusCode(t, err, http.StatusInternalServerError) - }) - - t.Run("create invalid action - wrong action type", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-invalid-action-wrong-action") - - // Create an action with a wrong action type - _, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: "wrong action type", - TriggerType: client.TriggerTypeNewMemberJoins, - Payload: client.WelcomeMessagePayload{ - Message: "Hello!", - }, - }) - - // Verify that the API fails with a 400 error - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("create invalid action - wrong trigger type", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-invalid-action-wrong-trigger") - - // Create an action with a wrong trigger type - _, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: client.ActionTypeWelcomeMessage, - TriggerType: "wrong trigger type", - Payload: client.WelcomeMessagePayload{ - Message: "Hello!", - }, - }) - - // Verify that the API fails with a 400 error - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("create action forbidden - not channel admin", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-action-forbidden") - - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - - // Tweak the permissions so that the user is no longer channel admin - e.Permissions.RemovePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId) - - // Attempt to create the action without those permissions - _, err := e.PlaybooksClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: client.ActionTypeWelcomeMessage, - TriggerType: client.TriggerTypeNewMemberJoins, - Payload: client.WelcomeMessagePayload{ - Message: "Hello!", - }, - }) - - // Verify that the API fails with a 403 error - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("create action allowed - not channel admin, but system admin", func(t *testing.T) { - // Create a brand new channel - channel := createNewChannel(t, "create-action-allowed") - - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - - // Tweak the permissions so that the user is no longer channel admin - e.Permissions.RemovePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId) - - // Attempt to create the action as a sysadmin without being a channel admin - actionID, err := e.PlaybooksAdminClient.Actions.Create(context.Background(), channel.Id, client.ChannelActionCreateOptions{ - ChannelID: channel.Id, - Enabled: true, - ActionType: client.ActionTypePromptRunPlaybook, - TriggerType: client.TriggerTypeKeywordsPosted, - Payload: client.PromptRunPlaybookFromKeywordsPayload{ - Keywords: []string{"one", "two"}, - PlaybookID: model.NewId(), - }, - }) - - // Verify that the API succeeds - assert.NoError(t, err) - assert.NotEmpty(t, actionID) - }) -} - -func TestActionList(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - // Create three valid actions - - welcomeActionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{ - ChannelID: e.BasicPublicChannel.Id, - Enabled: true, - ActionType: client.ActionTypeWelcomeMessage, - TriggerType: client.TriggerTypeNewMemberJoins, - Payload: client.WelcomeMessagePayload{ - Message: "msg", - }, - }) - assert.NoError(t, err) - - categorizeActionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{ - ChannelID: e.BasicPublicChannel.Id, - Enabled: true, - ActionType: client.ActionTypeCategorizeChannel, - TriggerType: client.TriggerTypeNewMemberJoins, - Payload: client.CategorizeChannelPayload{ - CategoryName: "category", - }, - }) - assert.NoError(t, err) - - playbookID := model.NewId() - promptActionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{ - ChannelID: e.BasicPublicChannel.Id, - Enabled: true, - ActionType: client.ActionTypePromptRunPlaybook, - TriggerType: client.TriggerTypeKeywordsPosted, - Payload: client.PromptRunPlaybookFromKeywordsPayload{ - Keywords: []string{"one", "two"}, - PlaybookID: playbookID, - }, - }) - assert.NoError(t, err) - - t.Run("view list allowed", func(t *testing.T) { - // List the actions with the default options - actions, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{}) - - // Verify that the API succeeds and that it returns the correct number of actions - assert.NoError(t, err) - assert.Len(t, actions, 3) - - // Verify that the returned actions contain the correct payloads - for _, action := range actions { - switch action.ID { - case welcomeActionID: - var payload client.WelcomeMessagePayload - err = mapstructure.Decode(action.Payload, &payload) - assert.NoError(t, err) - assert.Equal(t, "msg", payload.Message) - - case categorizeActionID: - var payload client.CategorizeChannelPayload - err = mapstructure.Decode(action.Payload, &payload) - assert.NoError(t, err) - assert.Equal(t, "category", payload.CategoryName) - - case promptActionID: - var payload client.PromptRunPlaybookFromKeywordsPayload - err = mapstructure.Decode(action.Payload, &payload) - assert.NoError(t, err) - assert.EqualValues(t, []string{"one", "two"}, payload.Keywords) - assert.Equal(t, playbookID, payload.PlaybookID) - - } - } - }) - - t.Run("view list forbidden", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - - // Tweak the permissions so that the user is no longer channel admin - e.Permissions.RemovePermissionFromRole(model.PermissionReadChannel.Id, model.ChannelUserRoleId) - - // Attempt to list the actions - _, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{}) - - // Verify that the API fails with a 403 error - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) -} - -func TestActionUpdate(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - // Create a valid action - action := client.GenericChannelAction{ - GenericChannelActionWithoutPayload: client.GenericChannelActionWithoutPayload{ - ChannelID: e.BasicPublicChannel.Id, - Enabled: true, - ActionType: client.ActionTypeWelcomeMessage, - TriggerType: client.TriggerTypeNewMemberJoins, - }, - Payload: client.WelcomeMessagePayload{ - Message: "msg", - }, - } - - id, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{ - ChannelID: e.BasicPublicChannel.Id, - Enabled: action.Enabled, - ActionType: action.ActionType, - TriggerType: action.TriggerType, - Payload: action.Payload, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - - action.ID = id - - t.Run("valid update", func(t *testing.T) { - // Make a valid modification - action.Enabled = false - - // Make the Update request - err := e.PlaybooksClient.Actions.Update(context.Background(), action) - - // Verify that the API succeeds - assert.NoError(t, err) - }) - - t.Run("valid update - remove keywords from action", func(t *testing.T) { - payload := client.PromptRunPlaybookFromKeywordsPayload{ - Keywords: []string{"one"}, - PlaybookID: e.BasicPlaybook.ID, - } - - newAction := client.GenericChannelAction{ - GenericChannelActionWithoutPayload: client.GenericChannelActionWithoutPayload{ - ChannelID: e.BasicPublicChannel.Id, - Enabled: true, - ActionType: client.ActionTypePromptRunPlaybook, - TriggerType: client.TriggerTypeKeywordsPosted, - }, - Payload: payload, - } - - // Create an action with keywords and playbook ID - actionID, err := e.PlaybooksClient.Actions.Create(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionCreateOptions{ - ChannelID: newAction.ChannelID, - Enabled: newAction.Enabled, - ActionType: newAction.ActionType, - TriggerType: newAction.TriggerType, - Payload: newAction.Payload, - }) - newAction.ID = actionID - - // Verify that the API succeeds - assert.NoError(t, err) - assert.NotEmpty(t, actionID) - - // Retrieve the newly created action and decode its payload - actions, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{ - TriggerType: client.TriggerTypeKeywordsPosted, - ActionType: client.ActionTypePromptRunPlaybook, - }) - assert.NoError(t, err) - assert.Len(t, actions, 1) - fetchedAction := actions[0] - var fetchedPayload client.PromptRunPlaybookFromKeywordsPayload - err = mapstructure.Decode(fetchedAction.Payload, &fetchedPayload) - assert.NoError(t, err) - - // Verify that the payload of the created action has one keyword - assert.Len(t, fetchedPayload.Keywords, 1) - - // Remove the keywords from the payload in the action - payload.Keywords = []string{} - newAction.Payload = payload - - // Make the Update request with the new action - err = e.PlaybooksClient.Actions.Update(context.Background(), newAction) - - // Verify that the API succeeds - assert.NoError(t, err) - - // Retrieve the updated action and decode its payload - updatedActions, err := e.PlaybooksClient.Actions.List(context.Background(), e.BasicPublicChannel.Id, client.ChannelActionListOptions{ - TriggerType: client.TriggerTypeKeywordsPosted, - ActionType: client.ActionTypePromptRunPlaybook, - }) - assert.NoError(t, err) - assert.Len(t, updatedActions, 1) - updatedAction := updatedActions[0] - var updatedPayload client.PromptRunPlaybookFromKeywordsPayload - err = mapstructure.Decode(updatedAction.Payload, &updatedPayload) - assert.NoError(t, err) - - // Verify that the payload of the updated action has no keywords - assert.Len(t, updatedPayload.Keywords, 0) - }) - - t.Run("invalid update - wrong action type", func(t *testing.T) { - // Make an invalid modification - action.ActionType = "wrong" - - // Make the Update request - err := e.PlaybooksClient.Actions.Update(context.Background(), action) - - // Verify that the API fails with a 400 error - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("invalid update - wrong trigger type", func(t *testing.T) { - // Make an invalid modification - action.TriggerType = "wrong" - - // Make the Update request - err := e.PlaybooksClient.Actions.Update(context.Background(), action) - - // Verify that the API fails with a 400 error - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("invalid update - wrong payload type", func(t *testing.T) { - // Make an invalid modification - action.Payload = client.WelcomeMessagePayload{Message: ""} - - // Make the Update request - err := e.PlaybooksClient.Actions.Update(context.Background(), action) - - // Verify that the API fails with a 400 error - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("update action forbidden - not channel admin", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - - // Tweak the permissions so that the user is no longer channel admin - e.Permissions.RemovePermissionFromRole(model.PermissionManagePublicChannelProperties.Id, model.ChannelUserRoleId) - - // Make a valid modification - action.Enabled = false - - // Make the Update request - err := e.PlaybooksClient.Actions.Update(context.Background(), action) - - // Verify that the API fails with a 403 error - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - -} diff --git a/server/playbooks/server/api_bot_test.go b/server/playbooks/server/api_bot_test.go deleted file mode 100644 index f97063fa940..00000000000 --- a/server/playbooks/server/api_bot_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "encoding/json" - "net/http" - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/stretchr/testify/assert" -) - -func TestTrialLicences(t *testing.T) { - // This test is flaky due to upstream connectivity issues. - t.Skip() - - e := Setup(t) - e.CreateBasic() - - t.Run("request trial license without permissions", func(t *testing.T) { - dialogRequest := model.PostActionIntegrationRequest{ - UserId: e.RegularUser.Id, - PostId: e.BasicPublicChannelPost.Id, - Context: map[string]interface{}{ - "users": 10, - "termsAccepted": true, - "receiveEmailsAccepted": true, - }, - } - dialogRequestBytes, _ := json.Marshal(dialogRequest) - resp, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/bot/notify-admins/button-start-trial", dialogRequestBytes, "") - assert.Error(t, err) - assert.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("request trial license with permissions", func(t *testing.T) { - dialogRequest := model.PostActionIntegrationRequest{ - UserId: e.AdminUser.Id, - PostId: e.BasicPublicChannelPost.Id, - Context: map[string]interface{}{ - "users": 10, - "termsAccepted": true, - "receiveEmailsAccepted": true, - }, - } - dialogRequestBytes, _ := json.Marshal(dialogRequest) - resp, err := e.ServerAdminClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/bot/notify-admins/button-start-trial", dialogRequestBytes, "") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - }) -} diff --git a/server/playbooks/server/api_general_test.go b/server/playbooks/server/api_general_test.go deleted file mode 100644 index 4fa69258821..00000000000 --- a/server/playbooks/server/api_general_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAPI(t *testing.T) { - e := Setup(t) - e.CreateClients() - - t.Run("404", func(t *testing.T) { - resp, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/nothing", nil, "") - assert.Error(t, err) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - }) -} diff --git a/server/playbooks/server/api_graphql_playbooks_test.go b/server/playbooks/server/api_graphql_playbooks_test.go deleted file mode 100644 index 26c8a0aca50..00000000000 --- a/server/playbooks/server/api_graphql_playbooks_test.go +++ /dev/null @@ -1,691 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "strings" - "testing" - - "github.com/graph-gophers/graphql-go" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/api" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGraphQLPlaybooks(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("basic get", func(t *testing.T) { - var pbResultTest struct { - Data struct { - Playbook struct { - ID string - Title string - } - } - } - testPlaybookQuery := ` - query Playbook($id: String!) { - playbook(id: $id) { - id - title - } - } - ` - err := e.PlaybooksAdminClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookQuery, - OperationName: "Playbook", - Variables: map[string]interface{}{"id": e.BasicPlaybook.ID}, - }, &pbResultTest) - require.NoError(t, err) - - assert.Equal(t, e.BasicPlaybook.ID, pbResultTest.Data.Playbook.ID) - assert.Equal(t, e.BasicPlaybook.Title, pbResultTest.Data.Playbook.Title) - }) - - t.Run("list", func(t *testing.T) { - var pbResultTest struct { - Data struct { - Playbooks []struct { - ID string - Title string - } - } - } - testPlaybookQuery := ` - query Playbooks { - playbooks { - id - title - } - } - ` - err := e.PlaybooksAdminClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookQuery, - OperationName: "Playbooks", - }, &pbResultTest) - require.NoError(t, err) - - assert.Len(t, pbResultTest.Data.Playbooks, 3) - }) - - t.Run("playbook mutate", func(t *testing.T) { - newUpdatedTitle := "graphqlmutatetitle" - - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"title": newUpdatedTitle}) - require.NoError(t, err) - - updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - - require.Equal(t, newUpdatedTitle, updatedPlaybook.Title) - }) - - t.Run("update playbook no permissions to broadcast", func(t *testing.T) { - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"broadcastChannelIDs": []string{e.BasicPrivateChannel.Id}}) - require.Error(t, err) - }) - - t.Run("update playbook without modifying broadcast channel ids without permission. should succeed because no modification.", func(t *testing.T) { - e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id} - err := e.PlaybooksAdminClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - - err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"description": "unrelatedupdate"}) - require.NoError(t, err) - }) - - t.Run("update playbook with too many webhoooks", func(t *testing.T) { - urls := []string{} - for i := 0; i < 65; i++ { - urls = append(urls, "http://localhost/"+strconv.Itoa(i)) - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "webhookOnCreationEnabled": true, - "webhookOnCreationURLs": urls, - }) - require.Error(t, err) - }) - - t.Run("change default owner", func(t *testing.T) { - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "defaultOwnerID": e.RegularUser.Id, - }) - require.NoError(t, err) - - err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "defaultOwnerID": e.RegularUserNotInTeam.Id, - }) - require.Error(t, err) - }) - t.Run("checklist with preset values that need to be cleared", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": "", - "assigneeModified": 101, - "state": "Closed", - "stateModified": 102, - "command": "", - "commandLastRun": 103, - "lastSkipped": 104, - "dueDate": 100, - }, - } - - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - }) - - require.NoError(t, err) - - actual := []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "title1", - Description: "description1", - AssigneeID: "", - AssigneeModified: 0, - State: "", - StateModified: 0, - Command: "", - CommandLastRun: 0, - LastSkipped: 0, - DueDate: 100, - }, - }, - }, - } - updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - - require.Equal(t, updatedPlaybook.Checklists, actual) - }) - - t.Run("update playbook with pre-assigned task, valid invite user list, and invitations enabled", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": e.RegularUser.Id, - "assigneeModified": 0, - "state": "", - "stateModified": 0, - "command": "", - "commandLastRun": 0, - "lastSkipped": 0, - "dueDate": 0, - }, - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - "invitedUserIDs": []string{e.RegularUser.Id}, - "inviteUsersEnabled": true, - }) - require.NoError(t, err) - }) - -} -func TestGraphQLUpdatePlaybookFails(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("update playbook fails because size constraints.", func(t *testing.T) { - e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id} - - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": []api.UpdateChecklist{ - { - Title: strings.Repeat("A", (256*1024)+1), - Items: []api.UpdateChecklistItem{}, - }, - }, - }) - require.Error(t, err) - - err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"title": strings.Repeat("A", 1025)}) - require.Error(t, err) - - err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{"description": strings.Repeat("A", 4097)}) - require.Error(t, err) - }) - - t.Run("update playbook with pre-assigned task fails due to disabled invitations", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": e.RegularUser.Id, - "assigneeModified": 0, - "state": "", - "stateModified": 0, - "command": "", - "commandLastRun": 0, - "lastSkipped": 0, - "dueDate": 0, - }, - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - "invitedUserIDs": []string{e.RegularUser.Id}, - }) - require.Error(t, err) - }) - - t.Run("update playbook with pre-assigned task fails due to missing assignee in existing invite user list", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": e.RegularUser.Id, - "assigneeModified": 0, - "state": "", - "stateModified": 0, - "command": "", - "commandLastRun": 0, - "lastSkipped": 0, - "dueDate": 0, - }, - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - "inviteUsersEnabled": true, - }) - require.Error(t, err) - }) - - t.Run("update playbook with pre-assigned task fails due to assignee missing in new invite user list", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": e.RegularUser.Id, - "assigneeModified": 0, - "state": "", - "stateModified": 0, - "command": "", - "commandLastRun": 0, - "lastSkipped": 0, - "dueDate": 0, - }, - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - "invitedUserIDs": []string{e.RegularUser2.Id}, - "inviteUsersEnabled": true, - }) - require.Error(t, err) - }) - - t.Run("update playbook with invite user list fails due to missing a pre-assignee", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": e.RegularUser.Id, - "assigneeModified": 0, - "state": "", - "stateModified": 0, - "command": "", - "commandLastRun": 0, - "lastSkipped": 0, - "dueDate": 0, - }, - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - "invitedUserIDs": []string{e.RegularUser.Id}, - "inviteUsersEnabled": true, - }) - require.NoError(t, err) - - err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "invitedUserIDs": []string{e.RegularUser2.Id}, - }) - require.Error(t, err) - }) - - t.Run("update playbook fails if invitations are getting disabled but there are pre-assigned users", func(t *testing.T) { - items := []map[string]interface{}{ - { - "title": "title1", - "description": "description1", - "assigneeID": e.RegularUser.Id, - "assigneeModified": 0, - "state": "", - "stateModified": 0, - "command": "", - "commandLastRun": 0, - "lastSkipped": 0, - "dueDate": 0, - }, - } - err := gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "checklists": map[string]interface{}{ - "title": "A", - "items": items, - }, - "invitedUserIDs": []string{e.RegularUser.Id}, - "inviteUsersEnabled": true, - }) - require.NoError(t, err) - - err = gqlTestPlaybookUpdate(e, t, e.BasicPlaybook.ID, map[string]interface{}{ - "inviteUsersEnabled": false, - }) - require.Error(t, err) - }) -} - -func TestUpdatePlaybookFavorite(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("favorite", func(t *testing.T) { - isFavorite, err := getPlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID) - require.NoError(t, err) - require.False(t, isFavorite) - - response, err := updatePlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID, true) - require.Empty(t, response.Errors) - require.NoError(t, err) - - isFavorite, err = getPlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID) - require.NoError(t, err) - require.True(t, isFavorite) - }) - - t.Run("unfavorite", func(t *testing.T) { - response, err := updatePlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID, false) - require.Empty(t, response.Errors) - require.NoError(t, err) - - isFavorite, err := getPlaybookFavorite(e.PlaybooksClient, e.BasicPlaybook.ID) - require.NoError(t, err) - require.False(t, isFavorite) - }) - - t.Run("favorite playbook with read access", func(t *testing.T) { - response, err := updatePlaybookFavorite(e.PlaybooksClient2, e.BasicPlaybook.ID, true) - require.Empty(t, response.Errors) - require.NoError(t, err) - - isFavorite, err := getPlaybookFavorite(e.PlaybooksClient2, e.BasicPlaybook.ID) - require.NoError(t, err) - require.True(t, isFavorite) - }) - - t.Run("favorite private playbook no access", func(t *testing.T) { - response, _ := updatePlaybookFavorite(e.PlaybooksClient, e.PrivatePlaybookNoMembers.ID, false) - require.NotEmpty(t, response.Errors) - }) -} - -func updatePlaybookFavorite(c *client.Client, playbookID string, favorite bool) (graphql.Response, error) { - mutation := `mutation UpdatePlaybookFavorite($id: String!, $favorite: Boolean!) { - updatePlaybookFavorite(id: $id, favorite: $favorite) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "UpdatePlaybookFavorite", - Variables: map[string]interface{}{ - "id": playbookID, - "favorite": favorite, - }, - }, &response) - - return response, err -} - -func getPlaybookFavorite(c *client.Client, playbookID string) (bool, error) { - query := ` - query GetPlaybookFavorite($id: String!) { - playbook(id: $id) { - isFavorite - } - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: query, - OperationName: "GetPlaybookFavorite", - Variables: map[string]interface{}{ - "id": playbookID, - }, - }, &response) - - if err != nil { - return false, err - } - if len(response.Errors) > 0 { - return false, fmt.Errorf("error from query %v", response.Errors) - } - - favoriteResponse := struct { - Playbook struct { - IsFavorite bool `json:"isFavorite"` - } `json:"playbook"` - }{} - err = json.Unmarshal(response.Data, &favoriteResponse) - if err != nil { - return false, err - } - return favoriteResponse.Playbook.IsFavorite, nil -} - -func gqlTestPlaybookUpdate(e *TestEnvironment, t *testing.T, playbookID string, updates map[string]interface{}) error { - testPlaybookMutateQuery := ` - mutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) { - updatePlaybook(id: $id, updates: $updates) - } - ` - var response graphql.Response - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookMutateQuery, - OperationName: "UpdatePlaybook", - Variables: map[string]interface{}{"id": playbookID, "updates": updates}, - }, &response) - - if err != nil { - return errors.Wrapf(err, "gqlTestPlaybookUpdate graphql failure") - } - - if len(response.Errors) != 0 { - return errors.Errorf("gqlTestPlaybookUpdate graphql failure %+v", response.Errors) - } - - return err -} - -func TestGraphQLPlaybooksMetrics(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("metrics get", func(t *testing.T) { - var pbResultTest struct { - Data struct { - Playbook struct { - ID string - Title string - Metrics []client.PlaybookMetricConfig - } - } - } - testPlaybookQuery := - ` - query Playbook($id: String!) { - playbook(id: $id) { - id - metrics { - id - title - description - type - target - } - } - } - ` - err := e.PlaybooksAdminClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookQuery, - OperationName: "Playbook", - Variables: map[string]interface{}{"id": e.BasicPlaybook.ID}, - }, &pbResultTest) - require.NoError(t, err) - - require.Len(t, pbResultTest.Data.Playbook.Metrics, len(e.BasicPlaybook.Metrics)) - require.Equal(t, e.BasicPlaybook.Metrics[0].Title, pbResultTest.Data.Playbook.Metrics[0].Title) - require.Equal(t, e.BasicPlaybook.Metrics[0].Type, pbResultTest.Data.Playbook.Metrics[0].Type) - require.Equal(t, e.BasicPlaybook.Metrics[0].Target, pbResultTest.Data.Playbook.Metrics[0].Target) - }) - - t.Run("add metric", func(t *testing.T) { - testAddMetricQuery := ` - mutation AddMetric($playbookID: String!, $title: String!, $description: String!, $type: String!, $target: Int) { - addMetric(playbookID: $playbookID, title: $title, description: $description, type: $type, target: $target) - } - ` - var response graphql.Response - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testAddMetricQuery, - OperationName: "AddMetric", - Variables: map[string]interface{}{ - "playbookID": e.BasicPlaybook.ID, - "title": "New Metric", - "description": "the description", - "type": app.MetricTypeDuration, - }, - }, &response) - require.NoError(t, err) - require.Empty(t, response.Errors) - - updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - - require.Len(t, updatedPlaybook.Metrics, 2) - assert.Equal(t, updatedPlaybook.Metrics[1].Title, "New Metric") - }) - - t.Run("update metric", func(t *testing.T) { - testUpdateMetricQuery := ` - mutation UpdateMetric($id: String!, $title: String, $description: String, $target: Int) { - updateMetric(id: $id, title: $title, description: $description, target: $target) - } - ` - - var response graphql.Response - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testUpdateMetricQuery, - OperationName: "UpdateMetric", - Variables: map[string]interface{}{ - "id": e.BasicPlaybook.Metrics[0].ID, - "title": "Updated Title", - }, - }, &response) - require.NoError(t, err) - require.Empty(t, response.Errors) - - updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - - require.Len(t, updatedPlaybook.Metrics, 2) - assert.Equal(t, "Updated Title", updatedPlaybook.Metrics[0].Title) - }) - - t.Run("delete metric", func(t *testing.T) { - testDeleteMetricQuery := ` - mutation DeleteMetric($id: String!) { - deleteMetric(id: $id) - } - ` - var response graphql.Response - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testDeleteMetricQuery, - OperationName: "DeleteMetric", - Variables: map[string]interface{}{ - "id": e.BasicPlaybook.Metrics[0].ID, - }, - }, &response) - require.NoError(t, err) - require.Empty(t, response.Errors) - - updatedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - - require.Len(t, updatedPlaybook.Metrics, 1) - }) -} - -func gqlTestPlaybookUpdateGuest(e *TestEnvironment, t *testing.T, playbookID string, updates map[string]interface{}) error { - testPlaybookMutateQuery := ` - mutation UpdatePlaybook($id: String!, $updates: PlaybookUpdates!) { - updatePlaybook(id: $id, updates: $updates) - } - ` - var response graphql.Response - err := e.PlaybooksClientGuest.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookMutateQuery, - OperationName: "UpdatePlaybook", - Variables: map[string]interface{}{"id": playbookID, "updates": updates}, - }, &response) - - if err != nil { - return errors.Wrapf(err, "gqlTestPlaybookUpdate graphql failure") - } - - if len(response.Errors) != 0 { - return errors.Errorf("gqlTestPlaybookUpdate graphql failure %+v", response.Errors) - } - - return err -} - -func TestGraphQLPlaybooksGuests(t *testing.T) { - e := Setup(t) - e.SetE20Licence() - e.CreateBasic() - e.CreateGuest() - - t.Run("update playbook guest not member", func(t *testing.T) { - err := gqlTestPlaybookUpdateGuest(e, t, e.BasicPlaybook.ID, map[string]interface{}{"title": "mutated"}) - require.Error(t, err) - }) - - t.Run("basic get guest not member", func(t *testing.T) { - testPlaybookQuery := ` - query Playbook($id: String!) { - playbook(id: $id) { - id - title - } - } - ` - var response graphql.Response - err := e.PlaybooksClientGuest.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookQuery, - OperationName: "Playbook", - Variables: map[string]interface{}{"id": e.BasicPlaybook.ID}, - }, &response) - require.NoError(t, err) - require.NotZero(t, len(response.Errors)) - }) - - t.Run("list guest", func(t *testing.T) { - var pbResultTest struct { - Data struct { - Playbooks []struct { - ID string - Title string - } - } - } - testPlaybookQuery := ` - query Playbooks { - playbooks { - id - title - } - } - ` - err := e.PlaybooksClientGuest.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testPlaybookQuery, - OperationName: "Playbooks", - }, &pbResultTest) - require.NoError(t, err) - - assert.Len(t, pbResultTest.Data.Playbooks, 0) - }) - -} diff --git a/server/playbooks/server/api_graphql_runs_test.go b/server/playbooks/server/api_graphql_runs_test.go deleted file mode 100644 index 0d3ab08420d..00000000000 --- a/server/playbooks/server/api_graphql_runs_test.go +++ /dev/null @@ -1,1345 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "encoding/json" - "fmt" - "sort" - "testing" - - "github.com/graph-gophers/graphql-go" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/app/request" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGraphQLRunList(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("list by participantOrFollower", func(t *testing.T) { - var rResultTest struct { - Data struct { - Runs struct { - TotalCount int - Edges []struct { - Node struct { - ID string - Name string - IsFavorite bool - } - } - } - } - Errors []struct { - Message string - Path string - } - } - testRunsQuery := ` - query Runs($userID: String!) { - runs(participantOrFollowerID: $userID) { - totalCount - edges { - node { - id - name - isFavorite - } - } - } - } - ` - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testRunsQuery, - OperationName: "Runs", - Variables: map[string]interface{}{"userID": "me"}, - }, &rResultTest) - require.NoError(t, err) - - assert.Len(t, rResultTest.Data.Runs.Edges, 1) - assert.Equal(t, 1, rResultTest.Data.Runs.TotalCount) - assert.Equal(t, e.BasicRun.ID, rResultTest.Data.Runs.Edges[0].Node.ID) - assert.Equal(t, e.BasicRun.Name, rResultTest.Data.Runs.Edges[0].Node.Name) - assert.False(t, rResultTest.Data.Runs.Edges[0].Node.IsFavorite) - }) - - t.Run("list by channel", func(t *testing.T) { - var rResultTest struct { - Data struct { - Runs struct { - TotalCount int - Edges []struct { - Node struct { - ID string - Name string - IsFavorite bool - } - } - } - } - Errors []struct { - Message string - Path string - } - } - testRunsQuery := ` - query Runs($channelID: String!) { - runs(channelID: $channelID) { - totalCount - edges { - node { - id - name - isFavorite - } - } - } - } - ` - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testRunsQuery, - OperationName: "Runs", - Variables: map[string]interface{}{"channelID": e.BasicRun.ChannelID}, - }, &rResultTest) - require.NoError(t, err) - - assert.Len(t, rResultTest.Data.Runs.Edges, 1) - assert.Equal(t, 1, rResultTest.Data.Runs.TotalCount) - assert.Equal(t, e.BasicRun.ID, rResultTest.Data.Runs.Edges[0].Node.ID) - assert.Equal(t, e.BasicRun.Name, rResultTest.Data.Runs.Edges[0].Node.Name) - assert.False(t, rResultTest.Data.Runs.Edges[0].Node.IsFavorite) - }) - - // Make more runs in the channel - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - ChannelID: e.BasicRun.ChannelID, - }) - require.NoError(e.T, err) - require.NotNil(e.T, run) - - run2, err2 := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - ChannelID: e.BasicRun.ChannelID, - }) - require.NoError(e.T, err2) - require.NotNil(e.T, run2) - - t.Run("paging", func(t *testing.T) { - var rResultTest struct { - Data struct { - Runs struct { - TotalCount int - Edges []struct { - Node struct { - ID string - Name string - IsFavorite bool - } - } - PageInfo struct { - EndCursor string - HasNextPage bool - } - } - } - Errors []struct { - Message string - Path string - } - } - testRunsQuery := ` - query Runs($channelID: String!, $first: Int, $after: String) { - runs(channelID: $channelID, first: $first, after: $after) { - totalCount - edges { - node { - id - name - isFavorite - } - } - pageInfo { - endCursor - hasNextPage - } - } - } - ` - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testRunsQuery, - OperationName: "Runs", - Variables: map[string]interface{}{"channelID": e.BasicRun.ChannelID, "first": 2}, - }, &rResultTest) - require.NoError(t, err) - - assert.Len(t, rResultTest.Data.Runs.Edges, 2) - assert.Equal(t, 3, rResultTest.Data.Runs.TotalCount) - assert.True(t, rResultTest.Data.Runs.PageInfo.HasNextPage) - assert.Equal(t, "1", rResultTest.Data.Runs.PageInfo.EndCursor) - - err2 := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testRunsQuery, - OperationName: "Runs", - Variables: map[string]interface{}{"channelID": e.BasicRun.ChannelID, "first": 2, "after": "1"}, - }, &rResultTest) - require.NoError(t, err2) - - assert.Len(t, rResultTest.Data.Runs.Edges, 1) - assert.Equal(t, 3, rResultTest.Data.Runs.TotalCount) - assert.False(t, rResultTest.Data.Runs.PageInfo.HasNextPage) - }) -} - -func TestGraphQLChangeRunParticipants(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - user3, _, err := e.ServerAdminClient.CreateUser(context.Background(), &model.User{ - Email: "thirduser@example.com", - Username: "thirduser", - Password: "Password123!", - }) - require.NoError(t, err) - _, _, err = e.ServerAdminClient.AddTeamMember(context.Background(), e.BasicTeam.Id, user3.Id) - require.NoError(t, err) - - userNotInTeam, _, err := e.ServerAdminClient.CreateUser(context.Background(), &model.User{ - Email: "notinteam@example.com", - Username: "notinteam", - Password: "Password123!", - }) - require.NoError(t, err) - - // if the test fits this testTable structure, add it here - // otherwise, create another t.Run() - testCases := []struct { - Name string - PlaybookCreateOptions client.PlaybookCreateOptions - PlaybookRunCreateOptions client.PlaybookRunCreateOptions - ParticipantsToBeAdded []string - ExpectedRunParticipants []string - ExpectedRunFollowers []string - ExpectedChannelMembers []string - UnexpectedChannelMembers []string - }{ - { - Name: "Add 2 participants, actions ON, reporter = owner", - PlaybookCreateOptions: client.PlaybookCreateOptions{ - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: true, - }, - PlaybookRunCreateOptions: client.PlaybookRunCreateOptions{ - OwnerUserID: e.RegularUser.Id, - }, - ParticipantsToBeAdded: []string{e.RegularUser2.Id, user3.Id}, - ExpectedRunParticipants: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - ExpectedRunFollowers: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - ExpectedChannelMembers: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - UnexpectedChannelMembers: []string{}, - }, - { - Name: "Add 1 participant, actions ON, reporter != owner", - PlaybookCreateOptions: client.PlaybookCreateOptions{ - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: true, - }, - PlaybookRunCreateOptions: client.PlaybookRunCreateOptions{ - OwnerUserID: e.RegularUser2.Id, - }, - ParticipantsToBeAdded: []string{user3.Id}, - ExpectedRunParticipants: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - ExpectedRunFollowers: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - ExpectedChannelMembers: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - UnexpectedChannelMembers: []string{}, - }, - { - Name: "Add 2 participants, actions OFF, reporter = owner", - PlaybookCreateOptions: client.PlaybookCreateOptions{ - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: false, - }, - PlaybookRunCreateOptions: client.PlaybookRunCreateOptions{ - OwnerUserID: e.RegularUser.Id, - }, - ParticipantsToBeAdded: []string{e.RegularUser2.Id, user3.Id}, - ExpectedRunParticipants: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - ExpectedRunFollowers: []string{e.RegularUser.Id, e.RegularUser2.Id, user3.Id}, - ExpectedChannelMembers: []string{e.RegularUser.Id}, - UnexpectedChannelMembers: []string{e.RegularUser2.Id, user3.Id}, - }, - { - Name: "Add 2 participants, actions OFF, one from another different team", - PlaybookCreateOptions: client.PlaybookCreateOptions{ - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: false, - }, - PlaybookRunCreateOptions: client.PlaybookRunCreateOptions{ - OwnerUserID: e.RegularUser.Id, - }, - ParticipantsToBeAdded: []string{e.RegularUser2.Id, userNotInTeam.Id}, - ExpectedRunParticipants: []string{e.RegularUser.Id, e.RegularUser2.Id}, - ExpectedRunFollowers: []string{e.RegularUser.Id, e.RegularUser2.Id}, - ExpectedChannelMembers: []string{e.RegularUser.Id}, - UnexpectedChannelMembers: []string{e.RegularUser2.Id}, - }, - } - - for _, tc := range testCases { - t.Run(tc.Name, func(t *testing.T) { - tc.PlaybookCreateOptions.Title = "Playbook title" - tc.PlaybookCreateOptions.TeamID = e.BasicTeam.Id - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), tc.PlaybookCreateOptions) - require.NoError(t, err) - - tc.PlaybookRunCreateOptions.Name = "Run title" - tc.PlaybookRunCreateOptions.TeamID = e.BasicTeam.Id - tc.PlaybookRunCreateOptions.PlaybookID = pbID - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), tc.PlaybookRunCreateOptions) - require.NoError(t, err) - - _, err = addParticipants(e.PlaybooksClient, run.ID, tc.ParticipantsToBeAdded) - require.NoError(t, err) - - // assert participants - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, len(tc.ExpectedRunParticipants)) - for _, ep := range tc.ExpectedRunParticipants { - found := false - for _, p := range run.ParticipantIDs { - if p == ep { - found = true - break - } - } - assert.True(t, found, fmt.Sprintf("Participant %s not found", ep)) - } - // assert followers - meta, err := e.PlaybooksClient.PlaybookRuns.GetMetadata(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, meta.Followers, len(tc.ExpectedRunFollowers)) - for _, ef := range tc.ExpectedRunFollowers { - found := false - for _, f := range meta.Followers { - if f == ef { - found = true - break - } - } - assert.True(t, found, fmt.Sprintf("Follower %s not found", ef)) - } - //assert channel members - for _, ecm := range tc.ExpectedChannelMembers { - member, err := e.A.GetChannelMember(request.EmptyContext(nil), run.ChannelID, ecm) - require.Nil(t, err) - assert.Equal(t, ecm, member.UserId) - } - // assert unexpected channel members - for _, ucm := range tc.UnexpectedChannelMembers { - _, err = e.A.GetChannelMember(request.EmptyContext(nil), run.ChannelID, ucm) - require.Error(t, err) - assert.Contains(t, err.Error(), "No channel member found for that user ID and channel ID") - } - }) - - } - - t.Run("remove two participants", func(t *testing.T) { - response, err := removeParticipants(e.PlaybooksClient, e.BasicRun.ID, []string{e.RegularUser2.Id, user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), e.BasicRun.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 1) - assert.Equal(t, e.RegularUser.Id, run.ParticipantIDs[0]) - - meta, err := e.PlaybooksClient.PlaybookRuns.GetMetadata(context.TODO(), e.BasicRun.ID) - require.NoError(t, err) - require.Len(t, meta.Followers, 1) - assert.Equal(t, e.RegularUser.Id, meta.Followers[0]) - - member, appErr := e.A.GetChannelMember(request.EmptyContext(nil), e.BasicRun.ChannelID, e.RegularUser2.Id) - require.NotNil(t, appErr) - assert.Nil(t, member) - - member, appErr = e.A.GetChannelMember(request.EmptyContext(nil), e.BasicRun.ChannelID, user3.Id) - require.NotNil(t, appErr) - assert.Nil(t, member) - }) - - t.Run("remove two participants without removing from channel members", func(t *testing.T) { - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPlaybookNoMembersNoChannelRemove", - TeamID: e.BasicTeam.Id, - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: true, - RemoveChannelMemberOnRemovedParticipant: false, - }) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: pbID, - }) - require.NoError(t, err) - - response, err := addParticipants(e.PlaybooksClient, run.ID, []string{e.RegularUser2.Id, user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - response, err = removeParticipants(e.PlaybooksClient, run.ID, []string{e.RegularUser2.Id, user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 1) - assert.Equal(t, e.RegularUser.Id, run.ParticipantIDs[0]) - - meta, err := e.PlaybooksClient.PlaybookRuns.GetMetadata(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, meta.Followers, 1) - assert.Equal(t, e.RegularUser.Id, meta.Followers[0]) - - member, appErr := e.A.GetChannelMember(request.EmptyContext(nil), run.ChannelID, e.RegularUser2.Id) - require.Nil(t, appErr) - assert.NotNil(t, member) - - member, appErr = e.A.GetChannelMember(request.EmptyContext(nil), run.ChannelID, user3.Id) - require.Nil(t, appErr) - assert.NotNil(t, member) - }) - - t.Run("add participant to a public run with private channel", func(t *testing.T) { - // This flow test a user with run access (regularUser) that adds another user (regularUser2) - // to a public run with a private channel - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybookNoMembers", - TeamID: e.BasicTeam.Id, - Public: true, - CreatePublicPlaybookRun: false, - CreateChannelMemberOnNewParticipant: true, - }) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: pbID, - }) - require.NoError(t, err) - require.NotNil(t, run) - - response, err := addParticipants(e.PlaybooksClient, run.ID, []string{e.RegularUser2.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 2) - expected := []string{e.RegularUser.Id, e.RegularUser2.Id} - sort.Strings(expected) - sort.Strings(run.ParticipantIDs) - assert.Equal(t, expected, run.ParticipantIDs) - - meta, err := e.PlaybooksClient.PlaybookRuns.GetMetadata(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, meta.Followers, 2) - expected = []string{e.RegularUser.Id, e.RegularUser2.Id} - sort.Strings(expected) - sort.Strings(meta.Followers) - assert.Equal(t, expected, meta.Followers) - - member, appErr := e.A.GetChannelMember(request.EmptyContext(nil), run.ChannelID, e.RegularUser2.Id) - require.Nil(t, appErr) - assert.Equal(t, e.RegularUser2.Id, member.UserId) - }) - - t.Run("join a public run with private channel", func(t *testing.T) { - - // This flow test a user (regularUser2) that wants to participate a public run with a private channel - - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybookNoMembers", - TeamID: e.BasicTeam.Id, - Public: true, - CreatePublicPlaybookRun: false, - CreateChannelMemberOnNewParticipant: true, - }) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: pbID, - }) - require.NoError(t, err) - require.NotNil(t, run) - - response, err := addParticipants(e.PlaybooksClient2, run.ID, []string{e.RegularUser2.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 2) - expected := []string{e.RegularUser.Id, e.RegularUser2.Id} - sort.Strings(expected) - sort.Strings(run.ParticipantIDs) - assert.Equal(t, expected, run.ParticipantIDs) - - meta, err := e.PlaybooksClient.PlaybookRuns.GetMetadata(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, meta.Followers, 2) - expected = []string{e.RegularUser.Id, e.RegularUser2.Id} - sort.Strings(expected) - sort.Strings(meta.Followers) - assert.Equal(t, expected, meta.Followers) - - member, appErr := e.A.GetChannelMember(request.EmptyContext(nil), run.ChannelID, e.RegularUser2.Id) - require.Nil(t, appErr) - assert.Equal(t, e.RegularUser2.Id, member.UserId) - }) - - t.Run("not participant tries to add other participant", func(t *testing.T) { - - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybookNoMembers", - TeamID: e.BasicTeam.Id, - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: true, - }) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: pbID, - }) - require.NoError(t, err) - - // Should not be able to add participants, because is not a participant - response, err := addParticipants(e.PlaybooksClient2, run.ID, []string{user3.Id}) - require.NotEmpty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 1) - - // Should be able to join the run - response, err = addParticipants(e.PlaybooksClient2, run.ID, []string{e.RegularUser2.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 2) - - // After joining the run user should be able to add other participants - response, err = addParticipants(e.PlaybooksClient2, run.ID, []string{user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 3) - }) - - t.Run("leave run", func(t *testing.T) { - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybookNoMembers", - TeamID: e.BasicTeam.Id, - Public: true, - CreatePublicPlaybookRun: true, - CreateChannelMemberOnNewParticipant: true, - RemoveChannelMemberOnRemovedParticipant: true, - }) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: pbID, - }) - require.NoError(t, err) - - // join the run - response, err := addParticipants(e.PlaybooksClient2, run.ID, []string{e.RegularUser2.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 2) - - // leave run - response, err = removeParticipants(e.PlaybooksClient2, run.ID, []string{e.RegularUser2.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 1) - }) - - t.Run("not participant tries to remove participant", func(t *testing.T) { - - pbID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybookNoMembers", - TeamID: e.BasicTeam.Id, - Public: true, - CreatePublicPlaybookRun: true, - }) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: pbID, - }) - require.NoError(t, err) - - // add participant - response, err := addParticipants(e.PlaybooksClient, run.ID, []string{user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 2) - - // try to remove the participant - response, err = removeParticipants(e.PlaybooksClient2, run.ID, []string{user3.Id}) - require.NotEmpty(t, response.Errors) - require.NoError(t, err) - - // join the run - response, err = addParticipants(e.PlaybooksClient2, run.ID, []string{e.RegularUser2.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 3) - - // now should be able to remove participant - response, err = removeParticipants(e.PlaybooksClient2, run.ID, []string{user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), run.ID) - require.NoError(t, err) - require.Len(t, run.ParticipantIDs, 2) - expected := []string{e.RegularUser.Id, e.RegularUser2.Id} - sort.Strings(expected) - sort.Strings(run.ParticipantIDs) - assert.Equal(t, expected, run.ParticipantIDs) - }) -} - -func TestGraphQLChangeRunOwner(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - // create a third user to test change owner - user3, _, err := e.ServerAdminClient.CreateUser(context.Background(), &model.User{ - Email: "thirduser@example.com", - Username: "thirduser", - Password: "Password123!", - }) - require.NoError(t, err) - _, _, err = e.ServerAdminClient.AddTeamMember(context.Background(), e.BasicTeam.Id, user3.Id) - require.NoError(t, err) - - t.Run("set another participant as owner", func(t *testing.T) { - // add another participant - response, err := addParticipants(e.PlaybooksClient, e.BasicRun.ID, []string{user3.Id}) - require.Empty(t, response.Errors) - require.NoError(t, err) - - response, err = changeRunOwner(e.PlaybooksClient, e.BasicRun.ID, user3.Id) - require.Empty(t, response.Errors) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.TODO(), e.BasicRun.ID) - require.NoError(t, err) - require.Equal(t, user3.Id, run.OwnerUserID) - }) - - t.Run("not participant tries to change an owner", func(t *testing.T) { - response, err := changeRunOwner(e.PlaybooksClient2, e.BasicRun.ID, e.RegularUser.Id) - require.NotEmpty(t, response.Errors) - require.NoError(t, err) - }) - - t.Run("set not participant as owner", func(t *testing.T) { - response, err := changeRunOwner(e.PlaybooksClient, e.BasicRun.ID, e.RegularUser2.Id) - require.Empty(t, response.Errors) - require.NoError(t, err) - }) - -} - -func TestSetRunFavorite(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - createRun := func() *client.PlaybookRun { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - return run - } - - t.Run("change favorite to true", func(t *testing.T) { - run := createRun() - - response, err := setRunFavorite(e.PlaybooksClient, run.ID, true) - require.Empty(t, response.Errors) - require.NoError(t, err) - - isFavorite, err := getRunFavorite(e.PlaybooksClient, run.ID) - require.NoError(t, err) - require.True(t, isFavorite) - }) - - t.Run("from true to false returns false", func(t *testing.T) { - run := createRun() - - response, err := setRunFavorite(e.PlaybooksClient, run.ID, true) - require.NoError(t, err) - require.Empty(t, response.Errors) - - isFavorite, err := getRunFavorite(e.PlaybooksClient, run.ID) - require.NoError(t, err) - require.True(t, isFavorite) - - // now that we have this run favorite set to true, if we change it again, - // it should return false - response, err = setRunFavorite(e.PlaybooksClient, run.ID, false) - require.NoError(t, err) - require.Empty(t, response.Errors) - - isFavorite, err = getRunFavorite(e.PlaybooksClient, run.ID) - require.NoError(t, err) - require.False(t, isFavorite) - }) - - t.Run("if already true, should give error", func(t *testing.T) { - run := createRun() - - response, err := setRunFavorite(e.PlaybooksClient, run.ID, true) - require.NoError(t, err) - require.Empty(t, response.Errors) - - isFavorite, err := getRunFavorite(e.PlaybooksClient, run.ID) - require.NoError(t, err) - require.True(t, isFavorite) - - response, err = setRunFavorite(e.PlaybooksClient, run.ID, true) - require.NoError(t, err) - require.NotEmpty(t, response.Errors) - }) - - t.Run("if already false, should give error", func(t *testing.T) { - run := createRun() - - response, err := setRunFavorite(e.PlaybooksClient, run.ID, false) - require.NoError(t, err) - require.NotEmpty(t, response.Errors) - }) - - t.Run("if user is not from the team", func(t *testing.T) { - run := createRun() - - response, err := setRunFavorite(e.PlaybooksClientNotInTeam, run.ID, true) - require.NoError(t, err) - require.NotEmpty(t, response.Errors) - - isFavorite, err := getRunFavorite(e.PlaybooksClient, run.ID) - require.NoError(t, err) - require.False(t, isFavorite) - }) -} - -func TestResolverFavorites(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - createRun := func() *client.PlaybookRun { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - return run - } - - runs := []*client.PlaybookRun{ - createRun(), - createRun(), - } - response, err := setRunFavorite(e.PlaybooksClient, runs[0].ID, true) - require.NoError(t, err) - require.Empty(t, response.Errors) - response, err = setRunFavorite(e.PlaybooksClient, runs[1].ID, true) - require.NoError(t, err) - require.Empty(t, response.Errors) - - favorites, err := getRunFavorites(e.PlaybooksClient) - require.NoError(t, err) - require.True(t, favorites[runs[0].ID]) - require.True(t, favorites[runs[1].ID]) -} - -func TestResolverPlaybooks(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - createRun := func() *client.PlaybookRun { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - return run - } - - runs := []*client.PlaybookRun{ - createRun(), - createRun(), - } - - playbooks, err := getRunPlaybooks(e.PlaybooksClient) - require.NoError(t, err) - require.Equal(t, e.BasicPlaybook.ID, playbooks[runs[0].ID]) - require.Equal(t, e.BasicPlaybook.ID, playbooks[runs[1].ID]) -} - -func TestUpdateRun(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - createRun := func() *client.PlaybookRun { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run with private channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - return run - } - - t.Run("update run summary", func(t *testing.T) { - run := createRun() - require.Equal(t, "", run.Summary) - oldSummaryModifiedAt := run.SummaryModifiedAt - - updates := map[string]interface{}{ - "summary": "The updated summary", - } - response, err := updateRun(e.PlaybooksClient, run.ID, updates) - require.Empty(t, response.Errors) - require.NoError(t, err) - - // Make sure the summary is updated - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, updates["summary"], editedRun.Summary) - require.Greater(t, editedRun.SummaryModifiedAt, oldSummaryModifiedAt) - }) - - t.Run("update run name", func(t *testing.T) { - run := createRun() - require.Equal(t, "Run with private channel", run.Name) - - updates := map[string]interface{}{ - "name": "The updated name", - } - response, err := updateRun(e.PlaybooksClient, run.ID, updates) - require.Empty(t, response.Errors) - require.NoError(t, err) - - // Make sure the name is updated - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, updates["name"], editedRun.Name) - }) - - t.Run("update run actions", func(t *testing.T) { - run := createRun() - - // data previous to update - prevRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - assert.False(t, prevRun.StatusUpdateBroadcastChannelsEnabled) - assert.False(t, prevRun.StatusUpdateBroadcastWebhooksEnabled) - assert.Empty(t, prevRun.WebhookOnStatusUpdateURLs) - assert.Empty(t, prevRun.BroadcastChannelIDs) - assert.True(t, prevRun.CreateChannelMemberOnNewParticipant) - assert.True(t, prevRun.RemoveChannelMemberOnRemovedParticipant) - - //update - updates := map[string]interface{}{ - "statusUpdateBroadcastChannelsEnabled": true, - "statusUpdateBroadcastWebhooksEnabled": true, - "broadcastChannelIDs": []string{e.BasicPublicChannel.Id}, - "webhookOnStatusUpdateURLs": []string{"https://url1", "https://url2"}, - "createChannelMemberOnNewParticipant": false, - "removeChannelMemberOnRemovedParticipant": false, - } - response, err := updateRun(e.PlaybooksClient, run.ID, updates) - require.Empty(t, response.Errors) - require.NoError(t, err) - - // Make sure the action settings are updated - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.True(t, editedRun.StatusUpdateBroadcastChannelsEnabled) - require.True(t, editedRun.StatusUpdateBroadcastWebhooksEnabled) - require.Equal(t, updates["broadcastChannelIDs"], editedRun.BroadcastChannelIDs) - require.Equal(t, updates["webhookOnStatusUpdateURLs"], editedRun.WebhookOnStatusUpdateURLs) - require.False(t, editedRun.CreateChannelMemberOnNewParticipant) - require.False(t, editedRun.RemoveChannelMemberOnRemovedParticipant) - }) - - t.Run("update fails due to lack of permissions", func(t *testing.T) { - run := createRun() - - //update - updates := map[string]interface{}{ - "statusUpdateBroadcastChannelsEnabled": true, - "statusUpdateBroadcastWebhooksEnabled": true, - "broadcastChannelIDs": []string{e.BasicPublicChannel.Id}, - "webhookOnStatusUpdateURLs": []string{"https://url1", "https://url2"}, - "createChannelMemberOnNewParticipant": false, - "removeChannelMemberOnRemovedParticipant": false, - } - response, err := updateRun(e.PlaybooksClient2, run.ID, updates) - require.NotEmpty(t, response.Errors) - require.NoError(t, err) - - // Make sure the action settings are not updated - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.False(t, editedRun.StatusUpdateBroadcastChannelsEnabled) - require.False(t, editedRun.StatusUpdateBroadcastWebhooksEnabled) - assert.Empty(t, editedRun.WebhookOnStatusUpdateURLs) - assert.Empty(t, editedRun.BroadcastChannelIDs) - require.True(t, editedRun.CreateChannelMemberOnNewParticipant) - require.True(t, editedRun.RemoveChannelMemberOnRemovedParticipant) - }) -} - -func TestUpdateRunTaskActions(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("task actions mutation create and update", func(t *testing.T) { - createNewRunWithNoChecklists := func(t *testing.T) *client.PlaybookRun { - t.Helper() - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - return run - } - run := createNewRunWithNoChecklists(t) - // Create a valid, empty checklist - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "First Checklist", - Items: []client.ChecklistItem{{ - Title: "First item", - }}, - }) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, run.Checklists, 1) - require.Len(t, run.Checklists[0].Items, 1) - - // create a new task action - triggerPayload := "{\"keywords\":[\"one\", \"two\"], \"user_ids\":[\"abc\"]}" - actionPayload := "{\"enabled\":false}" - response, err := UpdateRunTaskActions(e.PlaybooksClient, run.ID, 0, 0, &[]app.TaskAction{ - { - Trigger: app.Trigger{ - Type: app.KeywordsByUsersTriggerType, - Payload: triggerPayload, - }, - Actions: []app.Action{{ - Type: app.MarkItemAsDoneActionType, - Payload: actionPayload, - }}, - }, - }) - require.Empty(t, response.Errors) - require.NoError(t, err) - - // Make sure the taskaction is created - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Len(t, editedRun.Checklists[0].Items, 1) - require.Len(t, editedRun.Checklists[0].Items[0].TaskActions, 1) - require.Equal(t, string(app.KeywordsByUsersTriggerType), editedRun.Checklists[0].Items[0].TaskActions[0].Trigger.Type) - require.Equal(t, triggerPayload, editedRun.Checklists[0].Items[0].TaskActions[0].Trigger.Payload) - require.Equal(t, string(app.MarkItemAsDoneActionType), editedRun.Checklists[0].Items[0].TaskActions[0].Actions[0].Type) - require.Equal(t, actionPayload, editedRun.Checklists[0].Items[0].TaskActions[0].Actions[0].Payload) - - // Edit the task action - newTriggerPayload := "{\"keywords\":[\"one\", \"two\", \"edited\"], \"user_ids\":[\"abc\"]}" - response, err = UpdateRunTaskActions(e.PlaybooksClient, run.ID, 0, 0, &[]app.TaskAction{ - { - Trigger: app.Trigger{ - Type: app.KeywordsByUsersTriggerType, - Payload: newTriggerPayload, - }, - Actions: []app.Action{{ - Type: app.MarkItemAsDoneActionType, - Payload: actionPayload, - }}, - }, - }) - require.Empty(t, response.Errors) - require.NoError(t, err) - - // Make sure the taskaction is updated - editedRun, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Len(t, editedRun.Checklists[0].Items, 1) - require.Len(t, editedRun.Checklists[0].Items[0].TaskActions, 1) - require.Equal(t, string(app.KeywordsByUsersTriggerType), editedRun.Checklists[0].Items[0].TaskActions[0].Trigger.Type) - require.Equal(t, newTriggerPayload, editedRun.Checklists[0].Items[0].TaskActions[0].Trigger.Payload) - require.Equal(t, string(app.MarkItemAsDoneActionType), editedRun.Checklists[0].Items[0].TaskActions[0].Actions[0].Type) - require.Equal(t, actionPayload, editedRun.Checklists[0].Items[0].TaskActions[0].Actions[0].Payload) - }) -} - -func TestBadGraphQLRequest(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - testRunsQuery := ` - query Runs($userID: String!) { - runs(participantOrFollowerID: $userID) { - totalCount - these - fields - dont - exist - } - } - ` - var result struct { - Data struct{} - Errors []struct { - Message string - Path string - } - } - err := e.PlaybooksClient.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: testRunsQuery, - OperationName: "Runs", - Variables: map[string]interface{}{"userID": "me"}, - }, &result) - require.NoError(t, err) - require.Len(t, result.Errors, 4) -} - -// AddParticipants adds participants to the run -func addParticipants(c *client.Client, playbookRunID string, userIDs []string) (graphql.Response, error) { - mutation := ` - mutation AddRunParticipants($runID: String!, $userIDs: [String!]!) { - addRunParticipants(runID: $runID, userIDs: $userIDs) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "AddRunParticipants", - Variables: map[string]interface{}{ - "runID": playbookRunID, - "userIDs": userIDs, - }, - }, &response) - - return response, err -} - -// RemoveParticipants removes participants from the run -func removeParticipants(c *client.Client, playbookRunID string, userIDs []string) (graphql.Response, error) { - mutation := ` - mutation RemoveRunParticipants($runID: String!, $userIDs: [String!]!) { - removeRunParticipants(runID: $runID, userIDs: $userIDs) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "RemoveRunParticipants", - Variables: map[string]interface{}{ - "runID": playbookRunID, - "userIDs": userIDs, - }, - }, &response) - - return response, err -} - -// ChangeRunOwner changes run owner -func changeRunOwner(c *client.Client, playbookRunID string, newOwnerID string) (graphql.Response, error) { - mutation := ` - mutation ChangeRunOwner($runID: String!, $ownerID: String!) { - changeRunOwner(runID: $runID, ownerID: $ownerID) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "ChangeRunOwner", - Variables: map[string]interface{}{ - "runID": playbookRunID, - "ownerID": newOwnerID, - }, - }, &response) - - return response, err -} - -func setRunFavorite(c *client.Client, playbookRunID string, fav bool) (graphql.Response, error) { - mutation := `mutation SetRunFavorite($id: String!, $fav: Boolean!) { - setRunFavorite(id: $id, fav: $fav) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "SetRunFavorite", - Variables: map[string]interface{}{ - "id": playbookRunID, - "fav": fav, - }, - }, &response) - - return response, err -} - -func getRunFavorites(c *client.Client) (map[string]bool, error) { - query := ` - query GetFavorites { - runs { - edges { - node{ - id - isFavorite - } - } - } - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: query, - OperationName: "GetFavorites", - }, &response) - - if err != nil { - return nil, err - } - if len(response.Errors) > 0 { - return nil, fmt.Errorf("error from query %v", response.Errors) - } - rawResult := struct { - Runs struct { - Edges []struct { - Node struct { - ID string `json:"id"` - IsFavorite bool `json:"isFavorite"` - } `json:"node"` - } `json:"edges"` - } `json:"runs"` - }{} - err = json.Unmarshal(response.Data, &rawResult) - if err != nil { - return nil, err - } - result := make(map[string]bool) - for _, edges := range rawResult.Runs.Edges { - result[edges.Node.ID] = edges.Node.IsFavorite - } - return result, nil -} - -func getRunPlaybooks(c *client.Client) (map[string]string, error) { - query := ` - query GetRunsWithPlaybooks { - runs { - edges { - node{ - id - playbook { - id - } - } - } - } - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: query, - OperationName: "GetRunsWithPlaybooks", - }, &response) - - if err != nil { - return nil, err - } - if len(response.Errors) > 0 { - return nil, fmt.Errorf("error from query %v", response.Errors) - } - rawResult := struct { - Runs struct { - Edges []struct { - Node struct { - ID string `json:"id"` - Playbook struct { - ID string `json:"id"` - } - } `json:"node"` - } `json:"edges"` - } `json:"runs"` - }{} - err = json.Unmarshal(response.Data, &rawResult) - if err != nil { - return nil, err - } - result := make(map[string]string) - for _, edges := range rawResult.Runs.Edges { - result[edges.Node.ID] = edges.Node.Playbook.ID - } - return result, nil -} - -func getRunFavorite(c *client.Client, playbookRunID string) (bool, error) { - query := ` - query GetRunFavorite($id: String!) { - run(id: $id) { - isFavorite - } - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: query, - OperationName: "GetRunFavorite", - Variables: map[string]interface{}{ - "id": playbookRunID, - }, - }, &response) - - if err != nil { - return false, err - } - if len(response.Errors) > 0 { - return false, fmt.Errorf("error from query %v", response.Errors) - } - - favoriteResponse := struct { - Run struct { - IsFavorite bool `json:"isFavorite"` - } `json:"run"` - }{} - err = json.Unmarshal(response.Data, &favoriteResponse) - if err != nil { - return false, err - } - return favoriteResponse.Run.IsFavorite, nil -} - -// UpdateRun updates the run -func updateRun(c *client.Client, playbookRunID string, updates map[string]interface{}) (graphql.Response, error) { - mutation := ` - mutation UpdateRun($id: String!, $updates: RunUpdates!) { - updateRun(id: $id, updates: $updates) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "UpdateRun", - Variables: map[string]interface{}{ - "id": playbookRunID, - "updates": updates, - }, - }, &response) - - return response, err -} - -func UpdateRunTaskActions(c *client.Client, playbookRunID string, checklistNum float64, itemNum float64, taskActions *[]app.TaskAction) (graphql.Response, error) { - mutation := ` - mutation UpdateRunTaskActions($runID: String!, $checklistNum: Float!, $itemNum: Float!, $taskActions: [TaskActionUpdates!]!) { - updateRunTaskActions(runID: $runID, checklistNum: $checklistNum, itemNum: $itemNum, taskActions: $taskActions) - } - ` - var response graphql.Response - err := c.DoGraphql(context.Background(), &client.GraphQLInput{ - Query: mutation, - OperationName: "UpdateRunTaskActions", - Variables: map[string]interface{}{ - "runID": playbookRunID, - "checklistNum": checklistNum, - "itemNum": itemNum, - "taskActions": taskActions, - }, - }, &response) - - return response, err -} diff --git a/server/playbooks/server/api_playbooks_test.go b/server/playbooks/server/api_playbooks_test.go deleted file mode 100644 index 057d9056896..00000000000 --- a/server/playbooks/server/api_playbooks_test.go +++ /dev/null @@ -1,1543 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "sort" - "strconv" - "strings" - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPlaybooks(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - - t.Run("unlicenced servers can't create a private playbook", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: false, - }) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - assert.Empty(t, id) - }) - - t.Run("create public playbook, unlicensed with zero pre-existing playbooks in the team, should succeed", func(t *testing.T) { - _, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - }) - - t.Run("create public playbook, unlicensed with one pre-existing playbook in the team, should succeed", func(t *testing.T) { - _, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test2", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - }) - - e.SetE10Licence() - - t.Run("create playbook, e10 licenced with one pre-existing playbook in the team, should now succeed", func(t *testing.T) { - _, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test2", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - }) - - t.Run("e10 licenced servers can't create private playbooks", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test3", - TeamID: e.BasicTeam.Id, - Public: false, - }) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - assert.Empty(t, id) - }) - - e.SetE20Licence() - - t.Run("e20 licenced servers can create private playbooks", func(t *testing.T) { - _, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test4", - TeamID: e.BasicTeam.Id, - Public: false, - }) - assert.NoError(t, err) - }) - - t.Run("create playbook with no permissions to broadcast channel", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test5", - TeamID: e.BasicTeam.Id, - BroadcastChannelIDs: []string{e.BasicPrivateChannel.Id}, - }) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - assert.Empty(t, id) - }) - - t.Run("archived playbooks cannot be updated or used to create new runs", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test6 - to be archived", - TeamID: e.BasicTeam.Id, - }) - assert.NoError(t, err) - - playbook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), id) - assert.NoError(t, err) - - // Make sure we /can/ update - playbook.Title = "New Title!" - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *playbook) - assert.NoError(t, err) - - err = e.PlaybooksClient.Playbooks.Archive(context.Background(), id) - assert.NoError(t, err) - - // Test that we cannot update an archived playbook - playbook.Title = "Another title" - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *playbook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - - // Test that we cannot use an archived playbook to start a new run - _, err = e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "test", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: id, - }) - requireErrorWithStatusCode(t, err, http.StatusInternalServerError) - }) - - t.Run("playbooks can be searched by title", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "SearchTest 1 -- all access", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - - id, err = e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "SearchTest 2 -- only regular user access", - TeamID: e.BasicTeam.Id, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - - id, err = e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "SearchTest 3 -- strange string: hümberdångle", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - - id, err = e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "SearchTest 4 -- team 2 string: よこそ", - TeamID: e.BasicTeam2.Id, - Public: true, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - - playbookResults, err := e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "SearchTest", - }) - assert.NoError(t, err) - assert.Equal(t, 4, playbookResults.TotalCount) - - playbookResults, err = e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "SearchTest 2", - }) - assert.NoError(t, err) - assert.Equal(t, 1, playbookResults.TotalCount) - - playbookResults, err = e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "ümber", - }) - assert.NoError(t, err) - assert.Equal(t, 1, playbookResults.TotalCount) - - playbookResults, err = e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "よこそ", - }) - assert.NoError(t, err) - assert.Equal(t, 1, playbookResults.TotalCount) - - playbookResults, err = e.PlaybooksClient2.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "SearchTest", - }) - assert.NoError(t, err) - assert.Equal(t, 2, playbookResults.TotalCount) - - playbookResults, err = e.PlaybooksClient2.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "ümberdå", - }) - assert.NoError(t, err) - assert.Equal(t, 1, playbookResults.TotalCount) - }) - - t.Run("archived playbooks can be retrieved", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "ArchiveTest 1 -- not archived", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - - id, err = e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "ArchiveTest 2 -- archived", - TeamID: e.BasicTeam.Id, - Public: true, - }) - assert.NoError(t, err) - assert.NotEmpty(t, id) - err = e.PlaybooksClient.Playbooks.Archive(context.Background(), id) - assert.NoError(t, err) - - playbookResults, err := e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "ArchiveTest", - }) - assert.NoError(t, err) - assert.Equal(t, 1, playbookResults.TotalCount) - - playbookResults, err = e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 10, client.PlaybookListOptions{ - SearchTeam: "ArchiveTest", - WithArchived: true, - }) - assert.NoError(t, err) - assert.Equal(t, 2, playbookResults.TotalCount) - - }) - - t.Run("create playbook with valid user list", func(t *testing.T) { - _, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "pre-assigned-test1", - TeamID: e.BasicTeam.Id, - Public: true, - InvitedUserIDs: []string{e.RegularUser.Id}, - }) - assert.NoError(t, err) - }) - - t.Run("create playbook with pre-assigned task, valid user list, and invitations enabled", func(t *testing.T) { - _, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "pre-assigned-test2", - TeamID: e.BasicTeam.Id, - Public: true, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - }, - InvitedUserIDs: []string{e.RegularUser.Id}, - InviteUsersEnabled: true, - }) - assert.NoError(t, err) - }) -} - -func TestCreateInvalidPlaybook(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - - t.Run("fails if pre-assigned task is added but invitations are disabled", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "fail-pre-assigned-test1", - TeamID: e.BasicTeam.Id, - Public: true, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - }, - InvitedUserIDs: []string{e.RegularUser.Id}, - }) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - assert.Empty(t, id) - }) - - t.Run("fails if pre-assigned task is added but existing invite user list is missing assignee", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "fail-pre-assigned-test2", - TeamID: e.BasicTeam.Id, - Public: true, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - }, - InviteUsersEnabled: true, - }) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - assert.Empty(t, id) - }) - - t.Run("fails if pre-assigned task is added but assignee is missing in invite user list", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "fail-pre-assigned-test3", - TeamID: e.BasicTeam.Id, - Public: true, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - }, - InvitedUserIDs: []string{e.RegularUser2.Id}, - InviteUsersEnabled: true, - }) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - assert.Empty(t, id) - }) - - t.Run("fails if json is larger than 256K", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: true, - Checklists: []client.Checklist{ - { - Title: "checklist", - Items: []client.ChecklistItem{ - {Description: strings.Repeat("A", (256*1024)+1)}, - }, - }, - }, - }) - requireErrorWithStatusCode(t, err, http.StatusInternalServerError) - assert.Empty(t, id) - }) - - t.Run("fails if title is longer than 1024", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: strings.Repeat("A", 1025), - TeamID: e.BasicTeam.Id, - Public: true, - }) - requireErrorWithStatusCode(t, err, http.StatusInternalServerError) - assert.Empty(t, id) - }) -} - -func TestPlaybooksRetrieval(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("get playbook", func(t *testing.T) { - result, err := e.PlaybooksClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - assert.Equal(t, result.ID, e.BasicPlaybook.ID) - }) - - t.Run("get multiple playbooks", func(t *testing.T) { - actualList, err := e.PlaybooksClient.Playbooks.List(context.Background(), e.BasicTeam.Id, 0, 100, client.PlaybookListOptions{}) - require.NoError(t, err) - assert.Greater(t, len(actualList.Items), 0) - }) -} - -func TestPlaybookUpdate(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("update playbook properties", func(t *testing.T) { - e.BasicPlaybook.Description = "This is the updated description" - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - }) - - t.Run("update playbook no permissions to broadcast", func(t *testing.T) { - e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id} - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("update playbook without chaning existing broadcast channel", func(t *testing.T) { - e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id} - err := e.PlaybooksAdminClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - - e.BasicPlaybook.Description = "unrelated update" - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - }) - - t.Run("fails if pre-assigned task is added but invitations are disabled", func(t *testing.T) { - e.BasicPlaybook.InvitedUserIDs = []string{e.RegularUser2.Id} - e.BasicPlaybook.Checklists = []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser2.Id, - }, - }, - }, - } - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("fails if pre-assigned task is added but existing invite user list is missing assignee", func(t *testing.T) { - e.BasicPlaybook.InviteUsersEnabled = true - e.BasicPlaybook.Checklists = []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - } - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("fails if pre-assigned task is added but assignee is missing in updated invite user list", func(t *testing.T) { - e.BasicPlaybook.InviteUsersEnabled = true - e.BasicPlaybook.InvitedUserIDs = []string{e.RegularUser2.Id} - e.BasicPlaybook.Checklists = []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - } - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("update playbook with pre-assigned task, valid invite user list, and invitations enabled", func(t *testing.T) { - e.BasicPlaybook.InviteUsersEnabled = true - e.BasicPlaybook.InvitedUserIDs = []string{e.RegularUser.Id} - e.BasicPlaybook.Checklists = []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - AssigneeID: e.RegularUser.Id, - }, - }, - }, - } - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - }) - - t.Run("update playbook with valid invite user list", func(t *testing.T) { - e.BasicPlaybook.InvitedUserIDs = append(e.BasicPlaybook.InvitedUserIDs, e.RegularUser2.Id) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - }) - - t.Run("fails if invite user list is updated but is missing pre-assigned users", func(t *testing.T) { - e.BasicPlaybook.InvitedUserIDs = []string{} - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("fails if invitations are getting disabled but there are pre-assigned users", func(t *testing.T) { - e.BasicPlaybook.InviteUsersEnabled = false - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) - - t.Run("update playbook with too many webhoooks", func(t *testing.T) { - urls := []string{} - for i := 0; i < 65; i++ { - urls = append(urls, "http://localhost/"+strconv.Itoa(i)) - } - e.BasicPlaybook.WebhookOnCreationEnabled = true - e.BasicPlaybook.WebhookOnCreationURLs = urls - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusBadRequest) - }) -} - -func TestPlaybookUpdateCrossTeam(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("update playbook properties not in team public playbook", func(t *testing.T) { - e.BasicPlaybook.Description = "This is the updated description" - err := e.PlaybooksClientNotInTeam.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("lost acccess to playbook", func(t *testing.T) { - e.BasicPlaybook.Description = "This is the updated description" - e.BasicPlaybook.Members = append(e.BasicPlaybook.Members, - client.PlaybookMember{ - UserID: e.RegularUserNotInTeam.Id, - Roles: []string{app.PlaybookRoleMember}, - }) - uperr := e.PlaybooksAdminClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, uperr) - err := e.PlaybooksClientNotInTeam.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("update playbook properties in team public playbook", func(t *testing.T) { - e.BasicPlaybook.Description = "This is the updated description" - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - }) -} - -func TestPlaybooksSort(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - e.SetE20Licence() - - playbookAID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "A", - TeamID: e.BasicTeam.Id, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - }, - }, - }, - }, - }) - require.NoError(t, err) - playbookBID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "B", - TeamID: e.BasicTeam.Id, - Checklists: []client.Checklist{ - { - Title: "B", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - }, - { - Title: "Do this2", - }, - }, - }, - { - Title: "B", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - }, - { - Title: "Do this2", - }, - }, - }, - }, - }) - require.NoError(t, err) - _, err = e.PlaybooksAdminClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Some Run", - OwnerUserID: e.AdminUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookBID, - }) - require.NoError(t, err) - playbookCID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "C", - TeamID: e.BasicTeam.Id, - Checklists: []client.Checklist{ - { - Title: "C", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - }, - { - Title: "Do this2", - }, - { - Title: "Do this3", - }, - }, - }, - { - Title: "C", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - }, - { - Title: "Do this2", - }, - { - Title: "Do this3", - }, - }, - }, - { - Title: "C", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - }, - { - Title: "Do this2", - }, - { - Title: "Do this3", - }, - }, - }, - }, - }) - require.NoError(t, err) - _, err = e.PlaybooksAdminClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Some Run", - OwnerUserID: e.AdminUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookCID, - }) - require.NoError(t, err) - _, err = e.PlaybooksAdminClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Some Run", - OwnerUserID: e.AdminUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookCID, - }) - require.NoError(t, err) - - testData := []struct { - testName string - sortField client.Sort - sortDirection client.SortDirection - expectedList []string - expectedErr error - expectedStatusCode int - }{ - { - testName: "get playbooks with invalid sort field", - sortField: "test", - sortDirection: "", - expectedList: nil, - expectedErr: errors.New("bad parameter 'sort' (test)"), - expectedStatusCode: http.StatusBadRequest, - }, - { - testName: "get playbooks with invalid sort direction", - sortField: "", - sortDirection: "test", - expectedList: nil, - expectedErr: errors.New("bad parameter 'direction' (test)"), - expectedStatusCode: http.StatusBadRequest, - }, - { - testName: "get playbooks with no sort fields", - sortField: "", - sortDirection: "", - expectedList: []string{playbookAID, playbookBID, playbookCID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=title direction=asc", - sortField: client.SortByTitle, - sortDirection: "asc", - expectedList: []string{playbookAID, playbookBID, playbookCID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=title direction=desc", - sortField: client.SortByTitle, - sortDirection: "desc", - expectedList: []string{playbookCID, playbookBID, playbookAID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=stages direction=asc", - sortField: client.SortByStages, - sortDirection: "asc", - expectedList: []string{playbookAID, playbookBID, playbookCID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=stages direction=desc", - sortField: client.SortByStages, - sortDirection: "desc", - expectedList: []string{playbookCID, playbookBID, playbookAID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=steps direction=asc", - sortField: client.SortBySteps, - sortDirection: "asc", - expectedList: []string{playbookAID, playbookBID, playbookCID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=steps direction=desc", - sortField: client.SortBySteps, - sortDirection: "desc", - expectedList: []string{playbookCID, playbookBID, playbookAID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=runs direction=asc", - sortField: client.SortByRuns, - sortDirection: "asc", - expectedList: []string{playbookAID, playbookBID, playbookCID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with sort=runs direction=desc", - sortField: client.SortByRuns, - sortDirection: "desc", - expectedList: []string{playbookCID, playbookBID, playbookAID}, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - } - - for _, data := range testData { - t.Run(data.testName, func(t *testing.T) { - actualList, err := e.PlaybooksAdminClient.Playbooks.List(context.Background(), e.BasicTeam.Id, 0, 100, client.PlaybookListOptions{ - Sort: data.sortField, - Direction: data.sortDirection, - }) - - if data.expectedErr == nil { - require.NoError(t, err) - require.Equal(t, len(data.expectedList), len(actualList.Items)) - for i, item := range actualList.Items { - assert.Equal(t, data.expectedList[i], item.ID) - } - } else { - requireErrorWithStatusCode(t, err, data.expectedStatusCode) - assert.Contains(t, err.Error(), data.expectedErr.Error()) - require.Empty(t, actualList) - } - }) - } - -} - -func TestPlaybooksPaging(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - e.SetE20Licence() - - _, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: true, - }) - require.NoError(t, err) - _, err = e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test2", - TeamID: e.BasicTeam.Id, - Public: true, - }) - require.NoError(t, err) - _, err = e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test3", - TeamID: e.BasicTeam.Id, - Public: true, - }) - require.NoError(t, err) - - testData := []struct { - testName string - page int - perPage int - expectedErr error - expectedStatusCode int - expectedTotalCount int - expectedPageCount int - expectedHasMore bool - expectedNumItems int - }{ - { - testName: "get playbooks with negative page values", - page: -1, - perPage: -1, - expectedErr: errors.New("bad parameter"), - expectedStatusCode: http.StatusBadRequest, - }, - { - testName: "get playbooks with page=0 per_page=0", - page: 0, - perPage: 0, - expectedTotalCount: 3, - expectedPageCount: 1, - expectedHasMore: false, - expectedNumItems: 3, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with page=0 per_page=3", - page: 0, - perPage: 3, - expectedTotalCount: 3, - expectedPageCount: 1, - expectedHasMore: false, - expectedNumItems: 3, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with page=0 per_page=2", - page: 0, - perPage: 2, - expectedTotalCount: 3, - expectedPageCount: 2, - expectedHasMore: true, - expectedNumItems: 2, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with page=1 per_page=2", - page: 1, - perPage: 2, - expectedTotalCount: 3, - expectedPageCount: 2, - expectedHasMore: false, - expectedNumItems: 1, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with page=2 per_page=2", - page: 2, - perPage: 2, - expectedTotalCount: 3, - expectedPageCount: 2, - expectedHasMore: false, - expectedNumItems: 0, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - { - testName: "get playbooks with page=9999 per_page=2", - page: 9999, - perPage: 2, - expectedTotalCount: 3, - expectedPageCount: 2, - expectedHasMore: false, - expectedNumItems: 0, - expectedErr: nil, - expectedStatusCode: http.StatusOK, - }, - } - - for _, data := range testData { - t.Run(data.testName, func(t *testing.T) { - actualList, err := e.PlaybooksAdminClient.Playbooks.List(context.Background(), e.BasicTeam.Id, data.page, data.perPage, client.PlaybookListOptions{}) - - if data.expectedErr == nil { - require.NoError(t, err) - assert.Equal(t, data.expectedTotalCount, actualList.TotalCount) - assert.Equal(t, data.expectedPageCount, actualList.PageCount) - assert.Equal(t, data.expectedHasMore, actualList.HasMore) - assert.Len(t, actualList.Items, data.expectedNumItems) - } else { - requireErrorWithStatusCode(t, err, data.expectedStatusCode) - assert.Contains(t, err.Error(), data.expectedErr.Error()) - require.Empty(t, actualList) - } - }) - } -} - -func getPlaybookIDsList(playbooks []client.Playbook) []string { - ids := []string{} - for _, pb := range playbooks { - ids = append(ids, pb.ID) - } - - return ids -} - -func TestPlaybooksPermissions(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("test no permissions to create", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.RemovePermissionFromRole(model.PermissionPublicPlaybookCreate.Id, model.TeamUserRoleId) - e.Permissions.RemovePermissionFromRole(model.PermissionPrivatePlaybookCreate.Id, model.TeamUserRoleId) - - resultPublic, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: true, - }) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - assert.Equal(t, "", resultPublic) - - resultPrivate, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test2", - TeamID: e.BasicTeam.Id, - Public: false, - }) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - assert.Equal(t, "", resultPrivate) - - }) - - t.Run("permissions to get private playbook", func(t *testing.T) { - _, err := e.PlaybooksClient2.Playbooks.Get(context.Background(), e.BasicPrivatePlaybook.ID) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("list playbooks", func(t *testing.T) { - t.Run("user in private", func(t *testing.T) { - results, err := e.PlaybooksClient.Playbooks.List(context.Background(), e.BasicTeam.Id, 0, 100, client.PlaybookListOptions{}) - require.NoError(t, err) - - expectedIDs := getPlaybookIDsList([]client.Playbook{*e.BasicPlaybook, *e.BasicPrivatePlaybook}) - - assert.ElementsMatch(t, expectedIDs, getPlaybookIDsList(results.Items)) - }) - - t.Run("user in private list all", func(t *testing.T) { - results, err := e.PlaybooksClient.Playbooks.List(context.Background(), "", 0, 100, client.PlaybookListOptions{}) - require.NoError(t, err) - - expectedIDs := getPlaybookIDsList([]client.Playbook{*e.BasicPlaybook, *e.BasicPrivatePlaybook}) - - assert.ElementsMatch(t, expectedIDs, getPlaybookIDsList(results.Items)) - }) - - t.Run("user not in private", func(t *testing.T) { - results, err := e.PlaybooksClient2.Playbooks.List(context.Background(), e.BasicTeam.Id, 0, 100, client.PlaybookListOptions{}) - require.NoError(t, err) - - expectedIDs := getPlaybookIDsList([]client.Playbook{*e.BasicPlaybook}) - - assert.ElementsMatch(t, expectedIDs, getPlaybookIDsList(results.Items)) - }) - - t.Run("user not in private list all", func(t *testing.T) { - results, err := e.PlaybooksClient2.Playbooks.List(context.Background(), "", 0, 100, client.PlaybookListOptions{}) - require.NoError(t, err) - - expectedIDs := getPlaybookIDsList([]client.Playbook{*e.BasicPlaybook}) - - assert.ElementsMatch(t, expectedIDs, getPlaybookIDsList(results.Items)) - }) - - t.Run("not in team", func(t *testing.T) { - _, err := e.PlaybooksClientNotInTeam.Playbooks.List(context.Background(), e.BasicTeam.Id, 0, 100, client.PlaybookListOptions{}) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - }) - - t.Run("update playbook", func(t *testing.T) { - e.BasicPlaybook.Description = "updated" - - t.Run("user not in private", func(t *testing.T) { - err := e.PlaybooksClient2.Playbooks.Update(context.Background(), *e.BasicPrivatePlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("public with no permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.RemovePermissionFromRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("public with permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - assert.NoError(t, err) - }) - - e.BasicPrivatePlaybook.Description = "updated" - t.Run("private with no permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.RemovePermissionFromRole(model.PermissionPrivatePlaybookManageProperties.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPrivatePlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("private with permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPrivatePlaybookManageProperties.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPrivatePlaybook) - assert.NoError(t, err) - }) - - }) - - oldMembers := e.BasicPlaybook.Members - - t.Run("update playbook members", func(t *testing.T) { - e.BasicPlaybook.Members = append(e.BasicPlaybook.Members, client.PlaybookMember{UserID: "testuser", Roles: []string{model.PlaybookMemberRoleId}}) - - t.Run("without permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - e.Permissions.RemovePermissionFromRole(model.PermissionPublicPlaybookManageMembers.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("with permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageMembers.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - assert.NoError(t, err) - }) - - e.BasicPlaybook.Members = []client.PlaybookMember{} - t.Run("with permissions removal", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageMembers.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - assert.NoError(t, err) - }) - }) - - e.BasicPlaybook.Members = oldMembers - err := e.PlaybooksAdminClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - - t.Run("update playbook roles", func(t *testing.T) { - e.BasicPlaybook.Members[len(e.BasicPlaybook.Members)-1].Roles = append(e.BasicPlaybook.Members[len(e.BasicPlaybook.Members)-1].Roles, model.PlaybookAdminRoleId) - - t.Run("without permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageMembers.Id, model.PlaybookMemberRoleId) - e.Permissions.RemovePermissionFromRole(model.PermissionPublicPlaybookManageRoles.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("with permissions", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageProperties.Id, model.PlaybookMemberRoleId) - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageMembers.Id, model.PlaybookMemberRoleId) - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookManageRoles.Id, model.PlaybookMemberRoleId) - - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - assert.NoError(t, err) - }) - }) - -} - -func TestPlaybooksConversions(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("public to private conversion", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - e.SetE20Licence() - }() - e.Permissions.RemovePermissionFromRole(model.PermissionPublicPlaybookMakePrivate.Id, model.PlaybookMemberRoleId) - - e.BasicPlaybook.Public = false - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - - e.Permissions.AddPermissionToRole(model.PermissionPublicPlaybookMakePrivate.Id, model.PlaybookMemberRoleId) - - t.Run("E0", func(t *testing.T) { - e.RemoveLicence() - - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("E10", func(t *testing.T) { - e.SetE10Licence() - - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("E20", func(t *testing.T) { - e.SetE20Licence() - - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - }) - }) - - t.Run("private to public conversion", func(t *testing.T) { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - e.Permissions.RemovePermissionFromRole(model.PermissionPrivatePlaybookMakePublic.Id, model.PlaybookMemberRoleId) - - e.BasicPlaybook.Public = true - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - - e.Permissions.AddPermissionToRole(model.PermissionPrivatePlaybookMakePublic.Id, model.PlaybookMemberRoleId) - - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - - }) -} - -func TestPlaybooksImportExport(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - e.CreateBasicPublicPlaybook() - - t.Run("Export", func(t *testing.T) { - result, err := e.PlaybooksClient.Playbooks.Export(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - var exportedPlaybook app.Playbook - err = json.Unmarshal(result, &exportedPlaybook) - require.NoError(t, err) - assert.Equal(t, e.BasicPlaybook.Title, exportedPlaybook.Title) - }) - - t.Run("Import", func(t *testing.T) { - result, err := e.PlaybooksClient.Playbooks.Export(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - newPlaybookID, err := e.PlaybooksClient.Playbooks.Import(context.Background(), result, e.BasicTeam.Id) - require.NoError(t, err) - newPlaybook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), newPlaybookID) - require.NoError(t, err) - - assert.Equal(t, e.BasicPlaybook.Title, newPlaybook.Title) - assert.NotEqual(t, e.BasicPlaybook.ID, newPlaybook.ID) - }) -} - -func TestPlaybooksDuplicate(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - e.SetE20Licence() - e.CreateBasicPlaybook() - - t.Run("Duplicate", func(t *testing.T) { - newID, err := e.PlaybooksClient.Playbooks.Duplicate(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - require.NotEqual(t, e.BasicPlaybook.ID, newID) - - duplicatedPlaybook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), newID) - require.NoError(t, err) - - assert.Equal(t, "Copy of "+e.BasicPlaybook.Title, duplicatedPlaybook.Title) - assert.Equal(t, e.BasicPlaybook.Description, duplicatedPlaybook.Description) - assert.Equal(t, e.BasicPlaybook.TeamID, duplicatedPlaybook.TeamID) - }) -} - -func TestAddPostToTimeline(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - dialogRequest := model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: fmt.Sprintf(`{"post_id": "%s"}`, e.BasicPublicChannelPost.Id), - Submission: map[string]interface{}{ - app.DialogFieldPlaybookRunKey: e.BasicRun.ID, - app.DialogFieldSummary: "a summary", - }, - } - - // Build the payload for the dialog - dialogRequestBytes, err := json.Marshal(dialogRequest) - require.NoError(t, err) - - t.Run("unlicensed server", func(t *testing.T) { - // Make sure there is no license - e.RemoveLicence() - - // Post the request with the dialog payload and verify it is not allowed - resp, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/runs/add-to-timeline-dialog", dialogRequestBytes, "") - require.Error(t, err) - require.Equal(t, http.StatusForbidden, resp.StatusCode) - }) - - t.Run("E10 server", func(t *testing.T) { - // Set an E10 license - e.SetE10Licence() - - // Post the request with the dialog payload and verify it is allowed - _, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/runs/add-to-timeline-dialog", dialogRequestBytes, "") - require.NoError(t, err) - }) - - t.Run("E20 server", func(t *testing.T) { - // Set an E20 license - e.SetE20Licence() - - // Post the request with the dialog payload and verify it is allowed - _, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/runs/add-to-timeline-dialog", dialogRequestBytes, "") - require.NoError(t, err) - }) -} - -func TestPlaybookStats(t *testing.T) { - e := Setup(t) - e.CreateClients() - e.CreateBasicServer() - e.SetE20Licence() - e.CreateBasicPlaybook() - - t.Run("unlicensed server", func(t *testing.T) { - // Make sure there is no license - e.RemoveLicence() - - // Verify that retrieving stats is not allowed - _, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), e.BasicPlaybook.ID) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("E10 server", func(t *testing.T) { - // Set an E10 license - e.SetE10Licence() - - // Verify that ertrieving stats is not allowed - _, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), e.BasicPlaybook.ID) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("E20 server", func(t *testing.T) { - // Set an E20 license - e.SetE20Licence() - - // Verify that retrieving stats is allowed - _, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - }) -} - -func TestPlaybookGetAutoFollows(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - p1ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: true, - }) - require.NoError(t, err) - err = e.PlaybooksClient.Playbooks.AutoFollow(context.Background(), p1ID, e.RegularUser.Id) - require.NoError(t, err) - - p2ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test2", - TeamID: e.BasicTeam.Id, - Public: true, - }) - require.NoError(t, err) - err = e.PlaybooksClient.Playbooks.AutoFollow(context.Background(), p2ID, e.RegularUser.Id) - require.NoError(t, err) - err = e.PlaybooksClient2.Playbooks.AutoFollow(context.Background(), p2ID, e.RegularUser2.Id) - require.NoError(t, err) - - p3ID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test3", - TeamID: e.BasicTeam2.Id, - Public: false, - }) - require.NoError(t, err) - - testCases := []struct { - testName string - playbookID string - expectedError int - expectedTotalCount int - expectedFollowers []string - client *client.Client - }{ - { - testName: "Public playbook without followers", - client: e.PlaybooksClient, - playbookID: e.BasicPlaybook.ID, - expectedTotalCount: 0, - expectedFollowers: []string{}, - }, - { - testName: "Private playbook without followers", - client: e.PlaybooksClient, - playbookID: e.BasicPrivatePlaybook.ID, - expectedTotalCount: 0, - expectedFollowers: []string{}, - }, - { - testName: "Public playbook with 1 follower", - client: e.PlaybooksClient, - playbookID: p1ID, - expectedTotalCount: 1, - expectedFollowers: []string{e.RegularUser.Id}, - }, - { - testName: "Public playbook with 2 followers", - client: e.PlaybooksClient, - playbookID: p2ID, - expectedTotalCount: 2, - expectedFollowers: []string{e.RegularUser.Id, e.RegularUser2.Id}, - }, - { - testName: "Playbook does not exist", - client: e.PlaybooksClient, - playbookID: "fake playbook id", - expectedError: http.StatusNotFound, - }, - { - testName: "Playbook belongs to other team", - client: e.PlaybooksClient, - playbookID: p3ID, - expectedError: http.StatusForbidden, - }, - { - testName: "Playbook in same team but user lacks permission", - client: e.PlaybooksClient2, - playbookID: e.BasicPrivatePlaybook.ID, - expectedError: http.StatusForbidden, - }, - } - - for _, c := range testCases { - t.Run(c.testName, func(t *testing.T) { - res, err := c.client.Playbooks.GetAutoFollows(context.Background(), c.playbookID) - if c.expectedError != 0 { - requireErrorWithStatusCode(t, err, c.expectedError) - } else { - require.NoError(t, err) - require.Equal(t, c.expectedTotalCount, len(res)) - - sort.Strings(res) - sort.Strings(c.expectedFollowers) - require.Equal(t, c.expectedFollowers, res) - } - }) - - } - -} - -func TestPlaybookChecklistCleanup(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("update playbook", func(t *testing.T) { - e.BasicPlaybook.Checklists = []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "title1", - AssigneeModified: 101, - State: "Closed", - StateModified: 102, - CommandLastRun: 103, - }, - }, - }, - } - err := e.PlaybooksClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.NoError(t, err) - actual := []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "title1", - AssigneeModified: 0, - State: "", - StateModified: 0, - CommandLastRun: 0, - }, - }, - }, - } - require.Equal(t, pb.Checklists, actual) - }) - - t.Run("create playbook", func(t *testing.T) { - id, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test1", - TeamID: e.BasicTeam.Id, - Public: true, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "title1", - AssigneeModified: 101, - State: "Closed", - StateModified: 102, - CommandLastRun: 103, - }, - }, - }, - }}) - require.NoError(t, err) - pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), id) - require.NoError(t, err) - actual := []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "title1", - AssigneeModified: 0, - State: "", - StateModified: 0, - CommandLastRun: 0, - }, - }, - }, - } - require.Equal(t, pb.Checklists, actual) - }) -} - -func TestPlaybooksGuests(t *testing.T) { - e := Setup(t) - e.SetE20Licence() - e.CreateBasic() - e.CreateGuest() - - t.Run("guests can't create playbooks", func(t *testing.T) { - _, err := e.PlaybooksClientGuest.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "test4", - TeamID: e.BasicTeam.Id, - Public: false, - }) - assert.Error(t, err) - }) - - t.Run("get playbook guest", func(t *testing.T) { - _, err := e.PlaybooksClientGuest.Playbooks.Get(context.Background(), e.BasicPlaybook.ID) - require.Error(t, err) - }) - - t.Run("update playbook properties", func(t *testing.T) { - e.BasicPlaybook.Description = "This is the updated description" - err := e.PlaybooksClientGuest.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.Error(t, err) - }) -} diff --git a/server/playbooks/server/api_runs_test.go b/server/playbooks/server/api_runs_test.go deleted file mode 100644 index 31cd4fcb406..00000000000 --- a/server/playbooks/server/api_runs_test.go +++ /dev/null @@ -1,1767 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "encoding/json" - "net/http" - "strings" - "testing" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRunCreation(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - incompletePlaybookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPlaybook", - TeamID: e.BasicTeam.Id, - Public: true, - Members: []client.PlaybookMember{ - {UserID: e.RegularUser.Id, Roles: []string{app.PlaybookRoleMember}}, - {UserID: e.AdminUser.Id, Roles: []string{app.PlaybookRoleAdmin, app.PlaybookRoleMember}}, - }, - ChannelMode: client.PlaybookRunLinkExistingChannel, - ChannelID: "", - }) - require.NoError(t, err) - - t.Run("dialog requests", func(t *testing.T) { - for name, tc := range map[string]struct { - dialogRequest model.SubmitDialogRequest - expected func(t *testing.T, result *http.Response, err error) - permissionsPrep func() - }{ - "valid": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.BasicPlaybook.ID, - app.DialogFieldNameKey: "run number 1", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - require.NoError(t, err) - assert.Equal(t, http.StatusCreated, result.StatusCode) - }, - }, - "valid from post": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: `{"post_id": "` + e.BasicPublicChannelPost.Id + `"}`, - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.BasicPlaybook.ID, - app.DialogFieldNameKey: "run number 1", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - require.NoError(t, err) - assert.Equal(t, http.StatusCreated, result.StatusCode) - }, - }, - "somone else's user id": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.AdminUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.BasicPlaybook.ID, - app.DialogFieldNameKey: "somerun", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - assert.Equal(t, http.StatusBadRequest, result.StatusCode) - }, - }, - "missing playbook id": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: "noesnotexist", - app.DialogFieldNameKey: "somerun", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - assert.Equal(t, http.StatusInternalServerError, result.StatusCode) - }, - }, - "no permissions to postid": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: `{"post_id": "` + e.BasicPrivateChannelPost.Id + `"}`, - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.BasicPlaybook.ID, - app.DialogFieldNameKey: "no permissions", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - assert.Equal(t, http.StatusInternalServerError, result.StatusCode) - }, - }, - "no permissions to playbook": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.PrivatePlaybookNoMembers.ID, - app.DialogFieldNameKey: "not happening", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - assert.Equal(t, http.StatusForbidden, result.StatusCode) - }, - }, - "no permissions to private channels": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.BasicPlaybook.ID, - app.DialogFieldNameKey: "run number 1", - }, - }, - permissionsPrep: func() { - e.Permissions.RemovePermissionFromRole(model.PermissionCreatePrivateChannel.Id, model.TeamUserRoleId) - }, - expected: func(t *testing.T, result *http.Response, err error) { - require.Error(t, err) - assert.Equal(t, http.StatusForbidden, result.StatusCode) - }, - }, - "request userid doesn't match": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.AdminUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: e.BasicPlaybook.ID, - app.DialogFieldNameKey: "bad userid", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - require.Error(t, err) - assert.Equal(t, http.StatusBadRequest, result.StatusCode) - }, - }, - "invalid: missing channelid": { - dialogRequest: model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: incompletePlaybookID, - app.DialogFieldNameKey: "run number 1", - }, - }, - expected: func(t *testing.T, result *http.Response, err error) { - require.Error(t, err) - assert.Equal(t, http.StatusBadRequest, result.StatusCode) - }, - }, - } { - t.Run(name, func(t *testing.T) { - dialogRequestBytes, err := json.Marshal(tc.dialogRequest) - require.NoError(t, err) - - if tc.permissionsPrep != nil { - defaultRolePermissions := e.Permissions.SaveDefaultRolePermissions() - defer func() { - e.Permissions.RestoreDefaultRolePermissions(defaultRolePermissions) - }() - tc.permissionsPrep() - } - - result, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/runs/dialog", dialogRequestBytes, "") - tc.expected(t, result, err) - }) - } - }) - - t.Run("create valid run", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - assert.NoError(t, err) - assert.NotNil(t, run) - }) - - t.Run("create valid run without playbook", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "No playbook", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - }) - assert.NoError(t, err) - assert.NotNil(t, run) - }) - - t.Run("can't without owner", func(t *testing.T) { - _, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "No owner", - OwnerUserID: "", - TeamID: e.BasicTeam.Id, - }) - assert.Error(t, err) - }) - - t.Run("can't without team", func(t *testing.T) { - _, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - assert.Error(t, err) - }) - - t.Run("missing name", func(t *testing.T) { - _, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - assert.Error(t, err) - }) - - t.Run("archived playbook", func(t *testing.T) { - _, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.ArchivedPlaybook.ID, - }) - assert.Error(t, err) - }) - - t.Run("create valid run using playbook with due dates", func(t *testing.T) { - durations := []int64{ - 4 * time.Hour.Milliseconds(), // 4 hours - 30 * time.Minute.Milliseconds(), // 30 min - 4 * 24 * time.Hour.Milliseconds(), // 4 days - } - - // create playbook with relative due dates - playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Public: true, - Title: "PB", - TeamID: e.BasicTeam.Id, - Checklists: []client.Checklist{ - { - Title: "A", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - DueDate: durations[0], - }, - { - Title: "Do this2", - DueDate: durations[1], - }, - }, - }, - { - Title: "B", - Items: []client.ChecklistItem{ - { - Title: "Do this1", - DueDate: durations[2], - }, - { - Title: "Do this2", - }, - }, - }, - }, - }) - assert.NoError(t, err) - - now := model.GetMillis() - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "With due dates", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookID, - }) - assert.NoError(t, err) - assert.NotNil(t, run) - // compare date with 10^4 precision because run creation might take more than a second - assert.Equal(t, (now+durations[0])/10000, run.Checklists[0].Items[0].DueDate/10000) - assert.Equal(t, (now+durations[1])/10000, run.Checklists[0].Items[1].DueDate/10000) - assert.Equal(t, (now+durations[2])/10000, run.Checklists[1].Items[0].DueDate/10000) - assert.Zero(t, run.Checklists[1].Items[1].DueDate) - }) -} - -func TestCreateRunInExistingChannel(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - // create playbook - playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Public: true, - Title: "PB", - TeamID: e.BasicTeam.Id, - ChannelMode: client.PlaybookRunLinkExistingChannel, - ChannelID: e.BasicPublicChannel.Id, - }) - assert.NoError(t, err) - - t.Run("create a run", func(t *testing.T) { - // create a run, pass the channel id from the playbook configuration - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "run in existing channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookID, - ChannelID: e.BasicPublicChannel.Id, - }) - assert.NoError(t, err) - assert.NotNil(t, run) - assert.Equal(t, e.BasicPublicChannel.Id, run.ChannelID) - }) - - t.Run("no access to the linked channel", func(t *testing.T) { - // create a run, pass the channel id from the playbook configuration - run, err := e.PlaybooksClient2.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "run in existing channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookID, - ChannelID: e.BasicPublicChannel.Id, - }) - - // PlaybooksClient2 is not a channel member, so should not be able to start a run - assert.Error(t, err) - assert.Nil(t, run) - }) - - t.Run("create a run, pass a channel different from the playbook configs", func(t *testing.T) { - // create private channel - privateChannel, _, err := e.ServerAdminClient.CreateChannel(context.Background(), &model.Channel{ - DisplayName: "test_private", - Name: "test_private", - Type: model.ChannelTypePrivate, - TeamId: e.BasicTeam.Id, - }) - require.NoError(e.T, err) - _, _, err = e.ServerAdminClient.AddChannelMember(context.Background(), privateChannel.Id, e.RegularUser.Id) - require.NoError(e.T, err) - - // create a run, pass the channel id different from the playbook configs - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "run in existing channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookID, - ChannelID: privateChannel.Id, - }) - assert.NoError(t, err) - assert.NotNil(t, run) - assert.Equal(t, privateChannel.Id, run.ChannelID) - }) - - t.Run("create a run using dialog requests", func(t *testing.T) { - dialogRequest := model.SubmitDialogRequest{ - TeamId: e.BasicTeam.Id, - UserId: e.RegularUser.Id, - State: "{}", - Submission: map[string]interface{}{ - app.DialogFieldPlaybookIDKey: playbookID, - app.DialogFieldNameKey: "run number 1", - }, - } - dialogRequestBytes, err := json.Marshal(dialogRequest) - assert.NoError(t, err) - - result, err := e.ServerClient.DoAPIRequestBytes(context.Background(), "POST", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/runs/dialog", dialogRequestBytes, "") - - assert.NoError(t, err) - assert.Equal(t, http.StatusCreated, result.StatusCode) - - url, err := result.Location() - assert.NoError(t, err) - runID := url.Path[strings.LastIndex(url.Path, "/")+1:] - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), runID) - assert.NoError(t, err) - assert.Equal(t, e.BasicPublicChannel.Id, run.ChannelID) - }) -} - -func TestCreateInvalidRuns(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("fails if description is longer than 4096", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "test run", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - Description: strings.Repeat("A", 4097), - }) - requireErrorWithStatusCode(t, err, http.StatusInternalServerError) - assert.Nil(t, run) - }) -} - -func TestRunRetrieval(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("by channel id", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.GetByChannelID(context.Background(), e.BasicRun.ChannelID) - require.NoError(t, err) - require.Equal(t, e.BasicRun.ID, run.ID) - }) - - t.Run("by channel id not found", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.GetByChannelID(context.Background(), model.NewId()) - require.Error(t, err) - require.Nil(t, run) - }) - - t.Run("empty list", func(t *testing.T) { - list, err := e.PlaybooksAdminClient.PlaybookRuns.List(context.Background(), 0, 100, client.PlaybookRunListOptions{ - TeamID: e.BasicTeam2.Id, - }) - require.NoError(t, err) - require.Len(t, list.Items, 0) - }) - - t.Run("filters", func(t *testing.T) { - endedRun, err := e.PlaybooksAdminClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Anouther Run", - TeamID: e.BasicTeam.Id, - OwnerUserID: e.AdminUser.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - err = e.PlaybooksAdminClient.PlaybookRuns.Finish(context.Background(), endedRun.ID) - require.NoError(t, err) - - list, err := e.PlaybooksAdminClient.PlaybookRuns.List(context.Background(), 0, 100, client.PlaybookRunListOptions{ - TeamID: e.BasicTeam.Id, - }) - require.NoError(t, err) - require.Len(t, list.Items, 2) - - list, err = e.PlaybooksAdminClient.PlaybookRuns.List(context.Background(), 0, 100, client.PlaybookRunListOptions{ - TeamID: e.BasicTeam.Id, - Statuses: []client.Status{client.StatusInProgress}, - }) - require.NoError(t, err) - require.Len(t, list.Items, 1) - - list, err = e.PlaybooksAdminClient.PlaybookRuns.List(context.Background(), 0, 100, client.PlaybookRunListOptions{ - TeamID: e.BasicTeam.Id, - OwnerID: e.RegularUser.Id, - }) - require.NoError(t, err) - require.Len(t, list.Items, 1) - }) - - t.Run("checklist autocomplete", func(t *testing.T) { - resp, err := e.ServerClient.DoAPIRequest(context.Background(), "GET", e.ServerClient.URL+"/plugins/"+"playbooks"+"/api/v0/runs/checklist-autocomplete?channel_id="+e.BasicPrivateChannel.Id, "", "") - assert.Error(t, err) - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - }) - - t.Run("can't get cross team", func(t *testing.T) { - _, err := e.PlaybooksClientNotInTeam.PlaybookRuns.Get(context.Background(), e.BasicRun.ID) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("can't list cross team", func(t *testing.T) { - list, err := e.PlaybooksClient.PlaybookRuns.List(context.Background(), 0, 100, client.PlaybookRunListOptions{ - TeamID: e.BasicTeam.Id, - }) - require.NoError(t, err) - require.GreaterOrEqual(t, len(list.Items), 1) - list2, err2 := e.PlaybooksClientNotInTeam.PlaybookRuns.List(context.Background(), 0, 100, client.PlaybookRunListOptions{ - TeamID: e.BasicTeam.Id, - }) - assert.NoError(t, err2) - assert.Len(t, list2.Items, 0) - }) -} - -func TestRunPostStatusUpdate(t *testing.T) { - t.Skip("MM-52694") - e := Setup(t) - e.CreateBasic() - - t.Run("post an update", func(t *testing.T) { - err := e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), e.BasicRun.ID, "update", 600) - assert.NoError(t, err) - }) - - t.Run("creates a reminder post", func(t *testing.T) { - err := e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), e.BasicRun.ID, "update", 1) - assert.NoError(t, err) - - // wait for the scheduler to run the job - time.Sleep(2 * time.Second) - - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), e.BasicRun.ID) - assert.Equal(t, 1*time.Second, run.PreviousReminder) - assert.NotEmpty(t, run.ReminderPostID) - assert.NoError(t, err) - - // post created with expected props - post, _, err := e.ServerClient.GetPost(context.Background(), run.ReminderPostID, "") - assert.NoError(t, err) - assert.Equal(t, run.ID, post.GetProp("playbookRunId")) - assert.Equal(t, e.RegularUser.Username, post.GetProp("targetUsername")) - }) - - t.Run("poar an update with empty message", func(t *testing.T) { - err := e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), e.BasicRun.ID, " \t \r ", 600) - assert.Error(t, err) - }) - - t.Run("no permissions to run", func(t *testing.T) { - _, _, err := e.ServerAdminClient.AddChannelMember(context.Background(), e.BasicRun.ChannelID, e.RegularUser2.Id) - require.NoError(t, err) - err = e.PlaybooksClient2.PlaybookRuns.UpdateStatus(context.Background(), e.BasicRun.ID, "update", 600) - requireErrorWithStatusCode(t, err, http.StatusForbidden) - }) - - t.Run("test no permissions to broadcast channel", func(t *testing.T) { - // Create a run with a private channel in the broadcast channels - e.BasicPlaybook.BroadcastChannelIDs = []string{e.BasicPrivateChannel.Id} - err := e.PlaybooksAdminClient.Playbooks.Update(context.Background(), *e.BasicPlaybook) - require.NoError(t, err) - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Poison broadcast channel", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.NotNil(t, run) - - // Update should work even when we don't have access to private broadcast channel - err = e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), run.ID, "update", 600) - assert.NoError(t, err) - }) -} - -func TestChecklistManagement(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - createNewRunWithNoChecklists := func(t *testing.T) *client.PlaybookRun { - t.Helper() - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - return run - } - - t.Run("checklist creation - success: empty checklist", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - title := "A new checklist" - - // Create a valid, empty checklist - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: title, - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Make sure the new checklist is there - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Equal(t, title, editedRun.Checklists[0].Title) - }) - - t.Run("checklist creation - failure: no permissions", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - title := "A new checklist" - - // Create a valid, empty checklist - err := e.PlaybooksClient2.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: title, - Items: []client.ChecklistItem{}, - }) - require.Error(t, err) - }) - - t.Run("checklist creation - success: checklist with items", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - title := "A new checklist" - - // Create a valid checklist with some items - items := []client.ChecklistItem{ - { - Title: "First", - Description: "", - }, - { - Title: "Second", - Description: "Description", - }, - } - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: title, - Items: items, - }) - require.NoError(t, err) - - // Make sure the new checklist is there - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Equal(t, title, editedRun.Checklists[0].Title) - require.Equal(t, "First", editedRun.Checklists[0].Items[0].Title) - require.Equal(t, "Second", editedRun.Checklists[0].Items[1].Title) - require.Equal(t, "Description", editedRun.Checklists[0].Items[1].Description) - }) - - t.Run("checklist creation - failure: no title", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - - // Try to create a new checklist with no title - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "", - Items: []client.ChecklistItem{}, - }) - require.Error(t, err) - - // Make sure that the checklist was not added - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 0) - }) - - t.Run("checklist renaming - success", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - oldTitle := "Old Title" - newTitle := "New Title" - - // Create a new checklist with a known title - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: oldTitle, - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Rename the checklist to a new title - err = e.PlaybooksClient.PlaybookRuns.RenameChecklist(context.Background(), run.ID, 0, newTitle) - require.NoError(t, err) - - // Retrieve the run again and make sure that the checklist's title has changed - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Equal(t, newTitle, editedRun.Checklists[0].Title) - }) - - t.Run("checklist renaming - failure: no title", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - oldTitle := "Old Title" - newTitle := "" - - // Create a valid checklist - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: oldTitle, - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Try to rename the checklist to an empty title - err = e.PlaybooksClient.PlaybookRuns.RenameChecklist(context.Background(), run.ID, 0, newTitle) - require.Error(t, err) - }) - - t.Run("checklist renaming - failure: wrong checklist number", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - newTitle := "New Title" - - // Try to rename a checklist that does not exist (negative number) - err := e.PlaybooksClient.PlaybookRuns.RenameChecklist(context.Background(), run.ID, -1, newTitle) - require.Error(t, err) - - // Try to rename a checklist that does not exist (number greater than the index of the last checklist) - err = e.PlaybooksClient.PlaybookRuns.RenameChecklist(context.Background(), run.ID, len(run.Checklists), newTitle) - require.Error(t, err) - }) - - t.Run("checklist removal - success: result in no checklists", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - require.Len(t, run.Checklists, 0) - - // Create a valid checklist - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "title", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Retrieve the run again and make sure that the checklist was created - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - - // Remove the recently created checklist - err = e.PlaybooksClient.PlaybookRuns.RemoveChecklist(context.Background(), run.ID, 0) - require.NoError(t, err) - - // Retrieve the run again and make sure that the checklist was removed - editedRun, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 0) - }) - - t.Run("checklist removal - success: still some checklists", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - - // Create two valid checklists - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "First checklist", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - err = e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "Second checklist", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Retrieve the run again and make sure that the checklists were created - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 2) - - // Remove the last checklist - err = e.PlaybooksClient.PlaybookRuns.RemoveChecklist(context.Background(), run.ID, 1) - require.NoError(t, err) - - // Retrieve the run again and make sure that the checklist was removed - editedRun, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Equal(t, "First checklist", editedRun.Checklists[0].Title) - }) - - t.Run("checklist removal - failure: wrong checklist number", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - - // Try to remove a checklist that does not exist (negative number) - err := e.PlaybooksClient.PlaybookRuns.RemoveChecklist(context.Background(), run.ID, -1) - require.Error(t, err) - - // Try to rename a checklist that does not exist (number greater than the index of the last checklist) - err = e.PlaybooksClient.PlaybookRuns.RemoveChecklist(context.Background(), run.ID, 0) - require.Error(t, err) - - // Create a checklist so that there is at least one - err = e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "Second checklist", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Retrieve the run again and make sure that there is one checklist - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - - // Try to remove a checklist that does not exist (number greater than the index of the last checklist) - err = e.PlaybooksClient.PlaybookRuns.RemoveChecklist(context.Background(), run.ID, 1) - require.Error(t, err) - }) - - t.Run("checklist adding - success", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - - // Create a new checklist with a known title - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "Checklist Title", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Add the new checklistItem - itemTitle := "New echo item" - command := "/echo hi!" - description := "A very complicated checklist item." - err = e.PlaybooksClient.PlaybookRuns.AddChecklistItem(context.Background(), run.ID, 0, client.ChecklistItem{ - Title: itemTitle, - Command: command, - Description: description, - }) - require.NoError(t, err) - - // Retrieve the run again and make sure that the checklistItem is there - editedRun, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, editedRun.Checklists, 1) - require.Len(t, editedRun.Checklists[0].Items, 1) - require.Equal(t, itemTitle, editedRun.Checklists[0].Items[0].Title) - require.Equal(t, command, editedRun.Checklists[0].Items[0].Command) - require.Equal(t, description, editedRun.Checklists[0].Items[0].Description) - }) - - t.Run("checklist adding - failure: no title", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - - // Create a new checklist with a known title - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "Checklist Title", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Add the new checklistItem with an invalid title - err = e.PlaybooksClient.PlaybookRuns.AddChecklistItem(context.Background(), run.ID, 0, client.ChecklistItem{ - Title: "", - }) - require.Error(t, err) - }) - - t.Run("checklist adding - failure: wrong checklist number", func(t *testing.T) { - run := createNewRunWithNoChecklists(t) - - // Create a new checklist with a known title - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "Checklist Title", - Items: []client.ChecklistItem{}, - }) - require.NoError(t, err) - - // Add the new checklistItem -- to an invalid checklist number (negative) - err = e.PlaybooksClient.PlaybookRuns.AddChecklistItem(context.Background(), run.ID, -1, client.ChecklistItem{ - Title: "New echo item", - }) - require.Error(t, err) - - // Add the new checklistItem -- to an invalid checklist number (non-existent) - err = e.PlaybooksClient.PlaybookRuns.AddChecklistItem(context.Background(), run.ID, len(run.Checklists)+1, client.ChecklistItem{ - Title: "New echo item", - }) - require.Error(t, err) - }) - - type ExpectedError struct{ StatusCode int } - - moveItemTests := []struct { - Title string - Checklists [][]string - SourceChecklistIdx int - SourceItemIdx int - DestChecklistIdx int - DestItemIdx int - ExpectedItemTitles [][]string - ExpectedError *ExpectedError - }{ - { - "One checklist with two items - move the first item", - [][]string{{"00", "01"}}, - 0, 0, 0, 1, - [][]string{{"01", "00"}}, - nil, - }, - { - "One checklist with two items - move the second item", - [][]string{{"00", "01"}}, - 0, 1, 0, 0, - [][]string{{"01", "00"}}, - nil, - }, - { - "One checklist with three items - move the first item to the second position", - [][]string{{"00", "01", "02"}}, - 0, 0, 0, 1, - [][]string{{"01", "00", "02"}}, - nil, - }, - { - "One checklist with three items - move the second item to the first position", - [][]string{{"00", "01", "02"}}, - 0, 1, 0, 0, - [][]string{{"01", "00", "02"}}, - nil, - }, - { - "One checklist with three items - move the first item to the last position", - [][]string{{"00", "01", "02"}}, - 0, 0, 0, 2, - [][]string{{"01", "02", "00"}}, - nil, - }, - { - "Multiple checklists - move from one to another", - [][]string{{"10", "11", "12"}, {"00", "01", "02"}}, - 0, 1, 1, 0, - [][]string{{"00", "02"}, {"01", "10", "11", "12"}}, - nil, - }, - { - "Multiple checklists - move to an empty checklist", - [][]string{{}, {"00", "01"}}, - 0, 0, 1, 0, - [][]string{{"01"}, {"00"}}, - nil, - }, - { - "Multiple checklists - leave the original checklist empty", - [][]string{{"10"}, {"00"}}, - 0, 0, 1, 1, - [][]string{{}, {"10", "00"}}, - nil, - }, - { - "One checklist - invalid source checklist: greater than length of checklists", - [][]string{{"00"}}, - 1, 0, 0, 0, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid source checklist: negative number", - [][]string{{"00"}}, - -1, 0, 0, 0, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid dest checklist: greater than length of items", - [][]string{{"00"}}, - 0, 0, 1, 0, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid dest checklist: negative number", - [][]string{{"00"}}, - 0, 0, -1, 0, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid source item: greater than length of items", - [][]string{{"00"}}, - 0, 1, 0, 0, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid source item: negative number", - [][]string{{"00"}}, - 0, -1, 0, 0, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid dest item: greater than length of items", - [][]string{{"00"}}, - 0, 0, 0, 1, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - { - "One checklist - invalid dest item: negative number", - [][]string{{"00"}}, - 0, 0, 0, -1, - [][]string{}, - &ExpectedError{StatusCode: 500}, - }, - } - - for _, test := range moveItemTests { - t.Run(test.Title, func(t *testing.T) { - // Create a new empty run - run := createNewRunWithNoChecklists(t) - - // Add the specified checklists: note that we need to iterate backwards because CreateChecklist prepends new checklists - for i := len(test.Checklists) - 1; i >= 0; i-- { - // Generate the items for this checklist - checklist := test.Checklists[i] - items := make([]client.ChecklistItem, 0, len(checklist)) - for _, title := range checklist { - items = append(items, client.ChecklistItem{Title: title}) - } - - // Create the checklist with the defined items - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "Checklist", - Items: items, - }) - require.NoError(t, err) - } - - // Move the item from its source to its destination - err := e.PlaybooksClient.PlaybookRuns.MoveChecklistItem(context.Background(), run.ID, test.SourceChecklistIdx, test.SourceItemIdx, test.DestChecklistIdx, test.DestItemIdx) - - // If an error is expected, check that it's the one we expect - if test.ExpectedError != nil { - requireErrorWithStatusCode(t, err, test.ExpectedError.StatusCode) - return - } - - // If no error is expected, retrieve the run again - require.NoError(t, err) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - - // And check that the new checklists are ordered as specified by the test data - for checklistIdx, actualChecklist := range run.Checklists { - expectedItemTitles := test.ExpectedItemTitles[checklistIdx] - require.Len(t, actualChecklist.Items, len(expectedItemTitles)) - - for itemIdx, actualItem := range actualChecklist.Items { - require.Equal(t, expectedItemTitles[itemIdx], actualItem.Title) - } - } - }) - } - - moveChecklistTests := []struct { - Title string - Checklists []string - SourceChecklistIdx int - DestChecklistIdx int - ExpectedChecklists []string - ExpectedError *ExpectedError - }{ - { - "Move checklist to the same position", - []string{"0"}, - 0, 0, - []string{"0"}, - nil, - }, - { - "Swap two checklists, moving the first one", - []string{"1", "0"}, - 0, 1, - []string{"1", "0"}, - nil, - }, - { - "Swap two checklists, moving the second one", - []string{"1", "0"}, - 1, 0, - []string{"1", "0"}, - nil, - }, - { - "Move a checklist in a list of three checklists - first to second ", - []string{"2", "1", "0"}, - 0, 1, - []string{"1", "0", "2"}, - nil, - }, - { - "Move a checklist in a list of three checklists - first to third", - []string{"2", "1", "0"}, - 0, 2, - []string{"1", "2", "0"}, - nil, - }, - { - "Move a checklist in a list of three checklists - second to first", - []string{"2", "1", "0"}, - 1, 0, - []string{"1", "0", "2"}, - nil, - }, - { - "Move a checklist in a list of three checklists - second to third", - []string{"2", "1", "0"}, - 1, 2, - []string{"0", "2", "1"}, - nil, - }, - { - "Move a checklist in a list of three checklists - third to first", - []string{"2", "1", "0"}, - 2, 0, - []string{"2", "0", "1"}, - nil, - }, - { - "Move a checklist in a list of three checklists - third to second", - []string{"2", "1", "0"}, - 2, 1, - []string{"0", "2", "1"}, - nil, - }, - { - "Wrong destination index - greater than length of list", - []string{"2", "1", "0"}, - 0, 5, - []string{"0", "1", "2"}, - &ExpectedError{500}, - }, - { - "Wrong destination index - negative", - []string{"2", "1", "0"}, - 0, -5, - []string{"0", "1", "2"}, - &ExpectedError{500}, - }, - { - "Wrong source index - greater than length of list", - []string{"2", "1", "0"}, - 5, 0, - []string{"0", "1", "2"}, - &ExpectedError{500}, - }, - { - "Wrong source index - negative", - []string{"2", "1", "0"}, - -5, 0, - []string{"0", "1", "2"}, - &ExpectedError{500}, - }, - } - - for _, test := range moveChecklistTests { - t.Run(test.Title, func(t *testing.T) { - // Create a new empty run - run := createNewRunWithNoChecklists(t) - - // Add the specified checklists: note that we need to iterate backwards because CreateChecklist prepends new checklists - for i := len(test.Checklists) - 1; i >= 0; i-- { - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: test.Checklists[i], - }) - require.NoError(t, err) - } - - // Move the checklist from its source to its destination - err := e.PlaybooksClient.PlaybookRuns.MoveChecklist(context.Background(), run.ID, test.SourceChecklistIdx, test.DestChecklistIdx) - - // If an error is expected, check that it's the one we expect - if test.ExpectedError != nil { - requireErrorWithStatusCode(t, err, test.ExpectedError.StatusCode) - return - } - - // If no error is expected, retrieve the run again - require.NoError(t, err) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - - // And check that the new checklists are ordered as specified by the test data - for checklistIdx, actualChecklist := range run.Checklists { - require.Equal(t, test.ExpectedChecklists[checklistIdx], actualChecklist.Title) - } - }) - } -} - -func TestChecklisFailTooLarge(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("checklist creation - failure: too large checklist", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - err = e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, client.Checklist{ - Title: "My regular title", - Items: []client.ChecklistItem{ - {Title: "Item title", Description: strings.Repeat("A", (256*1024)+1)}, - }, - }) - require.Error(t, err) - }) -} - -func TestRunGetStatusUpdates(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("public - get no updates", func(t *testing.T) { - statusUpdates, err := e.PlaybooksClient.PlaybookRuns.GetStatusUpdates(context.Background(), e.BasicRun.ID) - assert.NoError(t, err) - assert.Len(t, statusUpdates, 0) - }) - - t.Run("public - get 2 updates as participant", func(t *testing.T) { - err := e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), e.BasicRun.ID, "update 1", 5000) - require.NoError(t, err) - err = e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), e.BasicRun.ID, "update 2", 10000) - require.NoError(t, err) - - statusUpdates, err := e.PlaybooksClient.PlaybookRuns.GetStatusUpdates(context.Background(), e.BasicRun.ID) - require.NoError(t, err) - assert.Len(t, statusUpdates, 2) - assert.Equal(t, "update 2", statusUpdates[0].Message) - assert.Equal(t, "update 1", statusUpdates[1].Message) - assert.Equal(t, e.RegularUser.Username, statusUpdates[0].AuthorUserName) - }) - - t.Run("public - get 2 updates as viewer", func(t *testing.T) { - statusUpdates, err := e.PlaybooksClient2.PlaybookRuns.GetStatusUpdates(context.Background(), e.BasicRun.ID) - require.NoError(t, err) - assert.Len(t, statusUpdates, 2) - assert.Equal(t, "update 2", statusUpdates[0].Message) - assert.Equal(t, "update 1", statusUpdates[1].Message) - assert.Equal(t, e.RegularUser.Username, statusUpdates[0].AuthorUserName) - assert.Equal(t, e.RegularUser.Username, statusUpdates[1].AuthorUserName) - }) - - t.Run("public - fails because not in team", func(t *testing.T) { - statusUpdates, err := e.PlaybooksClientNotInTeam.PlaybookRuns.GetStatusUpdates(context.Background(), e.BasicRun.ID) - require.Error(t, err) - assert.Len(t, statusUpdates, 0) - }) - - t.Run("private - get no updates", func(t *testing.T) { - privateRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPrivatePlaybook.ID, - }) - assert.NoError(t, err) - - statusUpdates, err := e.PlaybooksClient.PlaybookRuns.GetStatusUpdates(context.Background(), privateRun.ID) - assert.NoError(t, err) - assert.Len(t, statusUpdates, 0) - }) - - t.Run("private - get 2 updates as participant", func(t *testing.T) { - privateRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPrivatePlaybook.ID, - }) - assert.NoError(t, err) - - err = e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), privateRun.ID, "update 1", 5000) - require.NoError(t, err) - err = e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), privateRun.ID, "update 2", 10000) - require.NoError(t, err) - - statusUpdates, err := e.PlaybooksClient.PlaybookRuns.GetStatusUpdates(context.Background(), privateRun.ID) - require.NoError(t, err) - assert.Len(t, statusUpdates, 2) - assert.Equal(t, "update 2", statusUpdates[0].Message) - assert.Equal(t, "update 1", statusUpdates[1].Message) - assert.Equal(t, e.RegularUser.Username, statusUpdates[0].AuthorUserName) - assert.Equal(t, e.RegularUser.Username, statusUpdates[1].AuthorUserName) - }) - - t.Run("private - get 2 updates as viewer", func(t *testing.T) { - privatePlaybookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybook custom", - TeamID: e.BasicTeam.Id, - Public: false, - Members: []client.PlaybookMember{ - {UserID: e.RegularUser2.Id, Roles: []string{app.PlaybookRoleMember}}, - {UserID: e.RegularUser.Id, Roles: []string{app.PlaybookRoleMember}}, - {UserID: e.AdminUser.Id, Roles: []string{app.PlaybookRoleAdmin, app.PlaybookRoleMember}}, - }, - }) - require.NoError(t, err) - - privatePlaybook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), privatePlaybookID) - require.NoError(e.T, err) - - privateRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: privatePlaybook.ID, - }) - require.NoError(t, err) - - err = e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), privateRun.ID, "update 1", 5000) - require.NoError(t, err) - err = e.PlaybooksClient.PlaybookRuns.UpdateStatus(context.Background(), privateRun.ID, "update 2", 10000) - require.NoError(t, err) - - statusUpdates, err := e.PlaybooksClient2.PlaybookRuns.GetStatusUpdates(context.Background(), privateRun.ID) - require.NoError(t, err) - assert.Len(t, statusUpdates, 2) - assert.Equal(t, "update 2", statusUpdates[0].Message) - assert.Equal(t, "update 1", statusUpdates[1].Message) - assert.Equal(t, e.RegularUser.Username, statusUpdates[0].AuthorUserName) - }) - - t.Run("private - fails because not in playbook members", func(t *testing.T) { - privateRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPrivatePlaybook.ID, - }) - require.NoError(t, err) - - statusUpdates, err := e.PlaybooksClient2.PlaybookRuns.GetStatusUpdates(context.Background(), privateRun.ID) - require.Error(t, err) - assert.Len(t, statusUpdates, 0) - }) -} - -func TestRequestUpdate(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("private - no viewer access ", func(t *testing.T) { - privateRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPrivatePlaybook.ID, - }) - assert.NoError(t, err) - - err = e.PlaybooksClient2.PlaybookRuns.RequestUpdate(context.Background(), privateRun.ID, e.RegularUser2.Id) - assert.Error(t, err) - - err = e.PlaybooksClientNotInTeam.PlaybookRuns.RequestUpdate(context.Background(), privateRun.ID, e.RegularUserNotInTeam.Id) - assert.Error(t, err) - }) - - t.Run("private - viewer access ", func(t *testing.T) { - privatePlaybookID, err := e.PlaybooksClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybook custom", - TeamID: e.BasicTeam.Id, - Public: false, - Members: []client.PlaybookMember{ - {UserID: e.RegularUser.Id, Roles: []string{app.PlaybookRoleMember}}, - }, - }) - require.NoError(t, err) - - privatePlaybook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), privatePlaybookID) - require.NoError(e.T, err) - - privateRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: privatePlaybookID, - }) - assert.NoError(t, err) - - // No access, RegularUser2 is not a Viewer - err = e.PlaybooksClient2.PlaybookRuns.RequestUpdate(context.Background(), privateRun.ID, e.RegularUser2.Id) - assert.Error(t, err) - - // Add RegularUser2 as a Viewer - privatePlaybook.Members = append(privatePlaybook.Members, client.PlaybookMember{UserID: e.RegularUser2.Id, Roles: []string{app.PlaybookRoleMember}}) - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *privatePlaybook) - assert.NoError(t, err) - - // Gained Viewer access - err = e.PlaybooksClient2.PlaybookRuns.RequestUpdate(context.Background(), privateRun.ID, e.RegularUser2.Id) - assert.NoError(t, err) - - // Assert that timeline event is created - privateRun, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), privateRun.ID) - assert.NoError(t, err) - assert.NotEmpty(t, privateRun.TimelineEvents) - lastEvent := privateRun.TimelineEvents[len(privateRun.TimelineEvents)-1] - assert.Equal(t, client.StatusUpdateRequested, lastEvent.EventType) - assert.Equal(t, e.RegularUser2.Id, lastEvent.SubjectUserID) - assert.Equal(t, e.RegularUser2.Id, lastEvent.CreatorUserID) - assert.NotZero(t, lastEvent.PostID) - assert.Equal(t, "@playbooksuser2 requested a status update", lastEvent.Summary) - }) - - t.Run("public - viewer access ", func(t *testing.T) { - publicRun, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - assert.NoError(t, err) - - err = e.PlaybooksClient2.PlaybookRuns.RequestUpdate(context.Background(), publicRun.ID, e.RegularUser2.Id) - assert.NoError(t, err) - - // Assert that timeline event is created - publicRun, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), publicRun.ID) - assert.NoError(t, err) - assert.NotEmpty(t, publicRun.TimelineEvents) - lastEvent := publicRun.TimelineEvents[len(publicRun.TimelineEvents)-1] - assert.Equal(t, client.StatusUpdateRequested, lastEvent.EventType) - assert.Equal(t, e.RegularUser2.Id, lastEvent.SubjectUserID) - assert.Equal(t, "@playbooksuser2 requested a status update", lastEvent.Summary) - - err = e.PlaybooksClientNotInTeam.PlaybookRuns.RequestUpdate(context.Background(), publicRun.ID, e.RegularUserNotInTeam.Id) - assert.Error(t, err) - }) -} - -func TestReminderReset(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("reminder reset - timeline event created", func(t *testing.T) { - payload := client.ReminderResetPayload{ - NewReminderSeconds: 100, - } - err := e.PlaybooksClient.Reminders.Reset(context.Background(), e.BasicRun.ID, payload) - assert.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), e.BasicRun.ID) - assert.Equal(t, 100*time.Second, run.PreviousReminder) - assert.NoError(t, err) - - statusSnoozed := make([]client.TimelineEvent, 0) - for _, te := range run.TimelineEvents { - if te.EventType == "status_update_snoozed" { - statusSnoozed = append(statusSnoozed, te) - } - } - require.Len(t, statusSnoozed, 1) - }) - - t.Run("reminder reset - reminder post created", func(t *testing.T) { - payload := client.ReminderResetPayload{ - NewReminderSeconds: 1, - } - err := e.PlaybooksClient.Reminders.Reset(context.Background(), e.BasicRun.ID, payload) - assert.NoError(t, err) - - // wait for scheduler to run the job - time.Sleep(2 * time.Second) - - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), e.BasicRun.ID) - assert.Equal(t, 1*time.Second, run.PreviousReminder) - assert.NotEmpty(t, run.ReminderPostID) - assert.NoError(t, err) - - // post created with expected props - post, _, err := e.ServerClient.GetPost(context.Background(), run.ReminderPostID, "") - assert.NoError(t, err) - assert.Equal(t, run.ID, post.GetProp("playbookRunId")) - assert.Equal(t, e.RegularUser.Username, post.GetProp("targetUsername")) - }) -} - -func TestChecklisItem_SetAssignee(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - addSimpleChecklistToTun := func(t *testing.T, runID string) *client.PlaybookRun { - checklist := client.Checklist{ - Title: "Test Checklist", - Items: []client.ChecklistItem{ - { - Title: "Test Item", - }, - }, - } - - err := e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), runID, checklist) - require.NoError(t, err) - - run, err := e.PlaybooksClient.PlaybookRuns.Get(context.Background(), runID) - require.NoError(t, err) - require.Len(t, run.Checklists, 1) - require.Len(t, run.Checklists[0].Items, 1) - return run - } - - t.Run("set assignee and participant", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - run = addSimpleChecklistToTun(t, run.ID) - - // assignee is not set and user is not participant (before) - require.Empty(t, run.Checklists[0].Items[0].AssigneeID) - require.Len(t, run.ParticipantIDs, 1) - require.NotContains(t, run.ParticipantIDs, e.RegularUser2.Id) - - // set assignee - err = e.PlaybooksClient.PlaybookRuns.SetItemAssignee(context.Background(), run.ID, 0, 0, e.RegularUser2.Id) - require.NoError(t, err) - - // assignee is not set and user is not participant (after) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, e.RegularUser2.Id, run.Checklists[0].Items[0].AssigneeID) - require.Contains(t, run.ParticipantIDs, e.RegularUser2.Id) - }) - - t.Run("set and unset", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - run = addSimpleChecklistToTun(t, run.ID) - - // set assignee - err = e.PlaybooksClient.PlaybookRuns.SetItemAssignee(context.Background(), run.ID, 0, 0, e.RegularUser.Id) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, e.RegularUser.Id, run.Checklists[0].Items[0].AssigneeID) - - // unset assignee - err = e.PlaybooksClient.PlaybookRuns.SetItemAssignee(context.Background(), run.ID, 0, 0, "") - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, "", run.Checklists[0].Items[0].AssigneeID) - }) - - t.Run("idempotent action", func(t *testing.T) { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - run = addSimpleChecklistToTun(t, run.ID) - - // set assignee - err = e.PlaybooksClient.PlaybookRuns.SetItemAssignee(context.Background(), run.ID, 0, 0, e.RegularUser.Id) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, e.RegularUser.Id, run.Checklists[0].Items[0].AssigneeID) - - // unset assignee - err = e.PlaybooksClient.PlaybookRuns.SetItemAssignee(context.Background(), run.ID, 0, 0, e.RegularUser.Id) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, e.RegularUser.Id, run.Checklists[0].Items[0].AssigneeID) - }) -} - -func TestChecklisItem_SetCommand(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - require.Len(t, run.Checklists, 0) - - checklist := client.Checklist{ - Title: "Test Checklist", - Items: []client.ChecklistItem{ - { - Title: "Test Item", - }, - }, - } - - err = e.PlaybooksClient.PlaybookRuns.CreateChecklist(context.Background(), run.ID, checklist) - require.NoError(t, err) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Len(t, run.Checklists, 1) - require.Len(t, run.Checklists[0].Items, 1) - - t.Run("set command", func(t *testing.T) { - // command and commandlastrun are not set (before) - require.Empty(t, run.Checklists[0].Items[0].CommandLastRun) - require.Empty(t, run.Checklists[0].Items[0].Command) - - // set command - err = e.PlaybooksClient.PlaybookRuns.SetItemCommand(context.Background(), run.ID, 0, 0, "/playbook todo") - require.NoError(t, err) - - // command and commandlastrun are set (after) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, "/playbook todo", run.Checklists[0].Items[0].Command) - require.Equal(t, int64(0), run.Checklists[0].Items[0].CommandLastRun) - }) - - t.Run("run command", func(t *testing.T) { - // command and commandlastrun are not set (before) - require.Empty(t, run.Checklists[0].Items[0].CommandLastRun) - - // run command - err = e.PlaybooksClient.PlaybookRuns.RunItemCommand(context.Background(), run.ID, 0, 0) - require.NoError(t, err) - - // command and commandlastrun are set (after) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, "/playbook todo", run.Checklists[0].Items[0].Command) - require.NotZero(t, run.Checklists[0].Items[0].CommandLastRun) - }) - - t.Run("rerun command", func(t *testing.T) { - lastRun := run.Checklists[0].Items[0].CommandLastRun - - // rerun command - err = e.PlaybooksClient.PlaybookRuns.RunItemCommand(context.Background(), run.ID, 0, 0) - require.NoError(t, err) - - // command and commandlastrun are set (after) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Less(t, lastRun, run.Checklists[0].Items[0].CommandLastRun) - }) - - t.Run("set a the same command", func(t *testing.T) { - lastRun := run.Checklists[0].Items[0].CommandLastRun - - // set command - err = e.PlaybooksClient.PlaybookRuns.SetItemCommand(context.Background(), run.ID, 0, 0, "/playbook todo") - require.NoError(t, err) - - // command and commandlastrun are set (after) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, "/playbook todo", run.Checklists[0].Items[0].Command) - require.Equal(t, lastRun, run.Checklists[0].Items[0].CommandLastRun) - }) - - t.Run("set a different command", func(t *testing.T) { - // set command - err = e.PlaybooksClient.PlaybookRuns.SetItemCommand(context.Background(), run.ID, 0, 0, "/playbook finish") - require.NoError(t, err) - - // command and commandlastrun are set (after) - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(t, err) - require.Equal(t, "/playbook finish", run.Checklists[0].Items[0].Command) - require.Zero(t, run.Checklists[0].Items[0].CommandLastRun) - }) -} - -func TestGetOwners(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - ownerFromUser := func(u *model.User) client.OwnerInfo { - return client.OwnerInfo{ - UserID: u.Id, - Username: u.Username, - FirstName: u.FirstName, - LastName: u.LastName, - Nickname: u.Nickname, - } - } - - _, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - - _, err = e.PlaybooksClient2.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Run name", - OwnerUserID: e.RegularUser2.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(t, err) - - fullOwner1 := ownerFromUser(e.RegularUser) - fullOwner2 := ownerFromUser(e.RegularUser2) - partialOwner1 := fullOwner1 - partialOwner1.FirstName = "" - partialOwner1.LastName = "" - partialOwner2 := fullOwner2 - partialOwner2.FirstName = "" - partialOwner2.LastName = "" - for _, tc := range []struct { - Name string - ShowFullName bool - Client *client.Client - MustContain []client.OwnerInfo - }{ - { - Name: "showfullname set to true", - ShowFullName: true, - Client: e.PlaybooksClient, - MustContain: []client.OwnerInfo{fullOwner1, fullOwner2}, - }, - { - Name: "showfullname set to false", - ShowFullName: false, - Client: e.PlaybooksClient, - MustContain: []client.OwnerInfo{partialOwner1, partialOwner2}, - }, - { - Name: "showfullname set to false and sysadmin", - ShowFullName: false, - Client: e.PlaybooksAdminClient, - MustContain: []client.OwnerInfo{fullOwner1, fullOwner2}, - }, - } { - t.Run(tc.Name, func(t *testing.T) { - cfg := e.Srv.Config() - cfg.PrivacySettings.ShowFullName = model.NewBool(tc.ShowFullName) - _, _, err = e.ServerAdminClient.UpdateConfig(context.Background(), cfg) - require.NoError(t, err) - - owners, err := tc.Client.PlaybookRuns.GetOwners(context.Background()) - require.NoError(t, err) - require.Len(t, owners, len(tc.MustContain)) - for _, mc := range tc.MustContain { - require.Contains(t, owners, mc) - } - }) - } -} diff --git a/server/playbooks/server/api_settings_test.go b/server/playbooks/server/api_settings_test.go deleted file mode 100644 index 061b2db50e1..00000000000 --- a/server/playbooks/server/api_settings_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "net/http" - "testing" - - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSettings(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("get settings", func(t *testing.T) { - t.Run("unauthenticated", func(t *testing.T) { - settings, err := e.UnauthenticatedPlaybooksClient.Settings.Get(context.Background()) - assert.Nil(t, settings) - requireErrorWithStatusCode(t, err, http.StatusUnauthorized) - }) - - t.Run("get some settings", func(t *testing.T) { - defaultSettings := &client.GlobalSettings{ - EnableExperimentalFeatures: false, - } - - settings, err := e.PlaybooksClient.Settings.Get(context.Background()) - require.NoError(t, err) - assert.Equal(t, defaultSettings, settings) - }) - }) -} diff --git a/server/playbooks/server/api_stats_test.go b/server/playbooks/server/api_stats_test.go deleted file mode 100644 index 7d97503e2cb..00000000000 --- a/server/playbooks/server/api_stats_test.go +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "fmt" - "net/http" - "testing" - - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/guregu/null.v4" -) - -func TestGetSiteStats(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("get sites stats", func(t *testing.T) { - t.Run("unauthenticated", func(t *testing.T) { - stats, err := e.UnauthenticatedPlaybooksClient.Stats.GetSiteStats(context.Background()) - assert.Nil(t, stats) - requireErrorWithStatusCode(t, err, http.StatusUnauthorized) - }) - - t.Run("get stats for basic server", func(t *testing.T) { - stats, err := e.PlaybooksAdminClient.Stats.GetSiteStats(context.Background()) - require.NoError(t, err) - assert.NotEmpty(t, stats) - assert.Equal(t, 4, stats.TotalPlaybooks) - assert.Equal(t, 1, stats.TotalPlaybookRuns) - }) - - t.Run("add extra playbooks/runs and get stats again", func(t *testing.T) { - e.CreateBasicPlaybook() - e.CreateBasicRun() - e.CreateBasicRun() - - stats, err := e.PlaybooksAdminClient.Stats.GetSiteStats(context.Background()) - require.NoError(t, err) - assert.NotEmpty(t, stats) - assert.Equal(t, 6, stats.TotalPlaybooks) - assert.Equal(t, 3, stats.TotalPlaybookRuns) - }) - }) -} - -func TestPlaybookKeyMetricsStats(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("3 runs with published metrics, 2 runs without publishing", func(t *testing.T) { - playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "pb1", - TeamID: e.BasicTeam.Id, - Public: true, - Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency, client.MetricTypeDuration}), - }) - require.NoError(e.T, err) - - pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID) - require.NoError(e.T, err) - - metricsData := createMetricsData(pb.Metrics, [][]int64{{12312, 9123}, {653, 7262}, {322, 76575}}) - // create runs and publish metrics data - createRunsWithMetrics(t, e, playbookID, metricsData, true) - // create runs, set metrics data, but do not publish - createRunsWithMetrics(t, e, playbookID, metricsData[1:], false) - - stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID) - require.NoError(t, err) - require.Equal(t, stats.MetricOverallAverage, intsToNullInts([]int64{4429, 30986})) - require.Equal(t, stats.MetricRollingAverage, intsToNullInts([]int64{4429, 30986})) - require.Equal(t, stats.MetricRollingAverageChange, []null.Int{null.NewInt(0, false), null.NewInt(0, false)}) - require.Equal(t, stats.MetricRollingValues, [][]int64{{322, 653, 12312}, {76575, 7262, 9123}}) - require.Equal(t, stats.MetricValueRange, [][]int64{{322, 12312}, {7262, 76575}}) - }) - - t.Run("13 runs with published metrics, 7 runs without publishing", func(t *testing.T) { - playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "pb2", - TeamID: e.BasicTeam.Id, - Public: true, - Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency, client.MetricTypeInteger, client.MetricTypeDuration}), - }) - require.NoError(e.T, err) - - pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID) - require.NoError(e.T, err) - - data := make([][]int64, 15) - for i := range data { - data[i] = []int64{100 + int64(i), 2000000 + int64(i), 3000000000 + int64(i)} - } - metricsData := createMetricsData(pb.Metrics, data) - createRunsWithMetrics(t, e, playbookID, metricsData, true) - createRunsWithMetrics(t, e, playbookID, metricsData[8:], false) - - stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID) - require.NoError(t, err) - require.Equal(t, stats.MetricOverallAverage, intsToNullInts([]int64{107, 2000007, 3000000007})) - require.Equal(t, stats.MetricRollingAverage, intsToNullInts([]int64{109, 2000009, 3000000009})) // last 10 runs average - require.Equal(t, stats.MetricRollingAverageChange, intsToNullInts([]int64{6, 0, 0})) - require.Equal(t, stats.MetricRollingValues, - [][]int64{ - {114, 113, 112, 111, 110, 109, 108, 107, 106, 105}, - {2000014, 2000013, 2000012, 2000011, 2000010, 2000009, 2000008, 2000007, 2000006, 2000005}, - {3000000014, 3000000013, 3000000012, 3000000011, 3000000010, 3000000009, 3000000008, 3000000007, 3000000006, 3000000005}, - }) - require.Equal(t, stats.MetricValueRange, [][]int64{{100, 114}, {2000000, 2000014}, {3000000000, 3000000014}}) - }) - - t.Run("23 runs with published metrics", func(t *testing.T) { - playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "pb3", - TeamID: e.BasicTeam.Id, - Public: true, - Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency}), - }) - require.NoError(e.T, err) - - pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID) - require.NoError(e.T, err) - - data := make([][]int64, 23) - for i := range data { - data[i] = []int64{10 + int64(i)} //11, 12, 13 ... 32 - } - metricsData := createMetricsData(pb.Metrics, data) - createRunsWithMetrics(t, e, playbookID, metricsData, true) - - stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID) - require.NoError(t, err) - require.Equal(t, stats.MetricOverallAverage, intsToNullInts([]int64{21})) - require.Equal(t, stats.MetricRollingAverage, intsToNullInts([]int64{27})) // last 10 runs average - require.Equal(t, stats.MetricRollingAverageChange, intsToNullInts([]int64{58})) - require.Equal(t, stats.MetricRollingValues, [][]int64{{32, 31, 30, 29, 28, 27, 26, 25, 24, 23}}) - require.Equal(t, stats.MetricValueRange, [][]int64{{10, 32}}) - }) - - t.Run("publish runs with metrics, then add additional metric to the playbook", func(t *testing.T) { - playbookID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "pb4", - TeamID: e.BasicTeam.Id, - Public: true, - Metrics: createMetricsConfigs([]string{client.MetricTypeCurrency}), - }) - require.NoError(e.T, err) - - pb, err := e.PlaybooksClient.Playbooks.Get(context.Background(), playbookID) - require.NoError(e.T, err) - - metricsData := createMetricsData(pb.Metrics, [][]int64{{2}, {1}, {2}, {7}, {3}, {5}, {1}, {7}, {2}, {3}, {5}, {6}, {7}, {1}}) - createRunsWithMetrics(t, e, playbookID, metricsData, true) - - // add a metric to the playbook at first position - pb.Metrics = append(pb.Metrics, pb.Metrics[0]) - pb.Metrics[0] = client.PlaybookMetricConfig{ - Title: "metric2", - Type: client.MetricTypeInteger, - } - - err = e.PlaybooksClient.Playbooks.Update(context.Background(), *pb) - require.NoError(e.T, err) - - stats, err := e.PlaybooksClient.Playbooks.Stats(context.Background(), playbookID) - require.NoError(t, err) - require.Equal(t, stats.MetricOverallAverage, []null.Int{null.NewInt(0, false), null.IntFrom(3)}) - require.Equal(t, stats.MetricRollingAverage, []null.Int{null.NewInt(0, false), null.IntFrom(4)}) // last 10 runs average - require.Equal(t, stats.MetricRollingAverageChange, []null.Int{null.NewInt(0, false), null.IntFrom(33)}) - require.Equal(t, stats.MetricRollingValues, [][]int64{nil, {1, 7, 6, 5, 3, 2, 7, 1, 5, 3}}) - require.Equal(t, stats.MetricValueRange, [][]int64{nil, {1, 7}}) - }) -} - -func createRunsWithMetrics(t *testing.T, e *TestEnvironment, playbookID string, metricsData [][]client.RunMetricData, publish bool) { - for i, md := range metricsData { - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: fmt.Sprint("run", i), - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: playbookID, - }) - assert.NoError(t, err) - assert.NotNil(t, run) - - retrospective := client.RetrospectiveUpdate{ - Text: fmt.Sprint("retro text", i), - Metrics: md, - } - - //publish or save retro info - if publish { - err = e.PlaybooksClient.PlaybookRuns.PublishRetrospective(context.Background(), run.ID, e.RegularUser.Id, retrospective) - } else { - err = e.PlaybooksClient.PlaybookRuns.UpdateRetrospective(context.Background(), run.ID, e.RegularUser.Id, retrospective) - } - assert.NoError(t, err) - } -} - -func createMetricsData(metricsConfigs []client.PlaybookMetricConfig, data [][]int64) [][]client.RunMetricData { - metricsData := make([][]client.RunMetricData, len(data)) - for i, d := range data { - md := make([]client.RunMetricData, len(metricsConfigs)) - for j, c := range metricsConfigs { - md[j] = client.RunMetricData{MetricConfigID: c.ID, Value: null.IntFrom(d[j])} - } - metricsData[i] = md - } - return metricsData -} - -func createMetricsConfigs(types []string) []client.PlaybookMetricConfig { - configs := make([]client.PlaybookMetricConfig, len(types)) - for i, t := range types { - configs[i] = client.PlaybookMetricConfig{ - Title: fmt.Sprint("metric", i), - Type: t, - } - } - return configs -} - -func intsToNullInts(nums []int64) []null.Int { - res := make([]null.Int, len(nums)) - for i := range nums { - res[i] = null.IntFrom(nums[i]) - } - return res -} diff --git a/server/playbooks/server/api_telemetry_test.go b/server/playbooks/server/api_telemetry_test.go deleted file mode 100644 index 3a154381e7c..00000000000 --- a/server/playbooks/server/api_telemetry_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestCreateEvent(t *testing.T) { - e := Setup(t) - e.CreateBasic() - - t.Run("create an event with bad type fails", func(t *testing.T) { - err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "run_status_update", "bad_type", nil) - require.Error(t, err) - }) - - t.Run("create an event with bad name fails", func(t *testing.T) { - err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "bad_name", "page", nil) - require.Error(t, err) - }) - - t.Run("create an event correctly with no extra data", func(t *testing.T) { - err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "run_status_update", "page", nil) - require.NoError(t, err) - }) - - t.Run("create an event correctly with extra data", func(t *testing.T) { - extra := map[string]interface{}{ - "foo": "bar", - "baz": 5, - } - err := e.PlaybooksClient.Telemetry.CreateEvent(context.Background(), "run_status_update", "page", extra) - require.NoError(t, err) - }) -} diff --git a/server/playbooks/server/app/action.go b/server/playbooks/server/app/action.go deleted file mode 100644 index ea29d51e1c6..00000000000 --- a/server/playbooks/server/app/action.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import "github.com/mattermost/mattermost/server/public/model" - -type GenericChannelActionWithoutPayload struct { - ID string `json:"id"` - ChannelID string `json:"channel_id"` - Enabled bool `json:"enabled"` - DeleteAt int64 `json:"delete_at"` - ActionType ActionType `json:"action_type"` - TriggerType TriggerType `json:"trigger_type"` -} - -type GenericChannelAction struct { - GenericChannelActionWithoutPayload - Payload interface{} `json:"payload"` -} - -type WelcomeMessagePayload struct { - Message string `json:"message" mapstructure:"message"` -} - -type PromptRunPlaybookFromKeywordsPayload struct { - Keywords []string `json:"keywords" mapstructure:"keywords"` - PlaybookID string `json:"playbook_id" mapstructure:"playbook_id"` -} - -type CategorizeChannelPayload struct { - CategoryName string `json:"category_name" mapstructure:"category_name"` -} - -type ActionType string -type TriggerType string - -const ( - // Action types: add new types to the ValidTriggerTypes array below - ActionTypeWelcomeMessage ActionType = "send_welcome_message" - ActionTypePromptRunPlaybook ActionType = "prompt_run_playbook" - ActionTypeCategorizeChannel ActionType = "categorize_channel" - - // Trigger types: add new types to the ValidTriggerTypes array below - TriggerTypeNewMemberJoins TriggerType = "new_member_joins" - TriggerTypeKeywordsPosted TriggerType = "keywords" -) - -var ValidActionTypes = []ActionType{ - ActionTypeWelcomeMessage, - ActionTypePromptRunPlaybook, - ActionTypeCategorizeChannel, -} - -var ValidTriggerTypes = []TriggerType{ - TriggerTypeNewMemberJoins, - TriggerTypeKeywordsPosted, -} - -type GetChannelActionOptions struct { - ActionType ActionType - TriggerType TriggerType -} - -type ChannelActionService interface { - // Create creates a new action - Create(action GenericChannelAction) (string, error) - - // Get returns the action identified by id - Get(id string) (GenericChannelAction, error) - - // GetChannelActions returns all actions in channelID, - // filtered with the options if different from its zero value - GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error) - - // Validate checks that the action type, trigger type and - // payload are all valid and consistent with each other - Validate(action GenericChannelAction) error - - // Update updates an existing action identified by action.ID - Update(action GenericChannelAction, userID string) error - - // UserHasJoinedChannel is called when userID has joined channelID. If actorID is not blank, userID - // was invited by actorID. - UserHasJoinedChannel(userID, channelID, actorID string) - - // CheckAndSendMessageOnJoin checks if userID has viewed channelID and sends - // the registered welcome message action. Returns true if the message was sent. - CheckAndSendMessageOnJoin(userID, channelID string) bool - - // MessageHasBeenPosted suggests playbooks to the user if triggered - MessageHasBeenPosted(post *model.Post) -} - -type ChannelActionStore interface { - // Create creates a new action - Create(action GenericChannelAction) (string, error) - - // Get returns the action identified by id - Get(id string) (GenericChannelAction, error) - - // GetChannelActions returns all actions in channelID, - // filtered with the options if different from its zero value - GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error) - - // Update updates an existing action identified by action.ID - Update(action GenericChannelAction) error - - // HasViewedChannel returns true if userID has viewed channelID - HasViewedChannel(userID, channelID string) bool - - // SetViewedChannel records that userID has viewed channelID. NOTE: does not check if there is already a - // record of that userID/channelID (i.e., will create duplicate rows) - SetViewedChannel(userID, channelID string) error - - // SetViewedChannel records that all users in userIDs have viewed channelID. - SetMultipleViewedChannel(userIDs []string, channelID string) error -} - -// ChannelActionTelemetry defines the methods that the ChannelAction service needs from RudderTelemetry. -// userID is the user initiating the event. -type ChannelActionTelemetry interface { - // RunChannelAction tracks the execution of a channel action, performed by the specified user. - RunChannelAction(action GenericChannelAction, userID string) - - // UpdateChannelAction tracks the update of a channel action, performed by the specified user. - UpdateChannelAction(action GenericChannelAction, userID string) -} diff --git a/server/playbooks/server/app/actions_service.go b/server/playbooks/server/app/actions_service.go deleted file mode 100644 index 1e0f99d6a89..00000000000 --- a/server/playbooks/server/app/actions_service.go +++ /dev/null @@ -1,578 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/mitchellh/mapstructure" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -type PlaybookGetter interface { - Get(id string) (Playbook, error) -} - -type channelActionServiceImpl struct { - poster bot.Poster - configService config.Service - store ChannelActionStore - api playbooks.ServicesAPI - playbookGetter PlaybookGetter - keywordsThreadIgnorer KeywordsThreadIgnorer - telemetry ChannelActionTelemetry -} - -func NewChannelActionsService(api playbooks.ServicesAPI, poster bot.Poster, configService config.Service, store ChannelActionStore, playbookGetter PlaybookGetter, keywordsThreadIgnorer KeywordsThreadIgnorer, telemetry ChannelActionTelemetry) ChannelActionService { - return &channelActionServiceImpl{ - poster: poster, - configService: configService, - store: store, - api: api, - playbookGetter: playbookGetter, - keywordsThreadIgnorer: keywordsThreadIgnorer, - telemetry: telemetry, - } -} - -// setViewedChannelForEveryMember mark channelID as viewed for all its existing members -func (a *channelActionServiceImpl) setViewedChannelForEveryMember(channelID string) error { - // TODO: this is a magic number, we should load test this function to find a - // good threshold to share the workload between the goroutines - perPage := 200 - - page := 0 - var wg sync.WaitGroup - var goroutineErr error - - for { - members, err := a.api.GetChannelMembers(channelID, page, perPage) - if err != nil { - return fmt.Errorf("unable to retrieve members of channel with ID %q", channelID) - } - - if len(members) == 0 { - break - } - - wg.Add(1) - go func() { - defer wg.Done() - - userIDs := make([]string, 0, len(members)) - for _, member := range members { - userIDs = append(userIDs, member.UserId) - } - - if err := a.store.SetMultipleViewedChannel(userIDs, channelID); err != nil { - // We don't care whether multiple goroutines assign this value, as we're - // only interested in knowing if there was at least one error - goroutineErr = errors.Wrapf(err, "unable to mark channel with ID %q as viewed for users %v", channelID, userIDs) - } - }() - - page++ - } - - wg.Wait() - - return goroutineErr -} - -func (a *channelActionServiceImpl) Create(action GenericChannelAction) (string, error) { - actions, err := a.store.GetChannelActions(action.ChannelID, GetChannelActionOptions{ - ActionType: action.ActionType, - TriggerType: action.TriggerType, - }) - if err != nil { - return "", err - } - - if len(actions) > 0 { - return "", fmt.Errorf("only one action of action type %q and trigger type %q is allowed", string(action.ActionType), string(action.TriggerType)) - } - - if action.ActionType == ActionTypeWelcomeMessage && action.Enabled { - if err := a.setViewedChannelForEveryMember(action.ChannelID); err != nil { - return "", err - } - } - - return a.store.Create(action) -} - -func (a *channelActionServiceImpl) Get(id string) (GenericChannelAction, error) { - return a.store.Get(id) -} - -func (a *channelActionServiceImpl) GetChannelActions(channelID string, options GetChannelActionOptions) ([]GenericChannelAction, error) { - return a.store.GetChannelActions(channelID, options) -} - -func (a *channelActionServiceImpl) Validate(action GenericChannelAction) error { - // Validate the trigger type and action types - switch action.TriggerType { - case TriggerTypeNewMemberJoins: - switch action.ActionType { - case ActionTypeWelcomeMessage: - break - case ActionTypeCategorizeChannel: - break - default: - return fmt.Errorf("action type %q is not valid for trigger type %q", action.ActionType, action.TriggerType) - } - case TriggerTypeKeywordsPosted: - if action.ActionType != ActionTypePromptRunPlaybook { - return fmt.Errorf("action type %q is not valid for trigger type %q", action.ActionType, action.TriggerType) - } - default: - return fmt.Errorf("trigger type %q not recognized", action.TriggerType) - } - - // Validate the payload depending on the action type - switch action.ActionType { - case ActionTypeWelcomeMessage: - var payload WelcomeMessagePayload - if err := mapstructure.Decode(action.Payload, &payload); err != nil { - return fmt.Errorf("unable to decode payload from action") - } - case ActionTypePromptRunPlaybook: - var payload PromptRunPlaybookFromKeywordsPayload - if err := mapstructure.Decode(action.Payload, &payload); err != nil { - return fmt.Errorf("unable to decode payload from action") - } - if err := checkValidPromptRunPlaybookFromKeywordsPayload(payload); err != nil { - return err - } - case ActionTypeCategorizeChannel: - var payload CategorizeChannelPayload - if err := mapstructure.Decode(action.Payload, &payload); err != nil { - return fmt.Errorf("unable to decode payload from action") - } - - default: - return fmt.Errorf("action type %q not recognized", action.ActionType) - } - - return nil -} - -func checkValidPromptRunPlaybookFromKeywordsPayload(payload PromptRunPlaybookFromKeywordsPayload) error { - for _, keyword := range payload.Keywords { - if keyword == "" { - return fmt.Errorf("payload field 'keywords' must contain only non-empty keywords") - } - } - - if payload.PlaybookID != "" && !model.IsValidId(payload.PlaybookID) { - return fmt.Errorf("payload field 'playbook_id' must be a valid ID") - } - - return nil -} - -func (a *channelActionServiceImpl) Update(action GenericChannelAction, userID string) error { - oldAction, err := a.Get(action.ID) - if err != nil { - return fmt.Errorf("unable to retrieve existing action with ID %q", action.ID) - } - - if action.ActionType == ActionTypeWelcomeMessage && !oldAction.Enabled && action.Enabled { - if err := a.setViewedChannelForEveryMember(action.ChannelID); err != nil { - return err - } - } - - if err := a.store.Update(action); err != nil { - return err - } - - a.telemetry.UpdateChannelAction(action, userID) - - return nil -} - -// UserHasJoinedChannel is called when userID has joined channelID. If actorID is not blank, userID -// was invited by actorID. -func (a *channelActionServiceImpl) UserHasJoinedChannel(userID, channelID, actorID string) { - user, err := a.api.GetUserByID(userID) - if err != nil { - logrus.WithError(err).WithField("user_id", userID).Error("failed to resolve user") - return - } - - channel, err := a.api.GetChannelByID(channelID) - if err != nil { - logrus.WithError(err).WithField("channel_id", channelID).Error("failed to resolve channel") - return - } - - if user.IsBot { - return - } - - actions, err := a.GetChannelActions(channelID, GetChannelActionOptions{ - ActionType: ActionTypeCategorizeChannel, - TriggerType: TriggerTypeNewMemberJoins, - }) - if err != nil { - logrus.WithError(err).WithField("channel_id", channelID).Error("failed to get the channel actions") - return - } - - if len(actions) > 1 { - logrus.WithFields(logrus.Fields{ - "action_type": ActionTypeCategorizeChannel, - "trigger_type": TriggerTypeNewMemberJoins, - "num_actions": len(actions), - }).Error("expected only one action to be retrieved") - } - - if len(actions) != 1 { - return - } - - action := actions[0] - if !action.Enabled { - return - } - - var payload CategorizeChannelPayload - if err = mapstructure.Decode(action.Payload, &payload); err != nil { - logrus.WithError(err).Error("unable to decode payload of CategorizeChannelPayload") - return - } - - if payload.CategoryName != "" { - // Update sidebar category in the go-routine not to block the UserHasJoinedChannel hook - go func() { - // Wait for 5 seconds(a magic number) for the webapp to get the `user_added` event, - // finish channel categorization and update it's state in redux. - // Currently there is no way to detect when webapp finishes the job. - // After that we can update the categories safely. - // Technically if user starts multiple runs simultaneously we will still get the race condition - // on category update. Since that's not realistic at the moment we are not adding the - // distributed lock here. - time.Sleep(5 * time.Second) - - err = a.createOrUpdatePlaybookRunSidebarCategory(userID, channelID, channel.TeamId, payload.CategoryName) - if err != nil { - logrus.WithError(err).Error("failed to categorize channel") - } - - a.telemetry.RunChannelAction(action, userID) - }() - } -} - -// createOrUpdatePlaybookRunSidebarCategory creates or updates a "Playbook Runs" sidebar category if -// it does not already exist and adds the channel within the sidebar category -func (a *channelActionServiceImpl) createOrUpdatePlaybookRunSidebarCategory(userID, channelID, teamID, categoryName string) error { - sidebar, err := a.api.GetChannelSidebarCategories(userID, teamID) - if err != nil { - return err - } - - var categoryID string - for _, category := range sidebar.Categories { - if strings.EqualFold(category.DisplayName, categoryName) { - categoryID = category.Id - if !sliceContains(category.Channels, channelID) { - category.Channels = append(category.Channels, channelID) - } - break - } - } - - if categoryID == "" { - _, err = a.api.CreateChannelSidebarCategory(userID, teamID, &model.SidebarCategoryWithChannels{ - SidebarCategory: model.SidebarCategory{ - UserId: userID, - TeamId: teamID, - DisplayName: categoryName, - Muted: false, - }, - Channels: []string{channelID}, - }) - if err != nil { - return err - } - - return nil - } - - // remove channel from previous category - for _, category := range sidebar.Categories { - if strings.EqualFold(category.DisplayName, categoryName) { - continue - } - for i, channel := range category.Channels { - if channel == channelID { - category.Channels = append(category.Channels[:i], category.Channels[i+1:]...) - break - } - } - } - - _, err = a.api.UpdateChannelSidebarCategories(userID, teamID, sidebar.Categories) - if err != nil { - return err - } - - return nil -} - -func sliceContains(strs []string, target string) bool { - for _, s := range strs { - if s == target { - return true - } - } - return false -} - -// CheckAndSendMessageOnJoin checks if userID has viewed channelID and sends -// playbookRun.MessageOnJoin if it exists. Returns true if the message was sent. -func (a *channelActionServiceImpl) CheckAndSendMessageOnJoin(userID, channelID string) bool { - hasViewed := a.store.HasViewedChannel(userID, channelID) - - if hasViewed { - return true - } - - actions, err := a.store.GetChannelActions(channelID, GetChannelActionOptions{ - TriggerType: TriggerTypeNewMemberJoins, - }) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "channel_id": channelID, - "trigger_type": TriggerTypeNewMemberJoins, - }).Error("failed to resolve actions") - return false - } - - if err = a.store.SetViewedChannel(userID, channelID); err != nil { - // If duplicate entry, userID has viewed channelID. If not a duplicate, assume they haven't. - return errors.Is(err, ErrDuplicateEntry) - } - - // Look for the ActionTypeWelcomeMessage action - for _, action := range actions { - if action.ActionType == ActionTypeWelcomeMessage { - var payload WelcomeMessagePayload - if err := mapstructure.Decode(action.Payload, &payload); err != nil { - logrus.WithError(err).WithField("action_type", action.ActionType).Error("payload of action is not valid") - } - - // Run the action - a.poster.SystemEphemeralPost(userID, channelID, &model.Post{ - Message: payload.Message, - }) - - a.telemetry.RunChannelAction(action, userID) - } - } - - return true -} - -func (a *channelActionServiceImpl) MessageHasBeenPosted(post *model.Post) { - if post.IsSystemMessage() || a.keywordsThreadIgnorer.IsIgnored(post.RootId, post.UserId) || a.poster.IsFromPoster(post) { - return - } - - actions, err := a.GetChannelActions(post.ChannelId, GetChannelActionOptions{ - TriggerType: TriggerTypeKeywordsPosted, - ActionType: ActionTypePromptRunPlaybook, - }) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "channel_id": post.ChannelId, - "trigger_type": TriggerTypeKeywordsPosted, - }).Error("unable to retrieve channel actions") - return - } - - // Finish early if there are no actions to prompt running a playbook - if len(actions) == 0 { - return - } - - triggeredPlaybooksMap := make(map[string]Playbook) - presentTriggers := []string{} - for _, action := range actions { - if !action.Enabled { - continue - } - - var payload PromptRunPlaybookFromKeywordsPayload - if err := mapstructure.Decode(action.Payload, &payload); err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "payload": payload, - "actionType": action.ActionType, - "triggerType": action.TriggerType, - }).Error("unable to decode payload from action") - continue - } - - if len(payload.Keywords) == 0 || payload.PlaybookID == "" { - continue - } - - suggestedPlaybook, err := a.playbookGetter.Get(payload.PlaybookID) - if err != nil { - logrus.WithError(err).WithField("playbook_id", payload.PlaybookID).Error("unable to get playbook to run action") - continue - } - - triggers := payload.Keywords - actionExecuted := false - for _, trigger := range triggers { - if strings.Contains(post.Message, trigger) || containsAttachments(post.Attachments(), trigger) { - triggeredPlaybooksMap[payload.PlaybookID] = suggestedPlaybook - presentTriggers = append(presentTriggers, trigger) - actionExecuted = true - } - } - - if actionExecuted { - a.telemetry.RunChannelAction(action, post.UserId) - } - } - - if len(triggeredPlaybooksMap) == 0 { - return - } - - triggeredPlaybooks := []Playbook{} - for _, playbook := range triggeredPlaybooksMap { - triggeredPlaybooks = append(triggeredPlaybooks, playbook) - } - - message := getPlaybookSuggestionsMessage(triggeredPlaybooks, presentTriggers) - attachment := getPlaybookSuggestionsSlackAttachment(triggeredPlaybooks, post.Id, "playbooks") - - rootID := post.RootId - if rootID == "" { - rootID = post.Id - } - - newPost := &model.Post{ - Message: message, - ChannelId: post.ChannelId, - } - model.ParseSlackAttachment(newPost, []*model.SlackAttachment{attachment}) - if err := a.poster.PostMessageToThread(rootID, newPost); err != nil { - logrus.WithError(err).Error("unable to post message with suggestions to run playbooks") - } -} - -func getPlaybookSuggestionsMessage(suggestedPlaybooks []Playbook, triggers []string) string { - message := "" - triggerMessage := "" - if len(triggers) == 1 { - triggerMessage = fmt.Sprintf("`%s` is a trigger", triggers[0]) - } else { - triggerMessage = fmt.Sprintf("`%s` are triggers", strings.Join(triggers, "`, `")) - } - - if len(suggestedPlaybooks) == 1 { - playbookURL := fmt.Sprintf("[%s](%s)", suggestedPlaybooks[0].Title, GetPlaybookDetailsRelativeURL(suggestedPlaybooks[0].ID)) - message = fmt.Sprintf("%s for the %s playbook, would you like to run it?", triggerMessage, playbookURL) - } else { - message = fmt.Sprintf("%s for the multiple playbooks, would you like to run one of them?", triggerMessage) - } - - return message -} - -func getPlaybookSuggestionsSlackAttachment(playbooks []Playbook, triggeringPostID string, pluginID string) *model.SlackAttachment { - ignoreButton := &model.PostAction{ - Id: "ignoreKeywordsButton", - Name: "No, ignore thread", - Type: model.PostActionTypeButton, - Integration: &model.PostActionIntegration{ - URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/ignore-thread", pluginID), - Context: map[string]interface{}{ - "postID": triggeringPostID, - }, - }, - } - - if len(playbooks) == 1 { - yesButton := &model.PostAction{ - Id: "runPlaybookButton", - Name: "Yes, run playbook", - Type: model.PostActionTypeButton, - Integration: &model.PostActionIntegration{ - URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/run-playbook", pluginID), - Context: map[string]interface{}{ - "postID": triggeringPostID, - "selected_option": playbooks[0].ID, - }, - }, - Style: "primary", - } - - attachment := &model.SlackAttachment{ - Actions: []*model.PostAction{yesButton, ignoreButton}, - Text: "Open Channel Actions in the channel header to view and edit keywords.", - } - return attachment - } - - options := []*model.PostActionOptions{} - for _, playbook := range playbooks { - option := &model.PostActionOptions{ - Value: playbook.ID, - Text: playbook.Title, - } - options = append(options, option) - } - playbookChooser := &model.PostAction{ - Id: "playbookChooser", - Name: "Select a playbook to run", - Type: model.PostActionTypeSelect, - Integration: &model.PostActionIntegration{ - URL: fmt.Sprintf("/plugins/%s/api/v0/signal/keywords/run-playbook", pluginID), - Context: map[string]interface{}{ - "postID": triggeringPostID, - }, - }, - Options: options, - Style: "primary", - } - - attachment := &model.SlackAttachment{ - Actions: []*model.PostAction{playbookChooser, ignoreButton}, - } - return attachment -} - -func containsAttachments(attachments []*model.SlackAttachment, trigger string) bool { - // Check PreText, Title, Text and Footer SlackAttachments fields for trigger. - for _, attachment := range attachments { - switch { - case strings.Contains(attachment.Pretext, trigger): - return true - case strings.Contains(attachment.Title, trigger): - return true - case strings.Contains(attachment.Text, trigger): - return true - case strings.Contains(attachment.Footer, trigger): - return true - default: - continue - } - } - return false -} diff --git a/server/playbooks/server/app/category.go b/server/playbooks/server/app/category.go deleted file mode 100644 index c64e8fc639d..00000000000 --- a/server/playbooks/server/app/category.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "errors" - "strings" -) - -type CategoryItemType string - -const ( - PlaybookItemType CategoryItemType = "p" - RunItemType CategoryItemType = "r" -) - -func StringToItemType(item string) (CategoryItemType, error) { - var convertedItem CategoryItemType - if item == string(PlaybookItemType) { - convertedItem = PlaybookItemType - } else if item == string(RunItemType) { - convertedItem = RunItemType - } else { - return PlaybookItemType, errors.New("unknown item type") - } - return convertedItem, nil -} - -type CategoryItem struct { - ItemID string `json:"item_id"` - Type CategoryItemType `json:"type"` - Name string `json:"name"` - Public bool `json:"public"` -} - -// Category represents sidebar category with items -type Category struct { - ID string `json:"id"` - Name string `json:"name"` - TeamID string `json:"team_id"` - UserID string `json:"user_id"` - Collapsed bool `json:"collapsed"` - CreateAt int64 `json:"create_at"` - UpdateAt int64 `json:"update_at"` - DeleteAt int64 `json:"delete_at"` - Items []CategoryItem `json:"items"` -} - -func (c *Category) IsValid() error { - if strings.TrimSpace(c.ID) == "" { - return errors.New("category ID cannot be empty") - } - - if strings.TrimSpace(c.Name) == "" { - return errors.New("category name cannot be empty") - } - - if strings.TrimSpace(c.UserID) == "" { - return errors.New("category user ID cannot be empty") - } - - if strings.TrimSpace(c.TeamID) == "" { - return errors.New("category team id ID cannot be empty") - } - - for _, item := range c.Items { - if item.ItemID == "" { - return errors.New("item ID cannot be empty") - } - if item.Type != PlaybookItemType && item.Type != RunItemType { - return errors.New("item type is incorrect") - } - } - - return nil -} - -func (c *Category) ContainsItem(item CategoryItem) bool { - for _, catItem := range c.Items { - if catItem.ItemID == item.ItemID && catItem.Type == item.Type { - return true - } - } - return false -} - -// CategoryService is the category service for managing categories -type CategoryService interface { - // Create creates a new Category - Create(category Category) (string, error) - - // Get retrieves category with categoryID for user for team - Get(categoryID string) (Category, error) - - // GetCategories retrieves all categories for user for team - GetCategories(teamID, userID string) ([]Category, error) - - // Update updates a category - Update(category Category) error - - // Delete deletes a category - Delete(categoryID string) error - - // AddFavorite favorites an item, which may be either run or playbook - AddFavorite(item CategoryItem, teamID, userID string) error - - // DeleteFavorite unfavorites an item, which may be either run or playbook - DeleteFavorite(item CategoryItem, teamID, userID string) error - - // IsItemFavorite returns whether item was favorited or not - IsItemFavorite(item CategoryItem, teamID, userID string) (bool, error) - - AreItemsFavorites(items []CategoryItem, teamID, userID string) ([]bool, error) -} - -type CategoryStore interface { - // Get retrieves a Category. Returns ErrNotFound if not found. - Get(id string) (Category, error) - - // Create creates a new Category - Create(category Category) error - - // GetCategories retrieves all categories for user for team - GetCategories(teamID, userID string) ([]Category, error) - - // Update updates a category - Update(category Category) error - - // Delete deletes a category - Delete(categoryID string) error - - // GetFavoriteCategory returns favorite category - GetFavoriteCategory(teamID, userID string) (Category, error) - - // AddItemToFavoriteCategory adds an item to favorite category, - // if favorite category does not exist it creates one - AddItemToFavoriteCategory(item CategoryItem, teamID, userID string) error - - // AddItemToCategory adds an item to category - AddItemToCategory(item CategoryItem, categoryID string) error - - // DeleteItemFromCategory adds an item to category - DeleteItemFromCategory(item CategoryItem, categoryID string) error -} - -type CategoryTelemetry interface { - // FavoriteItem tracks run favoriting of an item. Item can be run or a playbook - FavoriteItem(item CategoryItem, userID string) - - // UnfavoriteItem tracks run unfavoriting of an item. Item can be run or a playbook - UnfavoriteItem(item CategoryItem, userID string) -} diff --git a/server/playbooks/server/app/category_service.go b/server/playbooks/server/app/category_service.go deleted file mode 100644 index f94a9040aab..00000000000 --- a/server/playbooks/server/app/category_service.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "database/sql" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" -) - -type categoryService struct { - store CategoryStore - api playbooks.ServicesAPI - telemetry CategoryTelemetry -} - -// NewPlaybookService returns a new playbook service -func NewCategoryService(store CategoryStore, api playbooks.ServicesAPI, categoryTelemetry CategoryTelemetry) CategoryService { - return &categoryService{ - store: store, - api: api, - telemetry: categoryTelemetry, - } -} - -// Create creates a new Category -func (c *categoryService) Create(category Category) (string, error) { - if category.ID != "" { - return "", errors.New("ID should be empty") - } - category.ID = model.NewId() - category.CreateAt = model.GetMillis() - category.UpdateAt = category.CreateAt - if err := category.IsValid(); err != nil { - return "", errors.Wrap(err, "invalid category") - - } - - if err := c.store.Create(category); err != nil { - return "", errors.Wrap(err, "Can't create category") - } - return category.ID, nil -} - -func (c *categoryService) Get(categoryID string) (Category, error) { - category, err := c.store.Get(categoryID) - if err != nil { - return Category{}, errors.Wrap(err, "Can't get category") - } - return category, nil -} - -// GetCategories retrieves all categories for user for team -func (c *categoryService) GetCategories(teamID, userID string) ([]Category, error) { - if !model.IsValidId(teamID) { - return nil, errors.New("teamID is not valid") - } - if !model.IsValidId(userID) { - return nil, errors.New("userID is not valid") - } - return c.store.GetCategories(teamID, userID) -} - -// Update updates a category -func (c *categoryService) Update(category Category) error { - if category.ID == "" { - return errors.New("id should not be empty") - } - if category.Name == "" { - return errors.New("name should not be empty") - } - - category.UpdateAt = model.GetMillis() - if err := category.IsValid(); err != nil { - return errors.Wrap(err, "invalid category") - } - if err := c.store.Update(category); err != nil { - return errors.Wrap(err, "can't update category") - } - return nil -} - -// Delete deletes a category -func (c *categoryService) Delete(categoryID string) error { - if err := c.store.Delete(categoryID); err != nil { - return errors.Wrap(err, "can't delete category") - } - - return nil -} - -// AddFavorite favorites an item, which may be either run or playbook -func (c *categoryService) AddFavorite(item CategoryItem, teamID, userID string) error { - if err := c.store.AddItemToFavoriteCategory(item, teamID, userID); err != nil { - return errors.Wrap(err, "failed to add favorite") - } - - c.telemetry.FavoriteItem(item, userID) - return nil -} - -func (c *categoryService) DeleteFavorite(item CategoryItem, teamID, userID string) error { - favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID) - if err != nil { - return errors.Wrap(err, "can't get favorite category") - } - - found := false - for _, favItem := range favoriteCategory.Items { - if favItem.ItemID == item.ItemID && favItem.Type == item.Type { - found = true - } - } - if !found { - return errors.New("Item is not favorited") - } - if err := c.store.DeleteItemFromCategory(item, favoriteCategory.ID); err != nil { - return errors.Wrap(err, "can't delete item from favorite category") - } - - c.telemetry.UnfavoriteItem(item, userID) - return nil -} - -func (c *categoryService) IsItemFavorite(item CategoryItem, teamID, userID string) (bool, error) { - favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID) - if err == sql.ErrNoRows { - return false, nil - } else if err != nil { - return false, errors.Wrap(err, "can't get favorite category") - } - - found := false - for _, favItem := range favoriteCategory.Items { - if favItem.ItemID == item.ItemID && favItem.Type == item.Type { - found = true - } - } - return found, nil -} - -func (c *categoryService) AreItemsFavorites(items []CategoryItem, teamID, userID string) ([]bool, error) { - result := make([]bool, len(items)) - - favoriteCategory, err := c.store.GetFavoriteCategory(teamID, userID) - if err == sql.ErrNoRows { - return result, nil - } else if err != nil { - return result, errors.Wrap(err, "can't get favorite category") - } - - categoryResult := make(map[CategoryItem]bool) - for _, favItem := range favoriteCategory.Items { - categoryResult[CategoryItem{ - ItemID: favItem.ItemID, - Type: favItem.Type, - }] = true - } - - for i, item := range items { - result[i] = categoryResult[item] - } - return result, nil -} diff --git a/server/playbooks/server/app/errors.go b/server/playbooks/server/app/errors.go deleted file mode 100644 index 7605b3fd29a..00000000000 --- a/server/playbooks/server/app/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import "github.com/pkg/errors" - -// ErrNotFound used when an entity is not found. -var ErrNotFound = errors.New("not found") - -// ErrChannelDisplayNameInvalid is used when a channel name is too long. -var ErrChannelDisplayNameInvalid = errors.New("channel name is invalid or too long") - -// ErrPlaybookRunNotActive occurs when trying to run a command on a playbook run that has ended. -var ErrPlaybookRunNotActive = errors.New("already ended") - -// ErrPlaybookRunActive occurs when trying to run a command on a playbook run that is active. -var ErrPlaybookRunActive = errors.New("already active") - -// ErrMalformedPlaybookRun occurs when a playbook run is not valid. -var ErrMalformedPlaybookRun = errors.New("malformed") - -// ErrDuplicateEntry occurs when failing to insert because the entry already existed. -var ErrDuplicateEntry = errors.New("duplicate entry") diff --git a/server/playbooks/server/app/export.go b/server/playbooks/server/app/export.go deleted file mode 100644 index 8f27d01310c..00000000000 --- a/server/playbooks/server/app/export.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "reflect" -) - -const CurrentPlaybookExportVersion = 1 - -func getFieldsForExport(in interface{}) map[string]interface{} { - out := map[string]interface{}{} - - inType := reflect.TypeOf(in) - inValue := reflect.ValueOf(in) - for i := 0; i < inType.NumField(); i++ { - field := inType.Field(i) - tag := field.Tag.Get("export") - fieldValue := inValue.Field(i) - if tag != "" && tag != "-" && !fieldValue.IsZero() { - out[tag] = fieldValue.Interface() - } - } - - return out -} - -func generateChecklistItemExport(checklistItems []ChecklistItem) []interface{} { - exported := make([]interface{}, 0, len(checklistItems)) - for _, item := range checklistItems { - exportItem := getFieldsForExport(item) - exported = append(exported, exportItem) - } - - return exported -} - -func generateChecklistExport(checklists []Checklist) []interface{} { - exported := make([]interface{}, 0, len(checklists)) - for _, checklist := range checklists { - exportList := getFieldsForExport(checklist) - exportList["items"] = generateChecklistItemExport(checklist.Items) - exported = append(exported, exportList) - } - - return exported -} - -func generateMetricsExport(metrics []PlaybookMetricConfig) []interface{} { - exported := make([]interface{}, 0, len(metrics)) - for _, checklist := range metrics { - exportList := getFieldsForExport(checklist) - exported = append(exported, exportList) - } - - return exported -} - -// GeneratePlaybookExport returns a playbook in export format. -// Fields marked with the stuct tag "export" are included using the given string. -func GeneratePlaybookExport(playbook Playbook) ([]byte, error) { - export := getFieldsForExport(playbook) - export["version"] = CurrentPlaybookExportVersion - export["checklists"] = generateChecklistExport(playbook.Checklists) - export["metrics"] = generateMetricsExport(playbook.Metrics) - - result, err := json.MarshalIndent(export, "", " ") - if err != nil { - return nil, err - } - - return result, nil -} diff --git a/server/playbooks/server/app/export_test.go b/server/playbooks/server/app/export_test.go deleted file mode 100644 index 961ef347a6b..00000000000 --- a/server/playbooks/server/app/export_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "reflect" - "strings" - "testing" - - "gopkg.in/guregu/null.v4" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGeneratePlaybookExport(t *testing.T) { - pb := Playbook{ - Title: "Testing", - CreateAt: 23423234, - Checklists: []Checklist{ - { - Title: "checklist 1", - Items: []ChecklistItem{ - { - Title: "This is an item", - Description: "It's an item", - }, - }, - }, - }, - Metrics: []PlaybookMetricConfig{ - { - ID: "1", - PlaybookID: "11", - Title: "Title 1", - Description: "Description 1", - Type: MetricTypeCurrency, - Target: null.IntFrom(147), - }, - }, - } - - output, err := GeneratePlaybookExport(pb) - require.NoError(t, err) - - result := Playbook{} - err = json.Unmarshal(output, &result) - require.NoError(t, err) - - // Should copy the specified stuff - assert.Equal(t, result.Title, pb.Title) - - // Shouldn't copy the not specificed stuff - assert.Equal(t, result.CreateAt, int64(0)) - - // Shouldn't copy metrics ID and PlaybookID fields - assert.NotEqual(t, result.Metrics, pb.Metrics) - //After cleaning ID and PlaybookID, should be equal - pb.Metrics[0].ID = "" - pb.Metrics[0].PlaybookID = "" - assert.Equal(t, result.Metrics, pb.Metrics) - -} - -func definesExports(t *testing.T, thing interface{}) { - inType := reflect.TypeOf(thing) - for i := 0; i < inType.NumField(); i++ { - field := inType.Field(i) - tag := strings.TrimSpace(field.Tag.Get("export")) - if tag == "" { - t.Errorf("%s struct does not define export for field %s. Please define this struct tag, see comment above playbook struct.", inType.Name(), field.Name) - } - } -} - -func TestPlaybookDefinesExports(t *testing.T) { - definesExports(t, Playbook{}) - definesExports(t, Checklist{}) - definesExports(t, ChecklistItem{}) -} diff --git a/server/playbooks/server/app/keywords_ignore.go b/server/playbooks/server/app/keywords_ignore.go deleted file mode 100644 index d13affb1770..00000000000 --- a/server/playbooks/server/app/keywords_ignore.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import "sync" - -type KeywordsThreadIgnorer interface { - Ignore(postID, userID string) - IsIgnored(postID, userID string) bool -} - -type keywordsThreadIgnorerImpl struct { - ignoredThreads map[string]map[string]bool // [postID][userID] - mutex sync.RWMutex -} - -func NewKeywordsThreadIgnorer() KeywordsThreadIgnorer { - return &keywordsThreadIgnorerImpl{ - ignoredThreads: map[string]map[string]bool{}, - mutex: sync.RWMutex{}, - } -} - -// Ignores ignores thread postID for the userID, -// other users will still get notifications in this thread -func (i *keywordsThreadIgnorerImpl) Ignore(postID, userID string) { - i.mutex.Lock() - defer i.mutex.Unlock() - if _, ok := i.ignoredThreads[postID]; !ok { - i.ignoredThreads[postID] = map[string]bool{} - } - i.ignoredThreads[postID][userID] = true -} - -// IsIgnored checks whether this thread should be ignored for userID -func (i *keywordsThreadIgnorerImpl) IsIgnored(postID, userID string) bool { - i.mutex.RLock() - defer i.mutex.RUnlock() - if _, ok := i.ignoredThreads[postID]; !ok { - return false - } - return i.ignoredThreads[postID][userID] -} diff --git a/server/playbooks/server/app/mocks/mock_job_once_scheduler.go b/server/playbooks/server/app/mocks/mock_job_once_scheduler.go deleted file mode 100644 index 1970435543a..00000000000 --- a/server/playbooks/server/app/mocks/mock_job_once_scheduler.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/app (interfaces: JobOnceScheduler) - -// Package mock_app is a generated GoMock package. -package mock_app - -import ( - reflect "reflect" - time "time" - - gomock "github.com/golang/mock/gomock" - cluster "github.com/mattermost/mattermost/server/v8/playbooks/product/pluginapi/cluster" -) - -// MockJobOnceScheduler is a mock of JobOnceScheduler interface. -type MockJobOnceScheduler struct { - ctrl *gomock.Controller - recorder *MockJobOnceSchedulerMockRecorder -} - -// MockJobOnceSchedulerMockRecorder is the mock recorder for MockJobOnceScheduler. -type MockJobOnceSchedulerMockRecorder struct { - mock *MockJobOnceScheduler -} - -// NewMockJobOnceScheduler creates a new mock instance. -func NewMockJobOnceScheduler(ctrl *gomock.Controller) *MockJobOnceScheduler { - mock := &MockJobOnceScheduler{ctrl: ctrl} - mock.recorder = &MockJobOnceSchedulerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockJobOnceScheduler) EXPECT() *MockJobOnceSchedulerMockRecorder { - return m.recorder -} - -// Cancel mocks base method. -func (m *MockJobOnceScheduler) Cancel(arg0 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Cancel", arg0) -} - -// Cancel indicates an expected call of Cancel. -func (mr *MockJobOnceSchedulerMockRecorder) Cancel(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cancel", reflect.TypeOf((*MockJobOnceScheduler)(nil).Cancel), arg0) -} - -// ListScheduledJobs mocks base method. -func (m *MockJobOnceScheduler) ListScheduledJobs() ([]cluster.JobOnceMetadata, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListScheduledJobs") - ret0, _ := ret[0].([]cluster.JobOnceMetadata) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListScheduledJobs indicates an expected call of ListScheduledJobs. -func (mr *MockJobOnceSchedulerMockRecorder) ListScheduledJobs() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListScheduledJobs", reflect.TypeOf((*MockJobOnceScheduler)(nil).ListScheduledJobs)) -} - -// ScheduleOnce mocks base method. -func (m *MockJobOnceScheduler) ScheduleOnce(arg0 string, arg1 time.Time) (*cluster.JobOnce, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ScheduleOnce", arg0, arg1) - ret0, _ := ret[0].(*cluster.JobOnce) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ScheduleOnce indicates an expected call of ScheduleOnce. -func (mr *MockJobOnceSchedulerMockRecorder) ScheduleOnce(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleOnce", reflect.TypeOf((*MockJobOnceScheduler)(nil).ScheduleOnce), arg0, arg1) -} - -// SetCallback mocks base method. -func (m *MockJobOnceScheduler) SetCallback(arg0 func(string)) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetCallback", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetCallback indicates an expected call of SetCallback. -func (mr *MockJobOnceSchedulerMockRecorder) SetCallback(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCallback", reflect.TypeOf((*MockJobOnceScheduler)(nil).SetCallback), arg0) -} - -// Start mocks base method. -func (m *MockJobOnceScheduler) Start() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Start") - ret0, _ := ret[0].(error) - return ret0 -} - -// Start indicates an expected call of Start. -func (mr *MockJobOnceSchedulerMockRecorder) Start() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockJobOnceScheduler)(nil).Start)) -} diff --git a/server/playbooks/server/app/permissions_service.go b/server/playbooks/server/app/permissions_service.go deleted file mode 100644 index fa3d87ea5be..00000000000 --- a/server/playbooks/server/app/permissions_service.go +++ /dev/null @@ -1,545 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "reflect" - "strings" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// ErrNoPermissions if the error is caused by the user not having permissions -var ErrNoPermissions = errors.New("does not have permissions") - -// ErrLicensedFeature if the error is caused by the server not having the needed license for the feature -var ErrLicensedFeature = errors.New("not covered by current server license") - -type LicenseChecker interface { - PlaybookAllowed(isPlaybookPublic bool) bool - RetrospectiveAllowed() bool - TimelineAllowed() bool - StatsAllowed() bool - ChecklistItemDueDateAllowed() bool -} - -type PermissionsService struct { - playbookService PlaybookService - runService PlaybookRunService - api playbooks.ServicesAPI - configService config.Service - licenseChecker LicenseChecker -} - -func NewPermissionsService( - playbookService PlaybookService, - runService PlaybookRunService, - api playbooks.ServicesAPI, - configService config.Service, - licenseChecker LicenseChecker, -) *PermissionsService { - return &PermissionsService{ - playbookService, - runService, - api, - configService, - licenseChecker, - } -} - -func (p *PermissionsService) PlaybookIsPublic(playbook Playbook) bool { - return playbook.Public -} - -func (p *PermissionsService) getPlaybookRole(userID string, playbook Playbook) []string { - if !p.canViewTeam(userID, playbook.TeamID) { - return []string{} - } - - for _, member := range playbook.Members { - if member.UserID == userID { - return member.SchemeRoles - } - } - - // Public playbooks - if playbook.Public { - // Public playbooks are public to those who can list channels on a team. (Not guests) - if p.api.HasPermissionToTeam(userID, playbook.TeamID, model.PermissionListTeamChannels) { - if playbook.DefaultPlaybookMemberRole == "" { - return []string{playbook.DefaultPlaybookMemberRole} - } - return []string{PlaybookRoleMember} - } - } - - return []string{} -} - -func (p *PermissionsService) hasPermissionsToPlaybook(userID string, playbook Playbook, permission *model.Permission) bool { - // Check at playbook level - if p.api.RolesGrantPermission(p.getPlaybookRole(userID, playbook), permission.Id) { - return true - } - - // Cascade normally to higher level permissions - return p.api.HasPermissionToTeam(userID, playbook.TeamID, permission) -} - -func (p *PermissionsService) HasPermissionsToRun(userID string, run *PlaybookRun, permission *model.Permission) bool { - // Check at run level - if err := p.runManagePropertiesWithPlaybookRun(userID, run); err != nil { - return false - } - - // Cascade normally to higher level permissions - return p.api.HasPermissionToTeam(userID, run.TeamID, permission) -} - -func (p *PermissionsService) canViewTeam(userID string, teamID string) bool { - if teamID == "" || userID == "" { - return false - } - - // This is list team channels so that Guests are excluded. - return p.api.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) -} - -func (p *PermissionsService) PlaybookCreate(userID string, playbook Playbook) error { - if !p.licenseChecker.PlaybookAllowed(p.PlaybookIsPublic(playbook)) { - return errors.Wrapf(ErrLicensedFeature, "the playbook is not valid with the current license") - } - - // Check the user has permissions over all broadcast channels - for _, channelID := range playbook.BroadcastChannelIDs { - if !p.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) { - return errors.Errorf("user `%s` does not have permission to create posts in channel `%s`", userID, channelID) - } - } - - // Check all invited users have permissions to the team. - for _, userID := range playbook.InvitedUserIDs { - if !p.api.HasPermissionToTeam(userID, playbook.TeamID, model.PermissionViewTeam) { - return errors.Errorf( - "invited user `%s` does not have permission to playbook's team `%s`", - userID, - playbook.TeamID, - ) - } - } - - // Respect setting for not allowing mentions of a group. - for _, groupID := range playbook.InvitedGroupIDs { - group, err := p.api.GetGroup(groupID) - if err != nil { - return errors.Wrap(err, "invalid group") - } - - if !group.AllowReference { - return errors.Errorf( - "group `%s` does not allow references", - groupID, - ) - } - } - - // Check general permissions - permission := model.PermissionPrivatePlaybookCreate - if p.PlaybookIsPublic(playbook) { - permission = model.PermissionPublicPlaybookCreate - } - - if p.api.HasPermissionToTeam(userID, playbook.TeamID, permission) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to create playbook", userID) -} - -func (p *PermissionsService) PlaybookManageProperties(userID string, playbook Playbook) error { - permission := model.PermissionPrivatePlaybookManageProperties - if p.PlaybookIsPublic(playbook) { - permission = model.PermissionPublicPlaybookManageProperties - } - - if p.hasPermissionsToPlaybook(userID, playbook, permission) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have access to playbook `%s`", userID, playbook.ID) -} - -// PlaybookodifyWithFixes checks both ManageProperties and ManageMembers permissions -// performs permissions checks that can be resolved though modification of the input. -// This function modifies the playbook argument. -func (p *PermissionsService) PlaybookModifyWithFixes(userID string, playbook *Playbook, oldPlaybook Playbook) error { - // It is assumed that if you are calling this function there are properties changes - // This means that you need the manage properties permission to manage members for now. - if err := p.PlaybookManageProperties(userID, oldPlaybook); err != nil { - return err - } - - if err := p.NoAddedBroadcastChannelsWithoutPermission(userID, playbook.BroadcastChannelIDs, oldPlaybook.BroadcastChannelIDs); err != nil { - return err - } - - filteredUsers := p.FilterInvitedUserIDs(playbook.InvitedUserIDs, playbook.TeamID) - playbook.InvitedUserIDs = filteredUsers - - filteredGroups := p.FilterInvitedGroupIDs(playbook.InvitedGroupIDs) - playbook.InvitedGroupIDs = filteredGroups - - if playbook.DefaultOwnerID != "" { - if !p.api.HasPermissionToTeam(playbook.DefaultOwnerID, playbook.TeamID, model.PermissionViewTeam) { - logrus.WithFields(logrus.Fields{ - "team_id": playbook.TeamID, - "user_id": playbook.DefaultOwnerID, - }).Warn("owner is not a member of the playbook's team, disabling default owner") - playbook.DefaultOwnerID = "" - playbook.DefaultOwnerEnabled = false - } - } - - // Check if we have changed members, if so check that permission. - if !reflect.DeepEqual(oldPlaybook.Members, playbook.Members) { - if err := p.PlaybookManageMembers(userID, oldPlaybook); err != nil { - return errors.Wrap(err, "attempted to modify members without permissions") - } - - oldMemberRoles := map[string]string{} - for _, member := range oldPlaybook.Members { - oldMemberRoles[member.UserID] = strings.Join(member.Roles, ",") - } - - // Also need to check if roles changed. If so we need to check manage roles permission. - for _, member := range playbook.Members { - oldRoles, memberExisted := oldMemberRoles[member.UserID] - userAddedAsMember := !memberExisted && len(member.Roles) == 1 && member.Roles[0] == PlaybookRoleMember - rolesHaveNotChanged := memberExisted && strings.Join(member.Roles, ",") == oldRoles - if !(userAddedAsMember || rolesHaveNotChanged) { - if err := p.PlaybookManageRoles(userID, oldPlaybook); err != nil { - return errors.Wrap(err, "attempted to modify members without permissions") - } - break - } - } - } - - // Check if we have done a public conversion - if oldPlaybook.Public != playbook.Public { - if oldPlaybook.Public { - if err := p.PlaybookMakePrivate(userID, oldPlaybook); err != nil { - return errors.Wrap(err, "attempted to make playbook private without permissions") - } - } else { - if err := p.PlaybookMakePublic(userID, oldPlaybook); err != nil { - return errors.Wrap(err, "attempted to make playbook public without permissions") - } - } - } - - if !p.licenseChecker.PlaybookAllowed(p.PlaybookIsPublic(*playbook)) { - return errors.Wrapf(ErrLicensedFeature, "the playbook is not valid with the current license") - } - - return nil -} - -func (p *PermissionsService) FilterInvitedUserIDs(invitedUserIDs []string, teamID string) []string { - filteredUsers := []string{} - for _, userID := range invitedUserIDs { - if !p.api.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) { - logrus.WithFields(logrus.Fields{ - "team_id": teamID, - "user_id": userID, - }).Warn("user does not have permissions to playbook's team, removing from automated invite list") - continue - } - filteredUsers = append(filteredUsers, userID) - } - return filteredUsers -} - -func (p *PermissionsService) FilterInvitedGroupIDs(invitedGroupIDs []string) []string { - filteredGroups := []string{} - for _, groupID := range invitedGroupIDs { - var group *model.Group - group, err := p.api.GetGroup(groupID) - if err != nil { - logrus.WithField("group_id", groupID).Error("failed to query group") - continue - } - - if !group.AllowReference { - logrus.WithField("group_id", groupID).Warn("group does not allow references, removing from automated invite list") - continue - } - - filteredGroups = append(filteredGroups, groupID) - } - return filteredGroups -} - -func (p *PermissionsService) DeletePlaybook(userID string, playbook Playbook) error { - return p.PlaybookManageProperties(userID, playbook) -} - -func (p *PermissionsService) NoAddedBroadcastChannelsWithoutPermission(userID string, broadcastChannelIDs, oldBroadcastChannelIDs []string) error { - oldChannelsSet := make(map[string]bool) - for _, channelID := range oldBroadcastChannelIDs { - oldChannelsSet[channelID] = true - } - - for _, channelID := range broadcastChannelIDs { - if !oldChannelsSet[channelID] && - !p.api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) { - return errors.Wrapf( - ErrNoPermissions, - "user `%s` does not have permission to create posts in channel `%s`", - userID, - channelID, - ) - } - } - - return nil -} - -func (p *PermissionsService) PlaybookManageMembers(userID string, playbook Playbook) error { - permission := model.PermissionPrivatePlaybookManageMembers - if p.PlaybookIsPublic(playbook) { - permission = model.PermissionPublicPlaybookManageMembers - } - - if p.hasPermissionsToPlaybook(userID, playbook, permission) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage members for playbook `%s`", userID, playbook.ID) -} - -func (p *PermissionsService) PlaybookManageRoles(userID string, playbook Playbook) error { - permission := model.PermissionPrivatePlaybookManageRoles - if p.PlaybookIsPublic(playbook) { - permission = model.PermissionPublicPlaybookManageRoles - } - - if p.hasPermissionsToPlaybook(userID, playbook, permission) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage roles for playbook `%s`", userID, playbook.ID) -} - -func (p *PermissionsService) PlaybookView(userID string, playbookID string) error { - playbook, err := p.playbookService.Get(playbookID) - if err != nil { - return errors.Wrapf(err, "Unable to get playbook to determine permissions, playbook id `%s`", playbookID) - } - - return p.PlaybookViewWithPlaybook(userID, playbook) -} - -func (p *PermissionsService) PlaybookList(userID, teamID string) error { - // Can list playbooks if you are on the team - if p.canViewTeam(userID, teamID) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to list playbooks for team `%s`", userID, teamID) -} - -func (p *PermissionsService) PlaybookViewWithPlaybook(userID string, playbook Playbook) error { - noAccessErr := errors.Wrapf( - ErrNoPermissions, - "user `%s` to access playbook `%s`", - userID, - playbook.ID, - ) - - // Playbooks are tied to teams. You must have permission to the team to have permission to the playbook. - if !p.canViewTeam(userID, playbook.TeamID) { - return errors.Wrapf(noAccessErr, "no playbook access; no team view permission for team `%s`", playbook.TeamID) - } - - if p.PlaybookIsPublic(playbook) { - if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPublicPlaybookView) { - return nil - } - } - - if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPrivatePlaybookView) { - return nil - } - - return noAccessErr -} - -func (p *PermissionsService) PlaybookMakePrivate(userID string, playbook Playbook) error { - if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPublicPlaybookMakePrivate) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to make playbook `%s` private", userID, playbook.ID) -} - -func (p *PermissionsService) PlaybookMakePublic(userID string, playbook Playbook) error { - if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionPrivatePlaybookMakePublic) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to make playbook `%s` public", userID, playbook.ID) -} - -func (p *PermissionsService) RunCreate(userID string, playbook Playbook) error { - if p.hasPermissionsToPlaybook(userID, playbook, model.PermissionRunCreate) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to run playbook `%s`", userID, playbook.ID) -} - -func (p *PermissionsService) RunManageProperties(userID, runID string) error { - run, err := p.runService.GetPlaybookRun(runID) - if err != nil { - return errors.Wrapf(err, "Unable to get run to determine permissions, run id `%s`", runID) - } - - return p.runManagePropertiesWithPlaybookRun(userID, run) -} - -func (p *PermissionsService) runManagePropertiesWithPlaybookRun(userID string, run *PlaybookRun) error { - if run.OwnerUserID == userID { - return nil - } - - for _, participantID := range run.ParticipantIDs { - if participantID == userID { - return nil - } - } - - if IsSystemAdmin(userID, p.api) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to manage run `%s`", userID, run.ID) -} - -func (p *PermissionsService) RunView(userID, runID string) error { - run, err := p.runService.GetPlaybookRun(runID) - if err != nil { - return errors.Wrapf(err, "Unable to get run to determine permissions, run id `%s`", runID) - } - - // Has permission if is the owner of the run - if run.OwnerUserID == userID { - return nil - } - - // Or if is a participant of the run - for _, participantID := range run.ParticipantIDs { - if participantID == userID { - return nil - } - } - - // Or has view access to the playbook that created it - return p.PlaybookView(userID, run.PlaybookID) -} - -func (p *PermissionsService) ChannelActionCreate(userID, channelID string) error { - if IsSystemAdmin(userID, p.api) || CanManageChannelProperties(userID, channelID, p.api) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to create actions for channel `%s`", userID, channelID) -} - -func (p *PermissionsService) ChannelActionView(userID, channelID string) error { - if p.api.HasPermissionToChannel(userID, channelID, model.PermissionReadChannel) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to view actions for channel `%s`", userID, channelID) -} - -func (p *PermissionsService) ChannelActionUpdate(userID, channelID string) error { - if IsSystemAdmin(userID, p.api) || CanManageChannelProperties(userID, channelID, p.api) { - return nil - } - - return errors.Wrapf(ErrNoPermissions, "user `%s` does not have permission to update actions for channel `%s`", userID, channelID) -} - -// IsSystemAdmin returns true if the userID is a system admin -func IsSystemAdmin(userID string, api playbooks.ServicesAPI) bool { - return api.HasPermissionTo(userID, model.PermissionManageSystem) -} - -// CanManageChannelProperties returns true if the userID is allowed to manage the properties of channelID -func CanManageChannelProperties(userID, channelID string, api playbooks.ServicesAPI) bool { - channel, err := api.GetChannelByID(channelID) - if err != nil { - return false - } - - permission := model.PermissionManagePublicChannelProperties - if channel.Type == model.ChannelTypePrivate { - permission = model.PermissionManagePrivateChannelProperties - } - - return api.HasPermissionToChannel(userID, channelID, permission) -} - -func CanPostToChannel(userID, channelID string, api playbooks.ServicesAPI) bool { - return api.HasPermissionToChannel(userID, channelID, model.PermissionCreatePost) -} - -func IsMemberOfTeam(userID, teamID string, api playbooks.ServicesAPI) bool { - teamMember, err := api.GetTeamMember(teamID, userID) - if err != nil { - return false - } - - return teamMember.DeleteAt == 0 -} - -// RequesterInfo holds the userID and teamID that this request is regarding, and permissions -// for the user making the request -type RequesterInfo struct { - UserID string - TeamID string - IsAdmin bool - IsGuest bool -} - -// IsGuest returns true if the userID is a system guest -func IsGuest(userID string, api playbooks.ServicesAPI) (bool, error) { - user, err := api.GetUserByID(userID) - if err != nil { - return false, errors.Wrapf(err, "Unable to get user to determine permissions, user id `%s`", userID) - } - - return user.IsGuest(), nil -} - -func GetRequesterInfo(userID string, api playbooks.ServicesAPI) (RequesterInfo, error) { - isAdmin := IsSystemAdmin(userID, api) - - isGuest, err := IsGuest(userID, api) - if err != nil { - return RequesterInfo{}, err - } - - return RequesterInfo{ - UserID: userID, - IsAdmin: isAdmin, - IsGuest: isGuest, - }, nil -} diff --git a/server/playbooks/server/app/playbook.go b/server/playbooks/server/app/playbook.go deleted file mode 100644 index c87eb0ca41f..00000000000 --- a/server/playbooks/server/app/playbook.go +++ /dev/null @@ -1,741 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "database/sql/driver" - "encoding/json" - "fmt" - "net/url" - "strings" - - "github.com/mattermost/mattermost/server/public/model" - "gopkg.in/guregu/null.v4" - - "github.com/pkg/errors" -) - -// Playbook represents a desired business outcome, from which playbook runs are started to solve -// a specific instance. -// The tag export supports the export/import feature. If the field makes sense for export, the value should be -// the JSON name of the item in the export format. If the field should not be exported the value should be "-". -// Fields should be exported if they are not server specific like InvitedUserIDs or are tracking metadata like CreateAt. -type Playbook struct { - ID string `json:"id" export:"-"` - Title string `json:"title" export:"title"` - Description string `json:"description" export:"description"` - Public bool `json:"public" export:"-"` - TeamID string `json:"team_id" export:"-"` - CreatePublicPlaybookRun bool `json:"create_public_playbook_run" export:"-"` - CreateAt int64 `json:"create_at" export:"-"` - UpdateAt int64 `json:"update_at" export:"-"` - DeleteAt int64 `json:"delete_at" export:"-"` - NumStages int64 `json:"num_stages" export:"-"` - NumSteps int64 `json:"num_steps" export:"-"` - NumRuns int64 `json:"num_runs" export:"-"` - NumActions int64 `json:"num_actions" export:"-"` - LastRunAt int64 `json:"last_run_at" export:"-"` - Checklists []Checklist `json:"checklists" export:"-"` - Members []PlaybookMember `json:"members" export:"-"` - ReminderMessageTemplate string `json:"reminder_message_template" export:"reminder_message_template"` - ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds" export:"reminder_timer_default_seconds"` - StatusUpdateEnabled bool `json:"status_update_enabled" export:"status_update_enabled"` - InvitedUserIDs []string `json:"invited_user_ids" export:"-"` - InvitedGroupIDs []string `json:"invited_group_ids" export:"-"` - InviteUsersEnabled bool `json:"invite_users_enabled" export:"-"` - DefaultOwnerID string `json:"default_owner_id" export:"-"` - DefaultOwnerEnabled bool `json:"default_owner_enabled" export:"-"` - BroadcastChannelIDs []string `json:"broadcast_channel_ids" export:"-"` - WebhookOnCreationURLs []string `json:"webhook_on_creation_urls" export:"-"` - WebhookOnCreationEnabled bool `json:"webhook_on_creation_enabled" export:"-"` - MessageOnJoin string `json:"message_on_join" export:"message_on_join"` - MessageOnJoinEnabled bool `json:"message_on_join_enabled" export:"message_on_join_enabled"` - RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds" export:"retrospective_reminder_interval_seconds"` - RetrospectiveTemplate string `json:"retrospective_template" export:"retrospective_template"` - RetrospectiveEnabled bool `json:"retrospective_enabled" export:"retrospective_enabled"` - WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls" export:"-"` - SignalAnyKeywords []string `json:"signal_any_keywords" export:"signal_any_keywords"` - SignalAnyKeywordsEnabled bool `json:"signal_any_keywords_enabled" export:"signal_any_keywords_enabled"` - CategorizeChannelEnabled bool `json:"categorize_channel_enabled" export:"categorize_channel_enabled"` - CategoryName string `json:"category_name" export:"category_name"` - RunSummaryTemplateEnabled bool `json:"run_summary_template_enabled" export:"run_summary_template_enabled"` - RunSummaryTemplate string `json:"run_summary_template" export:"run_summary_template"` - ChannelNameTemplate string `json:"channel_name_template" export:"channel_name_template"` - DefaultPlaybookAdminRole string `json:"default_playbook_admin_role" export:"-"` - DefaultPlaybookMemberRole string `json:"default_playbook_member_role" export:"-"` - DefaultRunAdminRole string `json:"default_run_admin_role" export:"-"` - DefaultRunMemberRole string `json:"default_run_member_role" export:"-"` - Metrics []PlaybookMetricConfig `json:"metrics" export:"metrics"` - ActiveRuns int64 `json:"active_runs" export:"-"` - CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant" export:"create_channel_member_on_new_participant"` - RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant" export:"create_channel_member_on_removed_participant"` - - // ChannelID is the identifier of the channel that would be -potentially- linked - // to any new run of this playbook - ChannelID string `json:"channel_id" export:"channel_id"` - - // ChannelMode is the playbook>run>channel flow used - ChannelMode ChannelPlaybookMode `json:"channel_mode" export:"channel_mode"` - - // Deprecated: preserved for backwards compatibility with v1.27 - BroadcastEnabled bool `json:"broadcast_enabled" export:"-"` - WebhookOnStatusUpdateEnabled bool `json:"webhook_on_status_update_enabled" export:"-"` -} - -const ( - PlaybookRoleMember = "playbook_member" - PlaybookRoleAdmin = "playbook_admin" -) - -const ( - MetricTypeDuration = "metric_duration" - MetricTypeCurrency = "metric_currency" - MetricTypeInteger = "metric_integer" -) - -const MaxMetricsPerPlaybook = 4 - -type PlaybookMember struct { - UserID string `json:"user_id"` - Roles []string `json:"roles"` - SchemeRoles []string `json:"scheme_roles"` -} - -type PlaybookMetricConfig struct { - ID string `json:"id" export:"-"` - PlaybookID string `json:"playbook_id" export:"-"` - Title string `json:"title" export:"title"` - Description string `json:"description" export:"description"` - Type string `json:"type" export:"type"` - Target null.Int `json:"target" export:"target"` -} - -func (pm PlaybookMember) Clone() PlaybookMember { - newPlaybookMember := pm - if len(pm.Roles) != 0 { - newPlaybookMember.Roles = append([]string(nil), pm.Roles...) - } - if len(pm.SchemeRoles) != 0 { - newPlaybookMember.SchemeRoles = append([]string(nil), pm.SchemeRoles...) - } - return newPlaybookMember -} - -func (p Playbook) Clone() Playbook { - newPlaybook := p - var newChecklists []Checklist - for _, c := range p.Checklists { - newChecklists = append(newChecklists, c.Clone()) - } - newPlaybook.Checklists = newChecklists - newPlaybook.Metrics = append([]PlaybookMetricConfig(nil), p.Metrics...) - var newMembers []PlaybookMember - for _, m := range p.Members { - newMembers = append(newMembers, m.Clone()) - } - newPlaybook.Members = newMembers - if len(p.InvitedUserIDs) != 0 { - newPlaybook.InvitedUserIDs = append([]string(nil), p.InvitedUserIDs...) - } - if len(p.InvitedGroupIDs) != 0 { - newPlaybook.InvitedGroupIDs = append([]string(nil), p.InvitedGroupIDs...) - } - if len(p.SignalAnyKeywords) != 0 { - newPlaybook.SignalAnyKeywords = append([]string(nil), p.SignalAnyKeywords...) - } - if len(p.BroadcastChannelIDs) != 0 { - newPlaybook.BroadcastChannelIDs = append([]string(nil), p.BroadcastChannelIDs...) - } - if len(p.WebhookOnCreationURLs) != 0 { - newPlaybook.WebhookOnCreationURLs = append([]string(nil), p.WebhookOnCreationURLs...) - } - if len(p.WebhookOnStatusUpdateURLs) != 0 { - newPlaybook.WebhookOnStatusUpdateURLs = append([]string(nil), p.WebhookOnStatusUpdateURLs...) - } - return newPlaybook -} - -func (p Playbook) MarshalJSON() ([]byte, error) { - type Alias Playbook - - old := Alias(p.Clone()) - // replace nils with empty slices for the frontend - if old.Checklists == nil { - old.Checklists = []Checklist{} - } - for j, cl := range old.Checklists { - if cl.Items == nil { - old.Checklists[j].Items = []ChecklistItem{} - } - } - if old.Members == nil { - old.Members = []PlaybookMember{} - } - if old.Metrics == nil { - old.Metrics = []PlaybookMetricConfig{} - } - if old.InvitedUserIDs == nil { - old.InvitedUserIDs = []string{} - } - if old.InvitedGroupIDs == nil { - old.InvitedGroupIDs = []string{} - } - if old.SignalAnyKeywords == nil { - old.SignalAnyKeywords = []string{} - } - if old.BroadcastChannelIDs == nil { - old.BroadcastChannelIDs = []string{} - } - if old.WebhookOnCreationURLs == nil { - old.WebhookOnCreationURLs = []string{} - } - if old.WebhookOnStatusUpdateURLs == nil { - old.WebhookOnStatusUpdateURLs = []string{} - } - - return json.Marshal(old) -} - -func (p Playbook) GetRunChannelID() string { - if p.ChannelMode == PlaybookRunLinkExistingChannel { - return p.ChannelID - } - return "" -} - -// ChecklistCommon allows access on common fields of Checklist and api.UpdateChecklist -type ChecklistCommon interface { - GetItems() []ChecklistItemCommon -} - -// Checklist represents a checklist in a playbook. -type Checklist struct { - // ID is the identifier of the checklist. - ID string `json:"id" export:"-"` - - // Title is the name of the checklist. - Title string `json:"title" export:"title"` - - // Items is an array of all the items in the checklist. - Items []ChecklistItem `json:"items" export:"-"` -} - -func (c Checklist) GetItems() []ChecklistItemCommon { - items := make([]ChecklistItemCommon, len(c.Items)) - for i := range c.Items { - items[i] = &c.Items[i] - } - return items -} - -func (c Checklist) Clone() Checklist { - newChecklist := c - newChecklist.Items = append([]ChecklistItem(nil), c.Items...) - return newChecklist -} - -// ChecklistItemCommon allows access on common fields of ChecklistItem and api.UpdateChecklistItem -type ChecklistItemCommon interface { - GetAssigneeID() string - - SetAssigneeModified(modified int64) - SetState(state string) - SetStateModified(modified int64) - SetCommandLastRun(lastRun int64) -} - -// ChecklistItem represents an item in a checklist. -type ChecklistItem struct { - // ID is the identifier of the checklist item. - ID string `json:"id" export:"-"` - - // Title is the content of the checklist item. - Title string `json:"title" export:"title"` - - // State is the state of the checklist item: "closed" if it's checked, "skipped" if it has - // been skipped, the empty string otherwise. - State string `json:"state" export:"-"` - - // StateModified is the timestamp, in milliseconds since epoch, of the last time the item's - // state was modified. 0 if it was never modified. - StateModified int64 `json:"state_modified" export:"-"` - - // AssigneeID is the identifier of the user to whom this item is assigned. - AssigneeID string `json:"assignee_id" export:"-"` - - // AssigneeModified is the timestamp, in milliseconds since epoch, of the last time the item's - // assignee was modified. 0 if it was never modified. - AssigneeModified int64 `json:"assignee_modified" export:"-"` - - // Command, if not empty, is the slash command that can be run as part of this item. - Command string `json:"command" export:"command"` - - // CommandLastRun is the timestamp, in milliseconds since epoch, of the last time the item's - // slash command was run. 0 if it was never run. - CommandLastRun int64 `json:"command_last_run" export:"-"` - - // Description is a string with the markdown content of the long description of the item. - Description string `json:"description" export:"description"` - - // LastSkipped is the timestamp, in milliseconds since epoch, of the last time the item - // was skipped. 0 if it was never skipped. - LastSkipped int64 `json:"delete_at" export:"-"` - - // DueDate is the timestamp, in milliseconds since epoch. indicates relative or absolute due date - // of the checklist item. 0 if not set. - // Playbook can have only relative timstamp, run can have only absolute timestamp. - DueDate int64 `json:"due_date" export:"due_date"` - - // TaskActions is an array of all the task actions associated with this task. - TaskActions []TaskAction `json:"task_actions" export:"-"` -} - -func (ci *ChecklistItem) GetAssigneeID() string { - return ci.AssigneeID -} - -func (ci *ChecklistItem) SetAssigneeModified(modified int64) { - ci.AssigneeModified = modified -} - -func (ci *ChecklistItem) SetState(state string) { - ci.State = state -} - -func (ci *ChecklistItem) SetStateModified(modified int64) { - ci.StateModified = modified -} - -func (ci *ChecklistItem) SetCommandLastRun(lastRun int64) { - ci.CommandLastRun = lastRun -} - -type GetPlaybooksResults struct { - TotalCount int `json:"total_count"` - PageCount int `json:"page_count"` - HasMore bool `json:"has_more"` - Items []Playbook `json:"items"` -} - -// MarshalJSON customizes the JSON marshalling for GetPlaybooksResults by rendering a nil Items as -// an empty slice instead. -func (r GetPlaybooksResults) MarshalJSON() ([]byte, error) { - type Alias GetPlaybooksResults - - if r.Items == nil { - r.Items = []Playbook{} - } - - aux := &struct { - *Alias - }{ - Alias: (*Alias)(&r), - } - - return json.Marshal(aux) -} - -// PlaybookService is the playbook service for managing playbooks -// userID is the user initiating the event. -type PlaybookService interface { - // Get retrieves a playbook. Returns ErrNotFound if not found. - Get(id string) (Playbook, error) - - // Create creates a new playbook - Create(playbook Playbook, userID string) (string, error) - - // Import imports a new playbook - Import(playbook Playbook, userID string) (string, error) - - // GetPlaybooks retrieves all playbooks - GetPlaybooks() ([]Playbook, error) - - // GetPlaybooksForTeam retrieves all playbooks on the specified team given the provided options - GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error) - - // Update updates a playbook - Update(playbook Playbook, userID string) error - - // Archive archives a playbook - Archive(playbook Playbook, userID string) error - - // Restores an archived playbook - Restore(playbook Playbook, userID string) error - - // AutoFollow method lets user auto-follow all runs of a specific playbook - AutoFollow(playbookID, userID string) error - - // AutoUnfollow method lets user to not auto-follow the newly created playbook runs - AutoUnfollow(playbookID, userID string) error - - // GetAutoFollows returns list of users who auto-follows a playbook - GetAutoFollows(playbookID string) ([]string, error) - - // Duplicate duplicates a playbook - Duplicate(playbook Playbook, userID string) (string, error) - - // Get top playbooks for teams - GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) - - // Get top playbooks for users - GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) -} - -// PlaybookStore is an interface for storing playbooks -type PlaybookStore interface { - // Get retrieves a playbook - Get(id string) (Playbook, error) - - // Create creates a new playbook - Create(playbook Playbook) (string, error) - - // GetPlaybooks retrieves all playbooks - GetPlaybooks() ([]Playbook, error) - - // GetPlaybooksForTeam retrieves all playbooks on the specified team - GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error) - - // GetPlaybooksWithKeywords retrieves all playbooks with keywords enabled - GetPlaybooksWithKeywords(opts PlaybookFilterOptions) ([]Playbook, error) - - // GetTimeLastUpdated retrieves time last playbook was updated at. - // Passed argument determines whether to include playbooks with - // SignalAnyKeywordsEnabled flag or not. - GetTimeLastUpdated(onlyPlaybooksWithKeywordsEnabled bool) (int64, error) - - // GetPlaybookIDsForUser retrieves playbooks user can access - GetPlaybookIDsForUser(userID, teamID string) ([]string, error) - - // Update updates a playbook - Update(playbook Playbook) error - - // GraphqlUpdate taking a setmap for graphql - GraphqlUpdate(id string, setmap map[string]interface{}) error - - // Archive archives a playbook - Archive(id string) error - - // Restore restores a deleted playbook - Restore(id string) error - - // AutoFollow method lets user auto-follow all runs of a specific playbook - AutoFollow(playbookID, userID string) error - - // AutoUnfollow method lets user to not auto-follow the newly created playbook runs - AutoUnfollow(playbookID, userID string) error - - // GetAutoFollows returns list of users who auto-follows a playbook - GetAutoFollows(playbookID string) ([]string, error) - - // GetPlaybooksActiveTotal returns number of active playbooks - GetPlaybooksActiveTotal() (int64, error) - - // GetMetric retrieves a metric by ID - GetMetric(id string) (*PlaybookMetricConfig, error) - - // AddMetric adds a metric - AddMetric(playbookID string, config PlaybookMetricConfig) error - - // UpdateMetric updates a metric - UpdateMetric(id string, setmap map[string]interface{}) error - - // DeleteMetric deletes a metric - DeleteMetric(id string) error - - // Get top playbooks for teams - GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) - - // Get top playbooks for users - GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) - - // AddPlaybookMember adds a user as a member to a playbook - AddPlaybookMember(id string, memberID string) error - - // RemovePlaybookMember removes a user from a playbook - RemovePlaybookMember(id string, memberID string) error -} - -// PlaybookTelemetry defines the methods that the Playbook service needs from the RudderTelemetry. -// userID is the user initiating the event. -type PlaybookTelemetry interface { - // CreatePlaybook tracks the creation of a playbook. - CreatePlaybook(playbook Playbook, userID string) - - // ImportPlaybook tracks the import of a playbook. - ImportPlaybook(playbook Playbook, userID string) - - // UpdatePlaybook tracks the update of a playbook. - UpdatePlaybook(playbook Playbook, userID string) - - // DeletePlaybook tracks the deletion of a playbook. - DeletePlaybook(playbook Playbook, userID string) - - // RestorePlaybook tracks the restoration of a playbook. - RestorePlaybook(playbook Playbook, userID string) - - // FrontendTelemetryForPlaybook tracks an event originating from the frontend - FrontendTelemetryForPlaybook(playbook Playbook, userID, action string) - - // FrontendTelemetryForPlaybookTemplate tracks an event originating from the frontend - FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string) - - // AutoFollowPlaybook tracks the auto-follow of a playbook. - AutoFollowPlaybook(playbook Playbook, userID string) - - // AutoUnfollowPlaybook tracks the auto-unfollow of a playbook. - AutoUnfollowPlaybook(playbook Playbook, userID string) -} - -const ( - ChecklistItemStateOpen = "" - ChecklistItemStateInProgress = "in_progress" - ChecklistItemStateClosed = "closed" - ChecklistItemStateSkipped = "skipped" -) - -func IsValidChecklistItemState(state string) bool { - return state == ChecklistItemStateClosed || - state == ChecklistItemStateInProgress || - state == ChecklistItemStateOpen || - state == ChecklistItemStateSkipped -} - -func IsValidChecklistItemIndex(checklists []Checklist, checklistNum, itemNum int) bool { - return checklists != nil && checklistNum >= 0 && itemNum >= 0 && checklistNum < len(checklists) && itemNum < len(checklists[checklistNum].Items) -} - -// PlaybookFilterOptions specifies the parameters when getting playbooks. -type PlaybookFilterOptions struct { - Sort SortField - Direction SortDirection - SearchTerm string - WithArchived bool - WithMembershipOnly bool //if true will return only playbooks you are a member of - PlaybookIDs []string - - // Pagination options. - Page int - PerPage int -} - -// Clone duplicates the given options. -func (o *PlaybookFilterOptions) Clone() PlaybookFilterOptions { - return *o -} - -// Validate returns a new, validated filter options or returns an error if invalid. -func (o PlaybookFilterOptions) Validate() (PlaybookFilterOptions, error) { - options := o.Clone() - - if options.PerPage <= 0 { - options.PerPage = PerPageDefault - } - - options.Sort = SortField(strings.ToLower(string(options.Sort))) - switch options.Sort { - case SortByID: - case SortByTitle: - case SortByStages: - case SortBySteps: - case "": // default - options.Sort = SortByID - default: - return PlaybookFilterOptions{}, errors.Errorf("unsupported sort '%s'", options.Sort) - } - - options.Direction = SortDirection(strings.ToUpper(string(options.Direction))) - switch options.Direction { - case DirectionAsc: - case DirectionDesc: - case "": //default - options.Direction = DirectionAsc - default: - return PlaybookFilterOptions{}, errors.Errorf("unsupported direction '%s'", options.Direction) - } - - return options, nil -} - -func ValidateWebhookURLs(urls []string) error { - if len(urls) > 64 { - return errors.New("too many registered urls, limit to less than 64") - } - - for _, webhook := range urls { - reqURL, err := url.ParseRequestURI(webhook) - if err != nil { - return errors.Wrapf(err, "unable to parse webhook: %v", webhook) - } - - if reqURL.Scheme != "http" && reqURL.Scheme != "https" { - return fmt.Errorf("protocol in webhook URL is %s; only HTTP and HTTPS are accepted", reqURL.Scheme) - } - } - - return nil -} - -func ValidateCategoryName(categoryName string) error { - categoryNameLength := len(categoryName) - if categoryNameLength > 22 { - msg := fmt.Sprintf("invalid category name: %s (maximum length is 22 characters)", categoryName) - return errors.Errorf(msg) - } - return nil -} - -// CleanUpChecklists sets empty values for checklist fields that are not editable -func CleanUpChecklists[T ChecklistCommon](checklists []T) { - for listIndex := range checklists { - items := checklists[listIndex].GetItems() - for itemIndex := range items { - items[itemIndex].SetAssigneeModified(0) - items[itemIndex].SetState("") - items[itemIndex].SetStateModified(0) - items[itemIndex].SetCommandLastRun(0) - } - } -} - -// ValidatePreAssignment checks if invitations are enabled and if all assignees are also invited -func ValidatePreAssignment(assignees []string, invitedUsers []string, inviteUsersEnabled bool) error { - if len(assignees) > 0 && !inviteUsersEnabled { - return errors.New("invitations are disabled") - } - if !assigneesAreInvited(assignees, invitedUsers) { - return errors.New("users missing in invite user list") - } - return nil -} - -// GetDistinctAssignees returns a list of distinct user ids that are assignees in the given checklists -func GetDistinctAssignees[T ChecklistCommon](checklists []T) []string { - uMap := make(map[string]bool) - for _, cl := range checklists { - for _, ci := range cl.GetItems() { - if id := ci.GetAssigneeID(); id != "" && !uMap[id] { - uMap[id] = true - } - } - } - uIds := make([]string, 0, len(uMap)) - for k := range uMap { - uIds = append(uIds, k) - } - return uIds -} - -func assigneesAreInvited(assignees []string, invited []string) bool { - for _, assignee := range assignees { - found := false - for _, user := range invited { - if user == assignee { - found = true - } - } - if !found { - return false - } - } - return true -} - -func removeDuplicates(a []string) []string { - items := make(map[string]bool) - for _, item := range a { - if item != "" { - items[item] = true - } - } - res := make([]string, 0, len(items)) - for item := range items { - res = append(res, item) - } - return res -} - -func ProcessSignalAnyKeywords(keywords []string) []string { - return removeDuplicates(keywords) -} - -// models for playbooks-insights - -// PlaybooksInsightsList is a response type with pagination support. -type PlaybooksInsightsList struct { - HasNext bool `json:"has_next"` - Items []*PlaybookInsight `json:"items"` -} - -// PlaybookInsight gives insight into activities related to a playbook - -type PlaybookInsight struct { - // ID of the playbook - // required: true - PlaybookID string `json:"playbook_id"` - - // Run count of playbook - // required: true - NumRuns int `json:"num_runs"` - - // Title of playbook - // required: true - Title string `json:"title"` - - // Time the playbook was last run. - // required: false - LastRunAt int64 `json:"last_run_at"` -} - -// ChannelPlaybookMode is a type alias to hold all possible -// modes for playbook > run > channel relation -type ChannelPlaybookMode int - -const ( - PlaybookRunCreateNewChannel ChannelPlaybookMode = iota - PlaybookRunLinkExistingChannel -) - -var channelPlaybookTypes = [...]string{ - PlaybookRunCreateNewChannel: "create_new_channel", - PlaybookRunLinkExistingChannel: "link_existing_channel", -} - -// String creates the string version of the TelemetryTrack -func (cpm ChannelPlaybookMode) String() string { - return channelPlaybookTypes[cpm] -} - -// MarshalText converts a ChannelPlaybookMode to a string for serializers (including JSON) -func (cpm ChannelPlaybookMode) MarshalText() ([]byte, error) { - return []byte(channelPlaybookTypes[cpm]), nil -} - -// UnmarshalText parses a ChannelPlaybookMode from text. For deserializers (including JSON) -func (cpm *ChannelPlaybookMode) UnmarshalText(text []byte) error { - for i, st := range channelPlaybookTypes { - if st == string(text) { - *cpm = ChannelPlaybookMode(i) - return nil - } - } - return fmt.Errorf("unknown ChannelPlaybookMode: %s", string(text)) -} - -// Scan parses a ChannelPlaybookMode back from the DB -func (cpm *ChannelPlaybookMode) Scan(src interface{}) error { - txt, ok := src.([]byte) // mysql - if !ok { - txt, ok := src.(string) //postgres - if !ok { - return fmt.Errorf("could not cast to string: %v", src) - } - return cpm.UnmarshalText([]byte(txt)) - } - return cpm.UnmarshalText(txt) -} - -// Value represents a ChannelPlaybookMode as a type writable into the DB -func (cpm ChannelPlaybookMode) Value() (driver.Value, error) { - return cpm.MarshalText() -} diff --git a/server/playbooks/server/app/playbook_run.go b/server/playbooks/server/app/playbook_run.go deleted file mode 100644 index 057a450c44e..00000000000 --- a/server/playbooks/server/app/playbook_run.go +++ /dev/null @@ -1,1209 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "strings" - "time" - - "gopkg.in/guregu/null.v4" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/v8/playbooks/product/pluginapi/cluster" -) - -const ( - StatusInProgress = "InProgress" - StatusFinished = "Finished" -) - -const ( - RunRoleMember = "run_member" - RunRoleAdmin = "run_admin" -) - -const ( - RunSourcePost = "post" - RunSourceDialog = "dialog" -) - -const ( - RunTypePlaybook = "playbook" - RunTypeChannelChecklist = "channelChecklist" -) - -// PlaybookRun holds the detailed information of a playbook run. -// -// NOTE: When adding a column to the db, search for "When adding a PlaybookRun column" to see where -// that column needs to be added in the sqlstore code. -type PlaybookRun struct { - // ID is the unique identifier of the playbook run. - ID string `json:"id"` - - // Name is the name of the playbook run's channel. - Name string `json:"name"` - - // Summary is a short string, in Markdown, describing what the run is. - Summary string `json:"summary"` - - // SummaryModifiedAt is date when the summary was modified - SummaryModifiedAt int64 `json:"summary_modified_at"` - - // OwnerUserID is the user identifier of the playbook run's owner. - OwnerUserID string `json:"owner_user_id"` - - // ReporterUserID is the user identifier of the playbook run's reporter; i.e., the user that created the run. - ReporterUserID string `json:"reporter_user_id"` - - // TeamID is the identifier of the team the playbook run lives in. - TeamID string `json:"team_id"` - - // ChannelID is the identifier of the playbook run's channel. - ChannelID string `json:"channel_id"` - - // CreateAt is the timestamp, in milliseconds since epoch, of when the playbook run was created. - CreateAt int64 `json:"create_at"` - - // EndAt is the timestamp, in milliseconds since epoch, of when the playbook run was ended. - // If 0, the run is still ongoing. - EndAt int64 `json:"end_at"` - - // Deprecated: preserved for backwards compatibility with v1.2. - DeleteAt int64 `json:"delete_at"` - - // Deprecated: preserved for backwards compatibility with v1.2. - ActiveStage int `json:"active_stage"` - - // Deprecated: preserved for backwards compatibility with v1.2. - ActiveStageTitle string `json:"active_stage_title"` - - // PostID, if not empty, is the identifier of the post from which this playbook run was originally created. - PostID string `json:"post_id"` - - // PlaybookID is the identifier of the playbook from which this run was created. - PlaybookID string `json:"playbook_id"` - - // Checklists is an array of the checklists in the run. - Checklists []Checklist `json:"checklists"` - - // StatusPosts is an array of all the status updates posted in the run. - StatusPosts []StatusPost `json:"status_posts"` - - // CurrentStatus is the current status of the playbook run. - // It can be StatusInProgress ("InProgress") or StatusFinished ("Finished") - CurrentStatus string `json:"current_status"` - - // LastStatusUpdateAt is the timestamp, in milliseconds since epoch, of the time the last - // status update was posted. - LastStatusUpdateAt int64 `json:"last_status_update_at"` - - // ReminderPostID, if not empty, is the identifier of the reminder posted to the channel to - // update the status. - ReminderPostID string `json:"reminder_post_id"` - - // PreviousReminder, if not empty, is the time.Duration (nanoseconds) at which the next - // scheduled status update will be posted. - PreviousReminder time.Duration `json:"previous_reminder"` - - // ReminderMessageTemplate, if not empty, is the template shown when updating the status of the - // playbook run for the first time. - ReminderMessageTemplate string `json:"reminder_message_template"` - - // ReminderTimerDefaultSeconds is the expected default interval, in seconds, - // between every status update - ReminderTimerDefaultSeconds int64 `json:"reminder_timer_default_seconds"` - - //Defines if status update functionality is enabled - StatusUpdateEnabled bool `json:"status_update_enabled"` - - // InvitedUserIDs, if not empty, is an array containing the identifiers of the users that were - // automatically invited to the playbook run when it was created. - InvitedUserIDs []string `json:"invited_user_ids"` - - // InvitedGroupIDs, if not empty, is an array containing the identifiers of the user groups that - // were automatically invited to the playbook run when it was created. - InvitedGroupIDs []string `json:"invited_group_ids"` - - // TimelineEvents is an array of the events saved to the timeline of the playbook run. - TimelineEvents []TimelineEvent `json:"timeline_events"` - - // DefaultOwnerID, if not empty, is the identifier of the user that was automatically assigned - // as owner of the playbook run when it was created. - DefaultOwnerID string `json:"default_owner_id"` - - // BroadcastChannelIDs is an array of the identifiers of the channels where the playbook run - // creation and status updates are announced. - BroadcastChannelIDs []string `json:"broadcast_channel_ids"` - - // WebhookOnCreationURLs, if not empty, is the URL to which a POST request is made with the whole - // playbook run as payload when the run is created. - WebhookOnCreationURLs []string `json:"webhook_on_creation_urls"` - - // WebhookOnStatusUpdateURLs, if not empty, is the URL to which a POST request is made with the - // whole playbook run as payload every time the status of the playbook run is updated. - WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"` - - // StatusUpdateBroadcastChannelsEnabled is true if the channels broadcast action is enabled for - // the run status update event, false otherwise. - StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"` - - // StatusUpdateBroadcastWebhooksEnabled is true if the webhooks broadcast action is enabled for - // the run status update event, false otherwise. - StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"` - - // Retrospective is a string containing the currently saved retrospective. - // If RetrospectivePublishedAt is different than 0, this is the final published retrospective. - Retrospective string `json:"retrospective"` - - // RetrospectivePublishedAt is the timestamp, in milliseconds since epoch, of the last time a - // retrospective was published. If 0, the retrospective has not been published yet. - RetrospectivePublishedAt int64 `json:"retrospective_published_at"` - - // RetrospectiveWasCanceled is true if the retrospective was cancelled, false otherwise. - RetrospectiveWasCanceled bool `json:"retrospective_was_canceled"` - - // RetrospectiveReminderIntervalSeconds is the interval, in seconds, between subsequent reminders - // to fill the retrospective. - RetrospectiveReminderIntervalSeconds int64 `json:"retrospective_reminder_interval_seconds"` - - // Defines if retrospective functionality is enabled - RetrospectiveEnabled bool `json:"retrospective_enabled"` - - // MessageOnJoin, if not empty, is the message shown to every user that joins the channel of - // the playbook run. - MessageOnJoin string `json:"message_on_join"` - - // ParticipantIDs is an array of the identifiers of all the participants in the playbook run. - // A participant is any member of the playbook run channel that isn't a bot. - ParticipantIDs []string `json:"participant_ids"` - - // CategoryName, if not empty, is the name of the category where the run channel will live. - CategoryName string `json:"category_name"` - - // Playbook run metric values - MetricsData []RunMetricData `json:"metrics_data"` - - // CreateChannelMemberOnNewParticipant is the Run action flag that defines if a new channel member will be added - // to the run's channel when a new participant is added to the run (by themselve or by other members). - CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant" export:"create_channel_member_on_new_participant"` - - // RemoveChannelMemberOnRemovedParticipant is the Run action flag that defines if an existent channel member will be removed - // from the run's channel when a new participant is added to the run (by themselve or by other members). - RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant" export:"create_channel_member_on_removed_participant"` - - // Type determines a type of a run. - // It can be RunTypePlaybook ("playbook") or RunTypeChannelChecklist ("channel") - Type string `json:"type"` -} - -func (r *PlaybookRun) Clone() *PlaybookRun { - newPlaybookRun := *r - var newChecklists []Checklist - for _, c := range r.Checklists { - newChecklists = append(newChecklists, c.Clone()) - } - newPlaybookRun.Checklists = newChecklists - - newPlaybookRun.StatusPosts = append([]StatusPost(nil), r.StatusPosts...) - newPlaybookRun.TimelineEvents = append([]TimelineEvent(nil), r.TimelineEvents...) - newPlaybookRun.InvitedUserIDs = append([]string(nil), r.InvitedUserIDs...) - newPlaybookRun.InvitedGroupIDs = append([]string(nil), r.InvitedGroupIDs...) - newPlaybookRun.ParticipantIDs = append([]string(nil), r.ParticipantIDs...) - newPlaybookRun.WebhookOnCreationURLs = append([]string(nil), r.WebhookOnCreationURLs...) - newPlaybookRun.WebhookOnStatusUpdateURLs = append([]string(nil), r.WebhookOnStatusUpdateURLs...) - newPlaybookRun.MetricsData = append([]RunMetricData(nil), r.MetricsData...) - - return &newPlaybookRun -} - -func (r PlaybookRun) MarshalJSON() ([]byte, error) { - type Alias PlaybookRun - - old := (*Alias)(r.Clone()) - // replace nils with empty slices for the frontend - if old.Checklists == nil { - old.Checklists = []Checklist{} - } - for j, cl := range old.Checklists { - if cl.Items == nil { - old.Checklists[j].Items = []ChecklistItem{} - } - } - if old.StatusPosts == nil { - old.StatusPosts = []StatusPost{} - } - if old.InvitedUserIDs == nil { - old.InvitedUserIDs = []string{} - } - if old.InvitedGroupIDs == nil { - old.InvitedGroupIDs = []string{} - } - if old.TimelineEvents == nil { - old.TimelineEvents = []TimelineEvent{} - } - if old.ParticipantIDs == nil { - old.ParticipantIDs = []string{} - } - if old.BroadcastChannelIDs == nil { - old.BroadcastChannelIDs = []string{} - } - if old.WebhookOnCreationURLs == nil { - old.WebhookOnCreationURLs = []string{} - } - if old.WebhookOnStatusUpdateURLs == nil { - old.WebhookOnStatusUpdateURLs = []string{} - } - if old.MetricsData == nil { - old.MetricsData = []RunMetricData{} - } - - return json.Marshal(old) -} - -// SetChecklistFromPlaybook overwrites this run's checklists with the ones in the provided playbook. -func (r *PlaybookRun) SetChecklistFromPlaybook(playbook Playbook) { - r.Checklists = playbook.Checklists - - // Playbooks can only have due dates relative to when a run starts, - // so we should convert them to absolute timestamp. - now := model.GetMillis() - for i := range r.Checklists { - for j := range r.Checklists[i].Items { - if r.Checklists[i].Items[j].DueDate > 0 { - r.Checklists[i].Items[j].DueDate += now - } - } - } -} - -// SetConfigurationFromPlaybook overwrites this run's configuration with the data from the provided playbook, -// effectively snapshoting the playbook's configuration in this moment of time. -func (r *PlaybookRun) SetConfigurationFromPlaybook(playbook Playbook, source string) { - // Runs created through managed dialog lack summary, and we should use the template (if enabled) - // Runs created though new modal would have filled the summary in the webapp - if playbook.RunSummaryTemplateEnabled && source == RunSourceDialog { - r.Summary = playbook.RunSummaryTemplate - } - r.ReminderMessageTemplate = playbook.ReminderMessageTemplate - r.StatusUpdateEnabled = playbook.StatusUpdateEnabled - r.PreviousReminder = time.Duration(playbook.ReminderTimerDefaultSeconds) * time.Second - r.ReminderTimerDefaultSeconds = playbook.ReminderTimerDefaultSeconds - - r.InvitedUserIDs = []string{} - r.InvitedGroupIDs = []string{} - if playbook.InviteUsersEnabled { - r.InvitedUserIDs = playbook.InvitedUserIDs - r.InvitedGroupIDs = playbook.InvitedGroupIDs - } - - if playbook.DefaultOwnerEnabled { - r.DefaultOwnerID = playbook.DefaultOwnerID - } - - // Do not propagate StatusUpdateBroadcastChannelsEnabled as true if there are no channels in BroadcastChannelIDs - r.StatusUpdateBroadcastChannelsEnabled = playbook.BroadcastEnabled && len(playbook.BroadcastChannelIDs) > 0 - r.BroadcastChannelIDs = playbook.BroadcastChannelIDs - - r.WebhookOnCreationURLs = []string{} - if playbook.WebhookOnCreationEnabled { - r.WebhookOnCreationURLs = playbook.WebhookOnCreationURLs - } - - // Do not propagate StatusUpdateBroadcastWebhooksEnabled as true if there are no URLs - r.StatusUpdateBroadcastWebhooksEnabled = playbook.WebhookOnStatusUpdateEnabled && len(playbook.WebhookOnStatusUpdateURLs) > 0 - r.WebhookOnStatusUpdateURLs = playbook.WebhookOnStatusUpdateURLs - - r.RetrospectiveEnabled = playbook.RetrospectiveEnabled - if playbook.RetrospectiveEnabled { - r.RetrospectiveReminderIntervalSeconds = playbook.RetrospectiveReminderIntervalSeconds - r.Retrospective = playbook.RetrospectiveTemplate - } - - r.CreateChannelMemberOnNewParticipant = playbook.CreateChannelMemberOnNewParticipant - r.RemoveChannelMemberOnRemovedParticipant = playbook.RemoveChannelMemberOnRemovedParticipant - - r.Type = RunTypePlaybook -} - -type StatusPost struct { - // ID is the identifier of the post containing the status update. - ID string `json:"id"` - - // CreateAt is the timestamp, in milliseconds since epoch, of the time this status update was - // posted. - CreateAt int64 `json:"create_at"` - - // DeleteAt is the timestamp, in milliseconds since epoch, of the time the post containing this - // status update was deleted. 0 if it was never deleted. - DeleteAt int64 `json:"delete_at"` -} - -// StatusPostComplete is the "complete" representation of a status update -// -// This type is part of an effort to decopuple channels and playbooks, where -// status updates will stop being -only- Posts in a channel. -type StatusPostComplete struct { - // ID is the identifier of the post containing the status update. - ID string `json:"id"` - - // CreateAt is the timestamp, in milliseconds since epoch, of the time this status update was - // posted. - CreateAt int64 `json:"create_at"` - - // DeleteAt is the timestamp, in milliseconds since epoch, of the time the post containing this - // status update was deleted. 0 if it was never deleted. - DeleteAt int64 `json:"delete_at"` - - // Message is the content of the status update. It supports markdown. - Message string `json:"message"` - - // AuthorUserName is the username of the user who sent the status update. - AuthorUserName string `json:"author_user_name"` -} - -// NewStatusPostComplete creates a StatusUpdate from a channel Post -func NewStatusPostComplete(post *model.Post) *StatusPostComplete { - author, _ := post.GetProp("authorUsername").(string) - return &StatusPostComplete{ - ID: post.Id, - CreateAt: post.CreateAt, - DeleteAt: post.DeleteAt, - Message: post.Message, - AuthorUserName: author, - } -} - -type UpdateOptions struct { -} - -// StatusUpdateOptions encapsulates the fields that can be set when updating a playbook run's status -// NOTE: changes made to this should be reflected in the client package. -type StatusUpdateOptions struct { - Message string `json:"message"` - Reminder time.Duration `json:"reminder"` - FinishRun bool `json:"finish_run"` -} - -// Metadata tracks ancillary metadata about a playbook run. -type Metadata struct { - ChannelName string `json:"channel_name"` - ChannelDisplayName string `json:"channel_display_name"` - TeamName string `json:"team_name"` - NumParticipants int64 `json:"num_participants"` - TotalPosts int64 `json:"total_posts"` - Followers []string `json:"followers"` -} - -type timelineEventType string - -const ( - PlaybookRunCreated timelineEventType = "incident_created" - TaskStateModified timelineEventType = "task_state_modified" - StatusUpdated timelineEventType = "status_updated" - StatusUpdateRequested timelineEventType = "status_update_requested" - OwnerChanged timelineEventType = "owner_changed" - AssigneeChanged timelineEventType = "assignee_changed" - RanSlashCommand timelineEventType = "ran_slash_command" - EventFromPost timelineEventType = "event_from_post" - UserJoinedLeft timelineEventType = "user_joined_left" - ParticipantsChanged timelineEventType = "participants_changed" - PublishedRetrospective timelineEventType = "published_retrospective" - CanceledRetrospective timelineEventType = "canceled_retrospective" - RunFinished timelineEventType = "run_finished" - RunRestored timelineEventType = "run_restored" - StatusUpdateSnoozed timelineEventType = "status_update_snoozed" - StatusUpdatesEnabled timelineEventType = "status_updates_enabled" - StatusUpdatesDisabled timelineEventType = "status_updates_disabled" -) - -type TimelineEvent struct { - // ID is the identifier of this event. - ID string `json:"id"` - - // PlaybookRunID is the identifier of the playbook run this event lives in. - PlaybookRunID string `json:"playbook_run_id"` - - // CreateAt is the timestamp, in milliseconds since epoch, of the time this event was created. - CreateAt int64 `json:"create_at"` - - // DeleteAt is the timestamp, in milliseconds since epoch, of the time this event was deleted. - // 0 if it was never deleted. - DeleteAt int64 `json:"delete_at"` - - // EventAt is the timestamp, in milliseconds since epoch, of the actual situation this event is - // describing. - EventAt int64 `json:"event_at"` - - // EventType is the type of this event. It can be "incident_created", "task_state_modified", - // "status_updated", "owner_changed", "assignee_changed", "ran_slash_command", - // "event_from_post", "user_joined_left", "published_retrospective", "canceled_retrospective" or "status_update_snoozed". - EventType timelineEventType `json:"event_type"` - - // Summary is a short description of the event. - Summary string `json:"summary"` - - // Details is the longer description of the event. - Details string `json:"details"` - - // PostID, if not empty, is the identifier of the post announcing in the channel this event - // happened. If the event is of type "event_from_post", this is the identifier of that post. - PostID string `json:"post_id"` - - // SubjectUserID is the identifier of the user involved in the event. For example, if the event - // is of type "owner_changed", this is the identifier of the new owner. - SubjectUserID string `json:"subject_user_id"` - - // CreatorUserID is the identifier of the user that created the event. - CreatorUserID string `json:"creator_user_id"` -} - -// GetPlaybookRunsResults collects the results of the GetPlaybookRuns call: the list of PlaybookRuns matching -// the HeaderFilterOptions, and the TotalCount of the matching playbook runs before paging was applied. -type GetPlaybookRunsResults struct { - TotalCount int `json:"total_count"` - PageCount int `json:"page_count"` - PerPage int `json:"per_page"` - HasMore bool `json:"has_more"` - Items []PlaybookRun `json:"items"` -} - -type SQLStatusPost struct { - PlaybookRunID string - PostID string - EndAt int64 -} - -type RunMetricData struct { - MetricConfigID string `json:"metric_config_id"` - Value null.Int `json:"value"` -} - -type RetrospectiveUpdate struct { - Text string `json:"retrospective"` - Metrics []RunMetricData `json:"metrics"` -} - -func (r GetPlaybookRunsResults) Clone() GetPlaybookRunsResults { - newGetPlaybookRunsResults := r - - newGetPlaybookRunsResults.Items = make([]PlaybookRun, 0, len(r.Items)) - for _, i := range r.Items { - newGetPlaybookRunsResults.Items = append(newGetPlaybookRunsResults.Items, *i.Clone()) - } - - return newGetPlaybookRunsResults -} - -func (r GetPlaybookRunsResults) MarshalJSON() ([]byte, error) { - type Alias GetPlaybookRunsResults - - old := Alias(r.Clone()) - - // replace nils with empty slices for the frontend - if old.Items == nil { - old.Items = []PlaybookRun{} - } - - return json.Marshal(old) -} - -// OwnerInfo holds the summary information of a owner. -type OwnerInfo struct { - UserID string `json:"user_id"` - Username string `json:"username"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Nickname string `json:"nickname"` -} - -// DialogState holds the start playbook run interactive dialog's state as it appears in the client -// and is submitted back to the server. -type DialogState struct { - PostID string `json:"post_id"` - ClientID string `json:"client_id"` - PromptPostID string `json:"prompt_post_id"` -} - -type DialogStateAddToTimeline struct { - PostID string `json:"post_id"` -} - -// RunLink represents the info needed to display and link to a run -type RunLink struct { - PlaybookRunID string - Name string -} - -// AssignedRun represents all the info needed to display a Run & ChecklistItem to a user -type AssignedRun struct { - RunLink - Tasks []AssignedTask -} - -// AssignedTask represents a ChecklistItem + extra info needed to display to a user -type AssignedTask struct { - // ID is the identifier of the containing checklist. - ChecklistID string - - // Title is the name of the containing checklist. - ChecklistTitle string - - ChecklistItem -} - -// RunAction represents the run action settings. Frontend passes this struct to update settings. -type RunAction struct { - BroadcastChannelIDs []string `json:"broadcast_channel_ids"` - WebhookOnStatusUpdateURLs []string `json:"webhook_on_status_update_urls"` - - StatusUpdateBroadcastChannelsEnabled bool `json:"status_update_broadcast_channels_enabled"` - StatusUpdateBroadcastWebhooksEnabled bool `json:"status_update_broadcast_webhooks_enabled"` - - CreateChannelMemberOnNewParticipant bool `json:"create_channel_member_on_new_participant"` - RemoveChannelMemberOnRemovedParticipant bool `json:"remove_channel_member_on_removed_participant"` -} - -type RunMetadata struct { - ID string - Name string - TeamID string -} - -type TopicMetadata struct { - ID string - RunID string - TeamID string -} - -const ( - ActionTypeBroadcastChannels = "broadcast_to_channels" - ActionTypeBroadcastWebhooks = "broadcast_to_webhooks" - - TriggerTypeStatusUpdatePosted = "status_update_posted" -) - -// PlaybookRunService is the playbook run service interface. -type PlaybookRunService interface { - // GetPlaybookRuns returns filtered playbook runs and the total count before paging. - GetPlaybookRuns(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) (*GetPlaybookRunsResults, error) - - // CreatePlaybookRun creates a new playbook run. userID is the user who initiated the CreatePlaybookRun. - CreatePlaybookRun(playbookRun *PlaybookRun, playbook *Playbook, userID string, public bool) (*PlaybookRun, error) - - // OpenCreatePlaybookRunDialog opens an interactive dialog to start a new playbook run. - OpenCreatePlaybookRunDialog(teamID, ownerID, triggerID, postID, clientID string, playbooks []Playbook) error - - // OpenUpdateStatusDialog opens an interactive dialog so the user can update the playbook run's status. - OpenUpdateStatusDialog(playbookRunID, userID, triggerID string) error - - // OpenAddToTimelineDialog opens an interactive dialog so the user can add a post to the playbook run timeline. - OpenAddToTimelineDialog(requesterInfo RequesterInfo, postID, teamID, triggerID string) error - - // OpenAddChecklistItemDialog opens an interactive dialog so the user can add a post to the playbook run timeline. - OpenAddChecklistItemDialog(triggerID, userID, playbookRunID string, checklist int) error - - // AddPostToTimeline adds an event based on a post to a playbook run's timeline. - AddPostToTimeline(playbookRunID, userID, postID, summary string) error - - // RemoveTimelineEvent removes the timeline event (sets the DeleteAt to the current time). - RemoveTimelineEvent(playbookRunID, userID, eventID string) error - - // UpdateStatus updates a playbook run's status. - UpdateStatus(playbookRunID, userID string, options StatusUpdateOptions) error - - // OpenFinishPlaybookRunDialog opens the dialog to confirm the run should be finished. - OpenFinishPlaybookRunDialog(playbookRunID, userID, triggerID string) error - - // FinishPlaybookRun changes a run's state to Finished. If run is already in Finished state, the call is a noop. - FinishPlaybookRun(playbookRunID, userID string) error - - // ToggleStatusUpdates enables or disables status update for the run - ToggleStatusUpdates(playbookRunID, userID string, enable bool) error - - // GetPlaybookRun gets a playbook run by ID. Returns error if it could not be found. - GetPlaybookRun(playbookRunID string) (*PlaybookRun, error) - - // GetPlaybookRunMetadata gets ancillary metadata about a playbook run. - GetPlaybookRunMetadata(playbookRunID string) (*Metadata, error) - - // GetPlaybookRunsForChannelByUser get the playbookRuns associated with this channel and user. - GetPlaybookRunsForChannelByUser(channelID string, userID string) ([]PlaybookRun, error) - - // GetOwners returns all the owners of playbook runs selected - GetOwners(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) ([]OwnerInfo, error) - - // IsOwner returns true if the userID is the owner for playbookRunID. - IsOwner(playbookRunID string, userID string) bool - - // ChangeOwner processes a request from userID to change the owner for playbookRunID - // to ownerID. Changing to the same ownerID is a no-op. - ChangeOwner(playbookRunID string, userID string, ownerID string) error - - // ModifyCheckedState modifies the state of the specified checklist item - // Idempotent, will not perform any actions if the checklist item is already in the specified state - ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber int, itemNumber int) error - - // ToggleCheckedState checks or unchecks the specified checklist item - ToggleCheckedState(playbookRunID, userID string, checklistNumber, itemNumber int) error - - // SetAssignee sets the assignee for the specified checklist item - // Idempotent, will not perform any actions if the checklist item is already assigned to assigneeID - SetAssignee(playbookRunID, userID, assigneeID string, checklistNumber, itemNumber int) error - - // SetCommandToChecklistItem sets command to checklist item - SetCommandToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, newCommand string) error - - // SetDueDate sets absolute due date timestamp for the specified checklist item - SetDueDate(playbookRunID, userID string, duedate int64, checklistNumber, itemNumber int) error - - // SetTaskActionsToChecklistItem sets Task Actions to checklist item - SetTaskActionsToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, taskActions []TaskAction) error - - // RunChecklistItemSlashCommand executes the slash command associated with the specified checklist item. - RunChecklistItemSlashCommand(playbookRunID, userID string, checklistNumber, itemNumber int) (string, error) - - // DuplicateChecklistItem duplicates the checklist item. - DuplicateChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error - - // AddChecklistItem adds an item to the specified checklist - AddChecklistItem(playbookRunID, userID string, checklistNumber int, checklistItem ChecklistItem) error - - // RemoveChecklistItem removes an item from the specified checklist - RemoveChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int) error - - // DuplicateChecklist duplicates a checklist - DuplicateChecklist(playbookRunID, userID string, checklistNumber int) error - - // SkipChecklist skips a checklist - SkipChecklist(playbookRunID, userID string, checklistNumber int) error - - // RestoreChecklist restores a skipped checklist - RestoreChecklist(playbookRunID, userID string, checklistNumber int) error - - // SkipChecklistItem removes an item from the specified checklist - SkipChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int) error - - // RestoreChecklistItem restores a skipped item from the specified checklist - RestoreChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int) error - - // EditChecklistItem changes the title, command and description of a specified checklist item. - EditChecklistItem(playbookRunID, userID string, checklistNumber int, itemNumber int, newTitle, newCommand, newDescription string) error - - // MoveChecklistItem moves a checklist item from one position to another. - MoveChecklist(playbookRunID, userID string, sourceChecklistIdx, destChecklistIdx int) error - - // MoveChecklistItem moves a checklist item from one position to another. - MoveChecklistItem(playbookRunID, userID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error - - // GetChecklistItemAutocomplete returns the list of checklist items for playbookRuns to be used in autocomplete - GetChecklistItemAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) - - // GetChecklistAutocomplete returns the list of checklists for playbookRuns to be used in autocomplete - GetChecklistAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) - - // GetRunsAutocomplete returns the list of runs to be used in autocomplete - GetRunsAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) - - // AddChecklist prepends a new checklist to the specified run - AddChecklist(playbookRunID, userID string, checklist Checklist) error - - // RemoveChecklist removes the specified checklist. - RemoveChecklist(playbookRunID, userID string, checklistNumber int) error - - // RenameChecklist renames the specified checklist - RenameChecklist(playbookRunID, userID string, checklistNumber int, newTitle string) error - - // NukeDB removes all playbook run related data. - NukeDB() error - - // SetReminder sets a reminder. After time.Now().Add(fromNow) in the future, - // the owner will be reminded to update the playbook run's status. - SetReminder(playbookRunID string, fromNow time.Duration) error - - // RemoveReminder removes the pending reminder for playbookRunID (if any). - RemoveReminder(playbookRunID string) - - // HandleReminder is the handler for all reminder events. - HandleReminder(key string) - - // SetNewReminder sets a new reminder for playbookRunID, removes any pending reminder, removes the - // reminder post in the playbookRun's channel, and resets the PreviousReminder and - // LastStatusUpdateAt (so the countdown timer to "update due" shows the correct time) - SetNewReminder(playbookRunID string, newReminder time.Duration) error - - // ResetReminder records an event for snoozing a reminder, then calls SetNewReminder to create - // the next reminder - ResetReminder(playbookRunID string, newReminder time.Duration) error - - // ChangeCreationDate changes the creation date of the specified playbook run. - ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error - - // UpdateRetrospective updates the retrospective for the given playbook run. - UpdateRetrospective(playbookRunID, userID string, retrospective RetrospectiveUpdate) error - - // PublishRetrospective publishes the retrospective. - PublishRetrospective(playbookRunID, userID string, retrospective RetrospectiveUpdate) error - - // CancelRetrospective cancels the retrospective. - CancelRetrospective(playbookRunID, userID string) error - - // EphemeralPostTodoDigestToUser gathers the list of assigned tasks, participating runs, and overdue updates, - // and sends an ephemeral post to userID on channelID. Use force = true to post even if there are no items. - EphemeralPostTodoDigestToUser(userID string, channelID string, force bool, includeRunsInProgress bool) error - - // DMTodoDigestToUser gathers the list of assigned tasks, participating runs, and overdue updates, - // and DMs the message to userID. Use force = true to DM even if there are no items. - DMTodoDigestToUser(userID string, force bool, includeRunsInProgress bool) error - - // GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID - GetRunsWithAssignedTasks(userID string) ([]AssignedRun, error) - - // GetParticipatingRuns returns the list of active runs with userID as participant - GetParticipatingRuns(userID string) ([]RunLink, error) - - // GetOverdueUpdateRuns returns the list of userID's runs that have overdue updates - GetOverdueUpdateRuns(userID string) ([]RunLink, error) - - // Follow method lets user follow a specific playbook run - Follow(playbookRunID, userID string) error - - // UnFollow method lets user unfollow a specific playbook run - Unfollow(playbookRunID, userID string) error - - // GetFollowers returns list of followers for a specific playbook run - GetFollowers(playbookRunID string) ([]string, error) - - // RestorePlaybookRun reverts a run from the Finished state. If run was not in Finished state, the call is a noop. - RestorePlaybookRun(playbookRunID, userID string) error - - // RequestUpdate posts a status update request message in the run's channel - RequestUpdate(playbookRunID, requesterID string) error - - // RequestJoinChannel posts a channel-join request message in the run's channel - RequestJoinChannel(playbookRunID, requesterID string) error - - // RemoveParticipants removes users from the run's participants - RemoveParticipants(playbookRunID string, userIDs []string, requesterUserID string) error - - // AddParticipants adds users to the participants list - AddParticipants(playbookRunID string, userIDs []string, requesterUserID string, forceAddToChannel bool) error - - // GetPlaybookRunIDsForUser returns run ids where user is a participant or is following - GetPlaybookRunIDsForUser(userID string) ([]string, error) - - // GetRunMetadataByIDs returns playbook runs metadata by passed run IDs. - // Notice that order of passed ids and returned runs might not coincide - GetRunMetadataByIDs(runIDs []string) ([]RunMetadata, error) - - // GetTaskMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs - GetTaskMetadataByIDs(taskIDs []string) ([]TopicMetadata, error) - - // GetStatusMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs - GetStatusMetadataByIDs(statusIDs []string) ([]TopicMetadata, error) - - // GraphqlUpdate taking a setmap for graphql - GraphqlUpdate(id string, setmap map[string]interface{}) error - - // MessageHasBeenPosted checks posted messages for triggers that may trigger task actions - MessageHasBeenPosted(post *model.Post) -} - -// PlaybookRunStore defines the methods the PlaybookRunServiceImpl needs from the interfaceStore. -type PlaybookRunStore interface { - // GetPlaybookRuns returns filtered playbook runs and the total count before paging. - GetPlaybookRuns(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) (*GetPlaybookRunsResults, error) - - // CreatePlaybookRun creates a new playbook run. If playbook run has an ID, that ID will be used. - CreatePlaybookRun(playbookRun *PlaybookRun) (*PlaybookRun, error) - - // UpdatePlaybookRun updates a playbook run. - UpdatePlaybookRun(playbookRun *PlaybookRun) (*PlaybookRun, error) - - // GraphqlUpdate taking a setmap for graphql - GraphqlUpdate(id string, setmap map[string]interface{}) error - - // UpdateStatus updates the status of a playbook run. - UpdateStatus(statusPost *SQLStatusPost) error - - // FinishPlaybookRun finishes a run at endAt (in millis) - FinishPlaybookRun(playbookRunID string, endAt int64) error - - // RestorePlaybookRun restores a run at restoreAt (in millis) - RestorePlaybookRun(playbookRunID string, restoreAt int64) error - - // GetTimelineEvent returns the timeline event for playbookRunID by the timeline event ID. - GetTimelineEvent(playbookRunID, eventID string) (*TimelineEvent, error) - - // CreateTimelineEvent inserts the timeline event into the DB and returns the new event ID - CreateTimelineEvent(event *TimelineEvent) (*TimelineEvent, error) - - // UpdateTimelineEvent updates an existing timeline event - UpdateTimelineEvent(event *TimelineEvent) error - - // GetPlaybookRun gets a playbook run by ID. - GetPlaybookRun(playbookRunID string) (*PlaybookRun, error) - - // GetPlaybookRunIDsForChannel gets a playbook runs list associated with the given channel id. - GetPlaybookRunIDsForChannel(channelID string) ([]string, error) - - // GetHistoricalPlaybookRunParticipantsCount returns the count of all participants of the - // playbook run associated with the given channel id since the beginning of the - // playbook run, excluding bots. - GetHistoricalPlaybookRunParticipantsCount(channelID string) (int64, error) - - // GetOwners returns the owners of the playbook runs selected by options - GetOwners(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) ([]OwnerInfo, error) - - // NukeDB removes all playbook run related data. - NukeDB() error - - // ChangeCreationDate changes the creation date of the specified playbook run. - ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error - - // GetBroadcastChannelIDsToRootIDs takes a playbookRunID and returns the mapping of - // broadcastChannelID->rootID (to keep track of the status updates thread in each of the - // playbook's broadcast channels). - GetBroadcastChannelIDsToRootIDs(playbookRunID string) (map[string]string, error) - - // SetBroadcastChannelIDsToRootID sets the broadcastChannelID->rootID mappings for playbookRunID - SetBroadcastChannelIDsToRootID(playbookRunID string, channelIDsToRootIDs map[string]string) error - - // GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID - GetRunsWithAssignedTasks(userID string) ([]AssignedRun, error) - - // GetParticipatingRuns returns the list of active runs with userID as a participant - GetParticipatingRuns(userID string) ([]RunLink, error) - - // GetOverdueUpdateRuns returns the list of runs that userID is participating in that have overdue updates - GetOverdueUpdateRuns(userID string) ([]RunLink, error) - - // Follow method lets user follow a specific playbook run - Follow(playbookRunID, userID string) error - - // UnFollow method lets user unfollow a specific playbook run - Unfollow(playbookRunID, userID string) error - - // GetFollowers returns list of followers for a specific playbook run - GetFollowers(playbookRunID string) ([]string, error) - - // GetRunsActiveTotal returns number of active runs - GetRunsActiveTotal() (int64, error) - - // GetOverdueUpdateRunsTotal returns number of runs that have overdue status updates - GetOverdueUpdateRunsTotal() (int64, error) - - // GetOverdueRetroRunsTotal returns the number of completed runs without retro and with reminder - GetOverdueRetroRunsTotal() (int64, error) - - // GetFollowersActiveTotal returns total number of active followers, including duplicates - // if a user is following more than one run, it will be counted multiple times - GetFollowersActiveTotal() (int64, error) - - // GetParticipantsActiveTotal returns number of active participants - // (i.e. members of the playbook run channel when the run is active) - // if a user is member of more than one channel, it will be counted multiple times - GetParticipantsActiveTotal() (int64, error) - - // AddParticipants adds particpants to the run - AddParticipants(playbookRunID string, userIDs []string) error - - // RemoveParticipants removes participants from the run - RemoveParticipants(playbookRunID string, userIDs []string) error - - // GetSchemeRolesForChannel scheme role ids for the channel - GetSchemeRolesForChannel(channelID string) (string, string, string, error) - - // GetSchemeRolesForTeam scheme role ids for the team - GetSchemeRolesForTeam(teamID string) (string, string, string, error) - - // GetPlaybookRunIDsForUser returns run ids where user is a participant or is following - GetPlaybookRunIDsForUser(userID string) ([]string, error) - - // GetRunMetadataByIDs returns playbook runs metadata by passed run IDs. - // Notice that order of passed ids and returned runs might not coincide - GetRunMetadataByIDs(runIDs []string) ([]RunMetadata, error) - - // GetTaskAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs - GetTaskAsTopicMetadataByIDs(taskIDs []string) ([]TopicMetadata, error) - - // GetStatusAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs - GetStatusAsTopicMetadataByIDs(statusIDs []string) ([]TopicMetadata, error) -} - -// PlaybookRunTelemetry defines the methods that the PlaybookRunServiceImpl needs from the RudderTelemetry. -// Unless otherwise noted, userID is the user initiating the event. -type PlaybookRunTelemetry interface { - // CreatePlaybookRun tracks the creation of a new playbook run. - CreatePlaybookRun(playbookRun *PlaybookRun, userID string, public bool) - - // FinishPlaybookRun tracks the end of a playbook run. - FinishPlaybookRun(playbookRun *PlaybookRun, userID string) - - // RestorePlaybookRun tracks the restoration of a playbook run. - RestorePlaybookRun(playbookRun *PlaybookRun, userID string) - - // RestartPlaybookRun tracks the restart of a playbook run. - RestartPlaybookRun(playbookRun *PlaybookRun, userID string) - - // ChangeOwner tracks changes in owner. - ChangeOwner(playbookRun *PlaybookRun, userID string) - - // UpdateStatus tracks when a playbook run's status has been updated - UpdateStatus(playbookRun *PlaybookRun, userID string) - - // FrontendTelemetryForPlaybookRun tracks an event originating from the frontend - FrontendTelemetryForPlaybookRun(playbookRun *PlaybookRun, userID, action string) - - // AddPostToTimeline tracks userID creating a timeline event from a post. - AddPostToTimeline(playbookRun *PlaybookRun, userID string) - - // RemoveTimelineEvent tracks userID removing a timeline event. - RemoveTimelineEvent(playbookRun *PlaybookRun, userID string) - - // ModifyCheckedState tracks the checking and unchecking of items. - ModifyCheckedState(playbookRunID, userID string, task ChecklistItem, wasOwner bool) - - // SetAssignee tracks the changing of an assignee on an item. - SetAssignee(playbookRunID, userID string, task ChecklistItem) - - // AddTask tracks the creation of a new checklist item. - AddTask(playbookRunID, userID string, task ChecklistItem) - - // RemoveTask tracks the removal of a checklist item. - RemoveTask(playbookRunID, userID string, task ChecklistItem) - - // SkipChecklist tracks the skipping of a checklist. - SkipChecklist(playbookRunID, userID string, checklist Checklist) - - // RestoreChecklist tracks the restoring of a checklist. - RestoreChecklist(playbookRunID, userID string, checklist Checklist) - - // SkipTask tracks the skipping of a checklist item. - SkipTask(playbookRunID, userID string, task ChecklistItem) - - // RestoreTask tracks the restoring of a checklist item. - RestoreTask(playbookRunID, userID string, task ChecklistItem) - - // RenameTask tracks the update of a checklist item. - RenameTask(playbookRunID, userID string, task ChecklistItem) - - // MoveChecklist tracks the movement of a checklist - MoveChecklist(playbookRunID, userID string, task Checklist) - - // MoveTask tracks the movement of a checklist item - MoveTask(playbookRunID, userID string, task ChecklistItem) - - // RunTaskSlashCommand tracks the execution of a slash command attached to - // a checklist item. - RunTaskSlashCommand(playbookRunID, userID string, task ChecklistItem) - - // AddChecklsit tracks the creation of a new checklist. - AddChecklist(playbookRunID, userID string, checklist Checklist) - - // RemoveChecklist tracks the removal of a checklist. - RemoveChecklist(playbookRunID, userID string, checklist Checklist) - - // RenameChecklsit tracks the creation of a new checklist. - RenameChecklist(playbookRunID, userID string, checklist Checklist) - - // UpdateRetrospective event - UpdateRetrospective(playbookRun *PlaybookRun, userID string) - - // PublishRetrospective event - PublishRetrospective(playbookRun *PlaybookRun, userID string) - - // Follow tracks userID following a playbook run. - Follow(playbookRun *PlaybookRun, userID string) - - // Unfollow tracks userID following a playbook run. - Unfollow(playbookRun *PlaybookRun, userID string) - - // RunAction tracks the run actions, i.e., status broadcast action - RunAction(playbookRun *PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) -} - -type JobOnceScheduler interface { - Start() error - SetCallback(callback func(string)) error - ListScheduledJobs() ([]cluster.JobOnceMetadata, error) - ScheduleOnce(key string, runAt time.Time) (*cluster.JobOnce, error) - Cancel(key string) -} - -const PerPageDefault = 1000 - -// PlaybookRunFilterOptions specifies the optional parameters when getting playbook runs. -type PlaybookRunFilterOptions struct { - // Gets all the headers with this TeamID. - TeamID string `url:"team_id,omitempty"` - - // Pagination options. - Page int `url:"page,omitempty"` - PerPage int `url:"per_page,omitempty"` - - // Sort sorts by this header field in json format (eg, "create_at", "end_at", "name", etc.); - // defaults to "create_at". - Sort SortField `url:"sort,omitempty"` - - // Direction orders by ascending or descending, defaulting to ascending. - Direction SortDirection `url:"direction,omitempty"` - - // Statuses filters by all statuses in the list (inclusive) - Statuses []string - - // OwnerID filters by owner's Mattermost user ID. Defaults to blank (no filter). - OwnerID string `url:"owner_user_id,omitempty"` - - // ParticipantID filters playbook runs that have this member. Defaults to blank (no filter). - ParticipantID string `url:"participant_id,omitempty"` - - // ParticipantOrFollowerID filters playbook runs that have this user as member or as follower. Defaults to blank (no filter). - ParticipantOrFollowerID string `url:"participant_or_follower,omitempty"` - - // IncludeFavorites filters playbook runs that ParticipantOrFollowerID has marked as favorite. - // There's no impact if ParticipantOrFollowerID is empty. - IncludeFavorites bool `url:"include_favorites,omitempty"` - - // SearchTerm returns results of the search term and respecting the other header filter options. - // The search term acts as a filter and respects the Sort and Direction fields (i.e., results are - // not returned in relevance order). - SearchTerm string `url:"search_term,omitempty"` - - // PlaybookID filters playbook runs that are derived from this playbook id. - // Defaults to blank (no filter). - PlaybookID string `url:"playbook_id,omitempty"` - - // ActiveGTE filters playbook runs that were active after (or equal) to the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - ActiveGTE int64 `url:"active_gte,omitempty"` - - // ActiveLT filters playbook runs that were active before the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - ActiveLT int64 `url:"active_lt,omitempty"` - - // StartedGTE filters playbook runs that were started after (or equal) to the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - StartedGTE int64 `url:"started_gte,omitempty"` - - // StartedLT filters playbook runs that were started before the unix time given (in millis). - // A value of 0 means the filter is ignored (which is the default). - StartedLT int64 `url:"started_lt,omitempty"` - - // ChannelID filters to playbook runs that are associated with the given channel ID - ChannelID string `url:"channel_id,omitempty"` - - // Types filters by all run types in the list (inclusive) - Types []string -} - -// Clone duplicates the given options. -func (o *PlaybookRunFilterOptions) Clone() PlaybookRunFilterOptions { - newPlaybookRunFilterOptions := *o - if len(o.Statuses) > 0 { - newPlaybookRunFilterOptions.Statuses = append([]string{}, o.Statuses...) - } - if len(o.Types) > 0 { - newPlaybookRunFilterOptions.Types = append([]string{}, o.Types...) - } - - return newPlaybookRunFilterOptions -} - -// Validate returns a new, validated filter options or returns an error if invalid. -func (o PlaybookRunFilterOptions) Validate() (PlaybookRunFilterOptions, error) { - options := o.Clone() - - if options.PerPage <= 0 { - options.PerPage = PerPageDefault - } - - options.Sort = SortField(strings.ToLower(string(options.Sort))) - switch options.Sort { - case SortByCreateAt: - case SortByID: - case SortByName: - case SortByOwnerUserID: - case SortByTeamID: - case SortByEndAt: - case SortByStatus: - case SortByLastStatusUpdateAt: - case SortByMetric0, SortByMetric1, SortByMetric2, SortByMetric3: - case "": // default - options.Sort = SortByCreateAt - default: - return PlaybookRunFilterOptions{}, errors.Errorf("unsupported sort '%s'", options.Sort) - } - - options.Direction = SortDirection(strings.ToUpper(string(options.Direction))) - switch options.Direction { - case DirectionAsc: - case DirectionDesc: - case "": //default - options.Direction = DirectionAsc - default: - return PlaybookRunFilterOptions{}, errors.Errorf("unsupported direction '%s'", options.Direction) - } - - if options.TeamID != "" && !model.IsValidId(options.TeamID) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter 'team_id': must be 26 characters or blank") - } - - if options.OwnerID != "" && !model.IsValidId(options.OwnerID) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter 'owner_id': must be 26 characters or blank") - } - - if options.ParticipantID != "" && !model.IsValidId(options.ParticipantID) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter 'participant_id': must be 26 characters or blank") - } - - if options.ParticipantOrFollowerID != "" && !model.IsValidId(options.ParticipantOrFollowerID) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter 'participant_or_follower_id': must be 26 characters or blank") - } - - if options.PlaybookID != "" && !model.IsValidId(options.PlaybookID) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter 'playbook_id': must be 26 characters or blank") - } - - if options.ActiveGTE < 0 { - options.ActiveGTE = 0 - } - if options.ActiveLT < 0 { - options.ActiveLT = 0 - } - if options.StartedGTE < 0 { - options.StartedGTE = 0 - } - if options.StartedLT < 0 { - options.StartedLT = 0 - } - - if options.ChannelID != "" && !model.IsValidId(options.ChannelID) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter 'channel_id': must be 26 characters or blank") - } - - for _, s := range options.Statuses { - if !validStatus(s) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter in 'statuses': must be InProgress or Finished") - } - } - - for _, t := range options.Types { - if !validType(t) { - return PlaybookRunFilterOptions{}, errors.New("bad parameter in 'types': must be playbook or channel") - } - } - - return options, nil -} - -func validStatus(status string) bool { - return status == "" || status == StatusInProgress || status == StatusFinished -} - -func validType(runType string) bool { - return runType == RunTypePlaybook || runType == RunTypeChannelChecklist -} diff --git a/server/playbooks/server/app/playbook_run_service.go b/server/playbooks/server/app/playbook_run_service.go deleted file mode 100644 index aa501748de4..00000000000 --- a/server/playbooks/server/app/playbook_run_service.go +++ /dev/null @@ -1,3674 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "regexp" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - stripmd "github.com/writeas/go-strip-markdown" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/i18n" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/httptools" - "github.com/mattermost/mattermost/server/v8/playbooks/server/metrics" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/mattermost/mattermost/server/v8/playbooks/server/timeutils" -) - -const checklistItemDescriptionCharLimit = 4000 - -const ( - // PlaybookRunCreatedWSEvent is for playbook run creation. - PlaybookRunCreatedWSEvent = "playbook_run_created" - playbookRunUpdatedWSEvent = "playbook_run_updated" - noAssigneeName = "No Assignee" -) - -// PlaybookRunServiceImpl holds the information needed by the PlaybookRunService's methods to complete their functions. -type PlaybookRunServiceImpl struct { - httpClient *http.Client - configService config.Service - store PlaybookRunStore - poster bot.Poster - scheduler JobOnceScheduler - telemetry PlaybookRunTelemetry - genericTelemetry GenericTelemetry - api playbooks.ServicesAPI - playbookService PlaybookService - actionService ChannelActionService - permissions *PermissionsService - licenseChecker LicenseChecker - metricsService *metrics.Metrics -} - -var allNonSpaceNonWordRegex = regexp.MustCompile(`[^\w\s]`) - -// DialogFieldPlaybookIDKey is the key for the playbook ID field used in OpenCreatePlaybookRunDialog. -const DialogFieldPlaybookIDKey = "playbookID" - -// DialogFieldNameKey is the key for the playbook run name field used in OpenCreatePlaybookRunDialog. -const DialogFieldNameKey = "playbookRunName" - -// DialogFieldDescriptionKey is the key for the description textarea field used in UpdatePlaybookRunDialog -const DialogFieldDescriptionKey = "description" - -// DialogFieldMessageKey is the key for the message textarea field used in UpdatePlaybookRunDialog -const DialogFieldMessageKey = "message" - -// DialogFieldReminderInSecondsKey is the key for the reminder select field used in UpdatePlaybookRunDialog -const DialogFieldReminderInSecondsKey = "reminder" - -// DialogFieldFinishRun is the key for the "Finish run" bool field used in UpdatePlaybookRunDialog -const DialogFieldFinishRun = "finish_run" - -// DialogFieldPlaybookRunKey is the key for the playbook run chosen in AddToTimelineDialog -const DialogFieldPlaybookRunKey = "playbook_run" - -// DialogFieldSummary is the key for the summary in AddToTimelineDialog -const DialogFieldSummary = "summary" - -// DialogFieldItemName is the key for the playbook run name in AddChecklistItemDialog -const DialogFieldItemNameKey = "name" - -// DialogFieldDescriptionKey is the key for the description in AddChecklistItemDialog -const DialogFieldItemDescriptionKey = "description" - -// DialogFieldCommandKey is the key for the command in AddChecklistItemDialog -const DialogFieldItemCommandKey = "command" - -// NewPlaybookRunService creates a new PlaybookRunServiceImpl. -func NewPlaybookRunService( - store PlaybookRunStore, - poster bot.Poster, - configService config.Service, - scheduler JobOnceScheduler, - telemetry PlaybookRunTelemetry, - genericTelemetry GenericTelemetry, - api playbooks.ServicesAPI, - playbookService PlaybookService, - channelActionService ChannelActionService, - licenseChecker LicenseChecker, - metricsService *metrics.Metrics, -) *PlaybookRunServiceImpl { - service := &PlaybookRunServiceImpl{ - store: store, - poster: poster, - configService: configService, - scheduler: scheduler, - telemetry: telemetry, - genericTelemetry: genericTelemetry, - httpClient: httptools.MakeClient(api), - api: api, - playbookService: playbookService, - actionService: channelActionService, - licenseChecker: licenseChecker, - metricsService: metricsService, - } - - service.permissions = NewPermissionsService(service.playbookService, service, api, service.configService, service.licenseChecker) - - return service -} - -// GetPlaybookRuns returns filtered playbook runs and the total count before paging. -func (s *PlaybookRunServiceImpl) GetPlaybookRuns(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) (*GetPlaybookRunsResults, error) { - return s.store.GetPlaybookRuns(requesterInfo, options) -} - -func (s *PlaybookRunServiceImpl) buildPlaybookRunCreationMessageTemplate(playbookTitle, playbookID string, playbookRun *PlaybookRun, reporter *model.User) (string, error) { - return fmt.Sprintf( - "##### [%s](%s%s)\n@%s ran the [%s](%s) playbook.", - playbookRun.Name, - GetRunDetailsRelativeURL(playbookRun.ID), - "%s", // for the telemetry data injection - reporter.Username, - playbookTitle, - GetPlaybookDetailsRelativeURL(playbookID), - ), nil -} - -// PlaybookRunWebhookPayload is the body of the payload sent via playbook run webhooks. -type PlaybookRunWebhookPayload struct { - PlaybookRun - - // ChannelURL is the absolute URL of the playbook run channel. - ChannelURL string `json:"channel_url"` - - // DetailsURL is the absolute URL of the playbook run overview page. - DetailsURL string `json:"details_url"` - - // Event is metadata concerning the event that triggered this webhook. - Event PlaybookRunWebhookEvent `json:"event"` -} - -type PlaybookRunWebhookEvent struct { - // Type is the type of event emitted. - Type timelineEventType `json:"type"` - - // At is the time when the event occurred. - At int64 `json:"at"` - - // UserId is the user who triggered the event. - UserID string `json:"user_id"` - - // Payload is optional, event-specific metadata. - Payload interface{} `json:"payload"` -} - -// sendWebhooksOnCreation sends a POST request to the creation webhook URL. -// It blocks until a response is received. -func (s *PlaybookRunServiceImpl) sendWebhooksOnCreation(playbookRun PlaybookRun) { - siteURL := s.api.GetConfig().ServiceSettings.SiteURL - if siteURL == nil { - logrus.Error("cannot send webhook on creation, please set siteURL") - return - } - - team, err := s.api.GetTeam(playbookRun.TeamID) - if err != nil { - logrus.WithError(err).Error("cannot send webhook on creation, not able to get playbookRun.TeamID") - return - } - - channel, err := s.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - logrus.WithError(err).Error("cannot send webhook on creation, not able to get playbookRun.ChannelID") - return - } - - channelURL := getChannelURL(*siteURL, team.Name, channel.Name) - - detailsURL := getRunDetailsURL(*siteURL, playbookRun.ID) - - event := PlaybookRunWebhookEvent{ - Type: PlaybookRunCreated, - At: playbookRun.CreateAt, - UserID: playbookRun.ReporterUserID, - } - - payload := PlaybookRunWebhookPayload{ - PlaybookRun: playbookRun, - ChannelURL: channelURL, - DetailsURL: detailsURL, - Event: event, - } - - body, err := json.Marshal(payload) - if err != nil { - logrus.WithError(err).Error("cannot send webhook on creation, unable to marshal payload") - return - } - - triggerWebhooks(s, playbookRun.WebhookOnCreationURLs, body) -} - -// CreatePlaybookRun creates a new playbook run. userID is the user who initiated the CreatePlaybookRun. -func (s *PlaybookRunServiceImpl) CreatePlaybookRun(playbookRun *PlaybookRun, pb *Playbook, userID string, public bool) (*PlaybookRun, error) { - if playbookRun.DefaultOwnerID != "" { - // Check if the user is a member of the team to which the playbook run belongs. - if !IsMemberOfTeam(playbookRun.DefaultOwnerID, playbookRun.TeamID, s.api) { - logrus.WithFields(logrus.Fields{ - "user_id": playbookRun.DefaultOwnerID, - "team_id": playbookRun.TeamID, - }).Warn("default owner specified, but it is not a member of the playbook run's team") - } else { - playbookRun.OwnerUserID = playbookRun.DefaultOwnerID - } - } - - playbookRun.ReporterUserID = userID - playbookRun.ID = model.NewId() - - logger := logrus.WithField("playbook_run_id", playbookRun.ID) - - var err error - var channel *model.Channel - - if playbookRun.ChannelID == "" { - header := "This channel was created as part of a playbook run. To view more information, select the shield icon then select *Tasks* or *Overview*." - if pb != nil { - overviewURL := GetRunDetailsRelativeURL(playbookRun.ID) - playbookURL := GetPlaybookDetailsRelativeURL(pb.ID) - header = fmt.Sprintf("This channel was created as part of the [%s](%s) playbook. Visit [the overview page](%s) for more information.", - pb.Title, playbookURL, overviewURL) - } - - channel, err = s.createPlaybookRunChannel(playbookRun, header, public) - if err != nil { - return nil, err - } - - playbookRun.ChannelID = channel.Id - } else { - channel, err = s.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - return nil, err - } - - } - - if pb != nil && pb.ChannelMode == PlaybookRunCreateNewChannel && playbookRun.Name == "" { - playbookRun.Name = pb.ChannelNameTemplate - } - - if pb != nil && pb.MessageOnJoinEnabled && pb.MessageOnJoin != "" { - welcomeAction := GenericChannelAction{ - GenericChannelActionWithoutPayload: GenericChannelActionWithoutPayload{ - ChannelID: playbookRun.ChannelID, - Enabled: true, - ActionType: ActionTypeWelcomeMessage, - TriggerType: TriggerTypeNewMemberJoins, - }, - Payload: WelcomeMessagePayload{ - Message: pb.MessageOnJoin, - }, - } - - if _, err = s.actionService.Create(welcomeAction); err != nil { - logger.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("unable to create welcome action for new run in channel") - } - } - - if pb != nil && pb.CategorizeChannelEnabled && pb.CategoryName != "" { - categorizeChannelAction := GenericChannelAction{ - GenericChannelActionWithoutPayload: GenericChannelActionWithoutPayload{ - ChannelID: playbookRun.ChannelID, - Enabled: true, - ActionType: ActionTypeCategorizeChannel, - TriggerType: TriggerTypeNewMemberJoins, - }, - Payload: CategorizeChannelPayload{ - CategoryName: pb.CategoryName, - }, - } - - if _, err = s.actionService.Create(categorizeChannelAction); err != nil { - logger.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("unable to create welcome action for new run in channel") - } - } - - now := model.GetMillis() - playbookRun.CreateAt = now - playbookRun.LastStatusUpdateAt = now - playbookRun.CurrentStatus = StatusInProgress - - // Start with a blank playbook with one empty checklist if one isn't provided - if playbookRun.PlaybookID == "" { - playbookRun.Checklists = []Checklist{ - { - Title: "Checklist", - Items: []ChecklistItem{}, - }, - } - } - - playbookRun, err = s.store.CreatePlaybookRun(playbookRun) - if err != nil { - return nil, errors.Wrap(err, "failed to create playbook run") - } - - s.telemetry.CreatePlaybookRun(playbookRun, userID, public) - s.metricsService.IncrementRunsCreatedCount(1) - - err = s.addPlaybookRunInitialMemberships(playbookRun, channel) - if err != nil { - return nil, errors.Wrap(err, "failed to setup core memberships at run/channel") - } - - invitedUserIDs := playbookRun.InvitedUserIDs - - for _, groupID := range playbookRun.InvitedGroupIDs { - groupLogger := logger.WithField("group_id", groupID) - - var group *model.Group - group, err = s.api.GetGroup(groupID) - if err != nil { - groupLogger.WithError(err).Error("failed to query group") - continue - } - - if !group.AllowReference { - groupLogger.Warn("group that does not allow references") - continue - } - - perPage := 1000 - for page := 0; ; page++ { - var users []*model.User - users, err = s.api.GetGroupMemberUsers(groupID, page, perPage) - if err != nil { - groupLogger.WithError(err).Error("failed to query group") - break - } - for _, user := range users { - invitedUserIDs = append(invitedUserIDs, user.Id) - } - - if len(users) < perPage { - break - } - } - } - - err = s.AddParticipants(playbookRun.ID, invitedUserIDs, s.configService.GetConfiguration().BotUserID, false) - if err != nil { - logrus.WithError(err).WithFields(map[string]any{ - "playbookRunId": playbookRun.ID, - "invitedUserIDs": invitedUserIDs, - }).Warn("failed to add invited users on playbook run creation") - } - - if len(invitedUserIDs) > 0 { - s.genericTelemetry.Track( - telemetryRunParticipate, - map[string]any{ - "count": len(invitedUserIDs), - "trigger": "invite_on_create", - "playbookrun_id": playbookRun.ID, - }, - ) - } - - var reporter *model.User - reporter, err = s.api.GetUserByID(playbookRun.ReporterUserID) - if err != nil { - return nil, errors.Wrapf(err, "failed to resolve user %s", playbookRun.ReporterUserID) - } - - // Do we send a DM to the new owner? - if playbookRun.OwnerUserID != playbookRun.ReporterUserID { - startMessage := fmt.Sprintf("You have been assigned ownership of the run: [%s](%s), reported by @%s.", - playbookRun.Name, GetRunDetailsRelativeURL(playbookRun.ID), reporter.Username) - - if err = s.poster.DM(playbookRun.OwnerUserID, &model.Post{Message: startMessage}); err != nil { - return nil, errors.Wrapf(err, "failed to send DM on CreatePlaybookRun") - } - } - - if pb != nil { - var messageTemplate string - messageTemplate, err = s.buildPlaybookRunCreationMessageTemplate(pb.Title, pb.ID, playbookRun, reporter) - if err != nil { - return nil, errors.Wrapf(err, "failed to build the playbook run creation message") - } - - if playbookRun.StatusUpdateBroadcastChannelsEnabled { - s.broadcastPlaybookRunMessageToChannels(playbookRun.BroadcastChannelIDs, &model.Post{Message: fmt.Sprintf(messageTemplate, "")}, creationMessage, playbookRun, logger) - s.telemetry.RunAction(playbookRun, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRun.BroadcastChannelIDs)) - } - - // dm to users who are auto-following the playbook - telemetryString := fmt.Sprintf("?telem_action=follower_clicked_run_started_dm&telem_run_id=%s", playbookRun.ID) - err = s.dmPostToAutoFollows(&model.Post{Message: fmt.Sprintf(messageTemplate, telemetryString)}, pb.ID, playbookRun.ID, userID) - if err != nil { - logger.WithError(err).Error("failed to dm post to auto follows") - } - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: playbookRun.CreateAt, - EventAt: playbookRun.CreateAt, - EventType: PlaybookRunCreated, - SubjectUserID: playbookRun.ReporterUserID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return playbookRun, errors.Wrap(err, "failed to create timeline event") - } - playbookRun.TimelineEvents = append(playbookRun.TimelineEvents, *event) - - //auto-follow playbook run - if pb != nil { - var autoFollows []string - autoFollows, err = s.playbookService.GetAutoFollows(pb.ID) - if err != nil { - return playbookRun, errors.Wrapf(err, "failed to get autoFollows of the playbook `%s`", pb.ID) - } - for _, autoFollow := range autoFollows { - if err = s.Follow(playbookRun.ID, autoFollow); err != nil { - logger.WithError(err).WithFields(logrus.Fields{ - "playbook_run_id": playbookRun.ID, - "auto_follow": autoFollow, - }).Warn("failed to follow the playbook run") - } - } - } - - if len(playbookRun.WebhookOnCreationURLs) != 0 { - s.sendWebhooksOnCreation(*playbookRun) - } - - if playbookRun.PostID == "" { - return playbookRun, nil - } - - // Post the content and link of the original post - post, err := s.api.GetPost(playbookRun.PostID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get original post") - } - - postURL := fmt.Sprintf("/_redirect/pl/%s", playbookRun.PostID) - postMessage := fmt.Sprintf("[Original Post](%s)\n > %s", postURL, post.Message) - - _, err = s.poster.PostMessage(channel.Id, postMessage) - if err != nil { - return nil, errors.Wrapf(err, "failed to post to channel") - } - - return playbookRun, nil -} - -func (s *PlaybookRunServiceImpl) failedInvitedUserActions(usersFailedToInvite []string, channel *model.Channel) { - if len(usersFailedToInvite) == 0 { - return - } - - usernames := make([]string, 0, len(usersFailedToInvite)) - numDeletedUsers := 0 - for _, userID := range usersFailedToInvite { - user, userErr := s.api.GetUserByID(userID) - if userErr != nil { - // User does not exist anymore - numDeletedUsers++ - continue - } - - usernames = append(usernames, "@"+user.Username) - } - - deletedUsersMsg := "" - if numDeletedUsers > 0 { - deletedUsersMsg = fmt.Sprintf(" %d users from the original list have been deleted since the creation of the playbook.", numDeletedUsers) - } - - if _, err := s.poster.PostMessage(channel.Id, "Failed to invite the following users: %s. %s", strings.Join(usernames, ", "), deletedUsersMsg); err != nil { - logrus.WithError(err).Error("failedInvitedUserActions: failed to post to channel") - } -} - -// OpenCreatePlaybookRunDialog opens a interactive dialog to start a new playbook run. -func (s *PlaybookRunServiceImpl) OpenCreatePlaybookRunDialog(teamID, requesterID, triggerID, postID, clientID string, playbooks []Playbook) error { - - filteredPlaybooks := make([]Playbook, 0, len(playbooks)) - for _, playbook := range playbooks { - if err := s.permissions.RunCreate(requesterID, playbook); err == nil { - filteredPlaybooks = append(filteredPlaybooks, playbook) - } - } - - dialog, err := s.newPlaybookRunDialog(teamID, requesterID, postID, clientID, filteredPlaybooks) - if err != nil { - return errors.Wrapf(err, "failed to create new playbook run dialog") - } - - dialogRequest := model.OpenDialogRequest{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/dialog", - "playbooks"), - Dialog: *dialog, - TriggerId: triggerID, - } - - if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil { - return errors.Wrapf(err, "failed to open new playbook run dialog") - } - - return nil -} - -func (s *PlaybookRunServiceImpl) OpenUpdateStatusDialog(playbookRunID, userID, triggerID string) error { - currentPlaybookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - user, err := s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - message := "" - newestPostID := findNewestNonDeletedPostID(currentPlaybookRun.StatusPosts) - if newestPostID != "" { - var post *model.Post - post, err = s.api.GetPost(newestPostID) - if err != nil { - return errors.Wrap(err, "failed to find newest post") - } - message = post.Message - } else { - message = currentPlaybookRun.ReminderMessageTemplate - } - - dialog, err := s.newUpdatePlaybookRunDialog(currentPlaybookRun.Summary, message, len(currentPlaybookRun.BroadcastChannelIDs), currentPlaybookRun.PreviousReminder, user.Locale) - if err != nil { - return errors.Wrap(err, "failed to create update status dialog") - } - - dialogRequest := model.OpenDialogRequest{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/update-status-dialog", - "playbooks", - playbookRunID), - Dialog: *dialog, - TriggerId: triggerID, - } - - if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil { - return errors.Wrap(err, "failed to open update status dialog") - } - - return nil -} - -func (s *PlaybookRunServiceImpl) OpenAddToTimelineDialog(requesterInfo RequesterInfo, postID, teamID, triggerID string) error { - options := PlaybookRunFilterOptions{ - TeamID: teamID, - ParticipantID: requesterInfo.UserID, - Sort: SortByCreateAt, - Direction: DirectionDesc, - Types: []string{RunTypePlaybook}, - Page: 0, - PerPage: PerPageDefault, - } - - result, err := s.GetPlaybookRuns(requesterInfo, options) - if err != nil { - return errors.Wrap(err, "Error retrieving the playbook runs: %v") - } - - dialog, err := s.newAddToTimelineDialog(result.Items, postID, requesterInfo.UserID) - if err != nil { - return errors.Wrap(err, "failed to create add to timeline dialog") - } - - dialogRequest := model.OpenDialogRequest{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/add-to-timeline-dialog", - "playbooks"), - Dialog: *dialog, - TriggerId: triggerID, - } - - if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil { - return errors.Wrap(err, "failed to open update status dialog") - } - - return nil -} - -func (s *PlaybookRunServiceImpl) OpenAddChecklistItemDialog(triggerID, userID, playbookRunID string, checklist int) error { - user, err := s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - T := i18n.GetUserTranslations(user.Locale) - - dialog := &model.Dialog{ - Title: T("app.user.run.add_checklist_item.title"), - Elements: []model.DialogElement{ - { - DisplayName: T("app.user.run.add_checklist_item.name"), - Name: DialogFieldItemNameKey, - Type: "text", - Default: "", - }, - { - DisplayName: T("app.user.run.add_checklist_item.description"), - Name: DialogFieldItemDescriptionKey, - Type: "textarea", - Default: "", - Optional: true, - MaxLength: checklistItemDescriptionCharLimit, - }, - }, - SubmitLabel: T("app.user.run.add_checklist_item.submit_label"), - NotifyOnCancel: false, - } - - dialogRequest := model.OpenDialogRequest{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/checklists/%v/add-dialog", - "playbooks", playbookRunID, checklist), - Dialog: *dialog, - TriggerId: triggerID, - } - - if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil { - return errors.Wrap(err, "failed to open update status dialog") - } - - return nil -} - -func (s *PlaybookRunServiceImpl) AddPostToTimeline(playbookRunID, userID, postID, summary string) error { - post, err := s.api.GetPost(postID) - if err != nil { - return errors.Wrap(err, "failed to find post") - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: model.GetMillis(), - DeleteAt: 0, - EventAt: post.CreateAt, - EventType: EventFromPost, - Summary: summary, - Details: "", - PostID: postID, - SubjectUserID: post.UserId, - CreatorUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - playbookRunModified, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - s.telemetry.AddPostToTimeline(playbookRunModified, userID) - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// RemoveTimelineEvent removes the timeline event (sets the DeleteAt to the current time). -func (s *PlaybookRunServiceImpl) RemoveTimelineEvent(playbookRunID, userID, eventID string) error { - event, err := s.store.GetTimelineEvent(playbookRunID, eventID) - if err != nil { - return err - } - - event.DeleteAt = model.GetMillis() - if err = s.store.UpdateTimelineEvent(event); err != nil { - return err - } - - playbookRunModified, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - s.telemetry.RemoveTimelineEvent(playbookRunModified, userID) - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -func (s *PlaybookRunServiceImpl) buildStatusUpdatePost(statusUpdate, playbookRunID, authorID string) (*model.Post, error) { - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve playbook run for id '%s'", playbookRunID) - } - - authorUser, err := s.api.GetUserByID(authorID) - if err != nil { - return nil, errors.Wrapf(err, "error when trying to get the author user with ID '%s'", authorID) - } - - numTasks := 0 - numTasksChecked := 0 - for _, checklist := range playbookRun.Checklists { - numTasks += len(checklist.Items) - for _, task := range checklist.Items { - if task.State == ChecklistItemStateClosed { - numTasksChecked++ - } - } - } - - return &model.Post{ - Message: statusUpdate, - Type: "custom_run_update", - Props: map[string]interface{}{ - "numTasksChecked": numTasksChecked, - "numTasks": numTasks, - "participantIds": playbookRun.ParticipantIDs, - "authorUsername": authorUser.Username, - "playbookRunId": playbookRun.ID, - "runName": playbookRun.Name, - }, - }, nil -} - -// sendWebhooksOnUpdateStatus sends a POST request to the status update webhook URL. -// It blocks until a response is received. -func (s *PlaybookRunServiceImpl) sendWebhooksOnUpdateStatus(playbookRunID string, event *PlaybookRunWebhookEvent) { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - logger.WithError(err).Error("cannot send webhook on update, not able to get playbookRun") - return - } - - siteURL := s.api.GetConfig().ServiceSettings.SiteURL - if siteURL == nil { - logger.Error("cannot send webhook on update, please set siteURL") - return - } - - team, err := s.api.GetTeam(playbookRun.TeamID) - if err != nil { - logger.WithField("team_id", playbookRun.TeamID).Error("cannot send webhook on update, not able to get playbookRun.TeamID") - return - } - - channel, err := s.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - logger.WithField("channel_id", playbookRun.ChannelID).Error("cannot send webhook on update, not able to get playbookRun.ChannelID") - return - } - - channelURL := getChannelURL(*siteURL, team.Name, channel.Name) - - detailsURL := getRunDetailsURL(*siteURL, playbookRun.ID) - - payload := PlaybookRunWebhookPayload{ - PlaybookRun: *playbookRun, - ChannelURL: channelURL, - DetailsURL: detailsURL, - Event: *event, - } - - body, err := json.Marshal(payload) - if err != nil { - logger.WithError(err).Error("cannot send webhook on update, unable to marshal payload") - return - } - - triggerWebhooks(s, playbookRun.WebhookOnStatusUpdateURLs, body) -} - -// UpdateStatus updates a playbook run's status. -func (s *PlaybookRunServiceImpl) UpdateStatus(playbookRunID, userID string, options StatusUpdateOptions) error { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - originalPost, err := s.buildStatusUpdatePost(options.Message, playbookRunID, userID) - if err != nil { - return err - } - originalPost.ChannelId = playbookRunToModify.ChannelID - - channelPost := originalPost.Clone() - if err = s.poster.Post(channelPost); err != nil { - return errors.Wrap(err, "failed to post update status message") - } - - // Add the status manually for the broadcasts - playbookRunToModify.StatusPosts = append(playbookRunToModify.StatusPosts, - StatusPost{ - ID: channelPost.Id, - CreateAt: channelPost.CreateAt, - DeleteAt: channelPost.DeleteAt, - }) - - if err = s.store.UpdateStatus(&SQLStatusPost{ - PlaybookRunID: playbookRunID, - PostID: channelPost.Id, - }); err != nil { - return errors.Wrap(err, "failed to write status post to store. there is now inconsistent state") - } - - if playbookRunToModify.StatusUpdateBroadcastChannelsEnabled { - s.broadcastPlaybookRunMessageToChannels(playbookRunToModify.BroadcastChannelIDs, originalPost.Clone(), statusUpdateMessage, playbookRunToModify, logger) - s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToModify.BroadcastChannelIDs)) - } - - err = s.dmPostToRunFollowers(originalPost.Clone(), statusUpdateMessage, playbookRunID, userID) - if err != nil { - logger.WithError(err).Error("failed to dm post to run followers") - } - - // Remove pending reminder (if any), even if current reminder was set to "none" (0 minutes) - if err = s.SetNewReminder(playbookRunID, options.Reminder); err != nil { - return errors.Wrapf(err, "failed to set new reminder") - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: channelPost.CreateAt, - EventAt: channelPost.CreateAt, - EventType: StatusUpdated, - PostID: channelPost.Id, - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.telemetry.UpdateStatus(playbookRunToModify, userID) - s.sendPlaybookRunUpdatedWS(playbookRunID) - - if playbookRunToModify.StatusUpdateBroadcastWebhooksEnabled { - - webhookEvent := PlaybookRunWebhookEvent{ - Type: StatusUpdated, - At: channelPost.CreateAt, - UserID: userID, - Payload: options, - } - - s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent) - s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToModify.WebhookOnStatusUpdateURLs)) - } - - return nil -} - -func (s *PlaybookRunServiceImpl) OpenFinishPlaybookRunDialog(playbookRunID, userID, triggerID string) error { - currentPlaybookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - user, err := s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - numOutstanding := 0 - for _, c := range currentPlaybookRun.Checklists { - for _, item := range c.Items { - if item.State == ChecklistItemStateOpen || item.State == ChecklistItemStateInProgress { - numOutstanding++ - } - } - } - - dialogRequest := model.OpenDialogRequest{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/finish-dialog", - "playbooks", - playbookRunID), - Dialog: *s.newFinishPlaybookRunDialog(currentPlaybookRun, numOutstanding, user.Locale), - TriggerId: triggerID, - } - - if err := s.api.OpenInteractiveDialog(dialogRequest); err != nil { - return errors.Wrap(err, "failed to open finish run dialog") - } - - return nil -} - -func (s *PlaybookRunServiceImpl) buildRunFinishedMessage(playbookRun *PlaybookRun, userName string) string { - telemetryString := fmt.Sprintf("?telem_action=follower_clicked_run_finished_dm&telem_run_id=%s", playbookRun.ID) - announcementMsg := fmt.Sprintf( - "### Run finished: [%s](%s%s)\n", - playbookRun.Name, - GetRunDetailsRelativeURL(playbookRun.ID), - telemetryString, - ) - announcementMsg += fmt.Sprintf( - "@%s just marked [%s](%s%s) as finished. Visit the link above for more information.", - userName, - playbookRun.Name, - GetRunDetailsRelativeURL(playbookRun.ID), - telemetryString, - ) - - return announcementMsg -} - -func (s *PlaybookRunServiceImpl) buildStatusUpdateMessage(playbookRun *PlaybookRun, userName string, status string) string { - telemetryString := fmt.Sprintf("?telem_run_id=%s", playbookRun.ID) - announcementMsg := fmt.Sprintf( - "### Run status update %s : [%s](%s%s)\n", - status, - playbookRun.Name, - GetRunDetailsRelativeURL(playbookRun.ID), - telemetryString, - ) - announcementMsg += fmt.Sprintf( - "@%s %s status update for [%s](%s%s). Visit the link above for more information.", - userName, - status, - playbookRun.Name, - GetRunDetailsRelativeURL(playbookRun.ID), - telemetryString, - ) - - return announcementMsg -} - -// FinishPlaybookRun changes a run's state to Finished. If run is already in Finished state, the call is a noop. -func (s *PlaybookRunServiceImpl) FinishPlaybookRun(playbookRunID, userID string) error { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - if playbookRunToModify.CurrentStatus == StatusFinished { - return nil - } - - endAt := model.GetMillis() - if err = s.store.FinishPlaybookRun(playbookRunID, endAt); err != nil { - return err - } - - user, err := s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - message := fmt.Sprintf("@%s marked [%s](%s) as finished.", user.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID)) - postID := "" - post, err := s.poster.PostMessage(playbookRunToModify.ChannelID, message) - if err != nil { - logger.WithError(err).WithField("channel_id", playbookRunToModify.ChannelID).Error("failed to post the status update to channel") - } else { - postID = post.Id - } - - if playbookRunToModify.StatusUpdateBroadcastChannelsEnabled { - s.broadcastPlaybookRunMessageToChannels(playbookRunToModify.BroadcastChannelIDs, &model.Post{Message: message}, finishMessage, playbookRunToModify, logger) - s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToModify.BroadcastChannelIDs)) - } - - runFinishedMessage := s.buildRunFinishedMessage(playbookRunToModify, user.Username) - err = s.dmPostToRunFollowers(&model.Post{Message: runFinishedMessage}, finishMessage, playbookRunToModify.ID, userID) - if err != nil { - logger.WithError(err).Error("failed to dm post to run followers") - } - - // Remove pending reminder (if any), even if current reminder was set to "none" (0 minutes) - s.RemoveReminder(playbookRunID) - - err = s.resetReminderTimer(playbookRunID) - if err != nil { - logger.WithError(err).Error("failed to reset the reminder timer when updating status to Archived") - } - - // We are resolving the playbook run. Send the reminder to fill out the retrospective - // Also start the recurring reminder if enabled. - if s.licenseChecker.RetrospectiveAllowed() { - if playbookRunToModify.RetrospectiveEnabled && playbookRunToModify.RetrospectivePublishedAt == 0 { - if err = s.postRetrospectiveReminder(playbookRunToModify, true); err != nil { - return errors.Wrap(err, "couldn't post retrospective reminder") - } - s.scheduler.Cancel(RetrospectivePrefix + playbookRunID) - if playbookRunToModify.RetrospectiveReminderIntervalSeconds != 0 { - if err = s.SetReminder(RetrospectivePrefix+playbookRunID, time.Duration(playbookRunToModify.RetrospectiveReminderIntervalSeconds)*time.Second); err != nil { - return errors.Wrap(err, "failed to set the retrospective reminder for playbook run") - } - } - } - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: endAt, - EventAt: endAt, - EventType: RunFinished, - PostID: postID, - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.telemetry.FinishPlaybookRun(playbookRunToModify, userID) - s.metricsService.IncrementRunsFinishedCount(1) - s.sendPlaybookRunUpdatedWS(playbookRunID) - - if playbookRunToModify.StatusUpdateBroadcastWebhooksEnabled { - - webhookEvent := PlaybookRunWebhookEvent{ - Type: RunFinished, - At: endAt, - UserID: userID, - } - - s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent) - s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToModify.WebhookOnStatusUpdateURLs)) - } - - return nil -} - -func (s *PlaybookRunServiceImpl) ToggleStatusUpdates(playbookRunID, userID string, enable bool) error { - - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - logger := logrus.WithField("playbook_run_id", playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - updateAt := model.GetMillis() - playbookRunToModify.StatusUpdateEnabled = enable - - if playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil { - return err - } - - user, err := s.api.GetUserByID(userID) - T := i18n.GetUserTranslations(user.Locale) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - statusUpdate := "enabled" - eventType := StatusUpdatesEnabled - if !enable { - statusUpdate = "disabled" - eventType = StatusUpdatesDisabled - } - - data := map[string]interface{}{ - "RunName": playbookRunToModify.Name, - "RunURL": GetRunDetailsRelativeURL(playbookRunID), - "Username": user.Username, - } - - message := T("app.user.run.status_disable", data) - if enable { - message = T("app.user.run.status_enable", data) - } - - postID := "" - post, err := s.poster.PostMessage(playbookRunToModify.ChannelID, message) - if err != nil { - logger.WithError(err).WithField("channel_id", playbookRunToModify.ChannelID).Error("failed to post the status update to channel") - } else { - postID = post.Id - } - - if playbookRunToModify.StatusUpdateBroadcastChannelsEnabled { - s.broadcastPlaybookRunMessageToChannels(playbookRunToModify.BroadcastChannelIDs, &model.Post{Message: message}, statusUpdateMessage, playbookRunToModify, logger) - s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToModify.BroadcastChannelIDs)) - } - - runStatusUpdateMessage := s.buildStatusUpdateMessage(playbookRunToModify, user.Username, statusUpdate) - if err = s.dmPostToRunFollowers(&model.Post{Message: runStatusUpdateMessage}, statusUpdateMessage, playbookRunToModify.ID, userID); err != nil { - logger.WithError(err).Error("failed to dm post toggle-run-status-updates to run followers") - } - - // Remove pending reminder (if any), even if current reminder was set to "none" (0 minutes) - if !enable { - s.RemoveReminder(playbookRunID) - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: updateAt, - EventAt: updateAt, - EventType: eventType, - PostID: postID, - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID) - - if playbookRunToModify.StatusUpdateBroadcastWebhooksEnabled { - - webhookEvent := PlaybookRunWebhookEvent{ - Type: eventType, - At: updateAt, - UserID: userID, - } - - s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent) - s.telemetry.RunAction(playbookRunToModify, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToModify.WebhookOnStatusUpdateURLs)) - } - - return nil -} - -// RestorePlaybookRun reverts a run from the Finished state. If run was not in Finished state, the call is a noop. -func (s *PlaybookRunServiceImpl) RestorePlaybookRun(playbookRunID, userID string) error { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRunToRestore, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - if playbookRunToRestore.CurrentStatus != StatusFinished { - return nil - } - - restoreAt := model.GetMillis() - if err = s.store.RestorePlaybookRun(playbookRunID, restoreAt); err != nil { - return err - } - - user, err := s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - message := fmt.Sprintf("@%s changed the status of [%s](%s) from Finished to In Progress.", user.Username, playbookRunToRestore.Name, GetRunDetailsRelativeURL(playbookRunID)) - postID := "" - post, err := s.poster.PostMessage(playbookRunToRestore.ChannelID, message) - if err != nil { - logger.WithField("channel_id", playbookRunToRestore.ChannelID).Error("failed to post the status update to channel") - } else { - postID = post.Id - } - - if playbookRunToRestore.StatusUpdateBroadcastChannelsEnabled { - s.broadcastPlaybookRunMessageToChannels(playbookRunToRestore.BroadcastChannelIDs, &model.Post{Message: message}, restoreMessage, playbookRunToRestore, logger) - s.telemetry.RunAction(playbookRunToRestore, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastChannels, len(playbookRunToRestore.BroadcastChannelIDs)) - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: restoreAt, - EventAt: restoreAt, - EventType: RunRestored, - PostID: postID, - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.telemetry.RestorePlaybookRun(playbookRunToRestore, userID) - s.sendPlaybookRunUpdatedWS(playbookRunID) - - if playbookRunToRestore.StatusUpdateBroadcastWebhooksEnabled { - - webhookEvent := PlaybookRunWebhookEvent{ - Type: RunRestored, - At: restoreAt, - UserID: userID, - } - - s.sendWebhooksOnUpdateStatus(playbookRunID, &webhookEvent) - s.telemetry.RunAction(playbookRunToRestore, userID, TriggerTypeStatusUpdatePosted, ActionTypeBroadcastWebhooks, len(playbookRunToRestore.WebhookOnStatusUpdateURLs)) - } - - return nil -} - -// GraphqlUpdate updates fields based on a setmap -func (s *PlaybookRunServiceImpl) GraphqlUpdate(id string, setmap map[string]interface{}) error { - if len(setmap) == 0 { - return nil - } - if err := s.store.GraphqlUpdate(id, setmap); err != nil { - return err - } - s.sendPlaybookRunUpdatedWS(id) - return nil -} - -func (s *PlaybookRunServiceImpl) postRetrospectiveReminder(playbookRun *PlaybookRun, isInitial bool) error { - retrospectiveURL := getRunRetrospectiveURL("", playbookRun.ID) - - attachments := []*model.SlackAttachment{ - { - Actions: []*model.PostAction{ - { - Type: "button", - Name: "No Retrospective", - Integration: &model.PostActionIntegration{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/no-retrospective-button", - "playbooks", - playbookRun.ID), - }, - }, - }, - }, - } - - customPostType := "custom_retro_rem" - if isInitial { - customPostType = "custom_retro_rem_first" - } - - if _, err := s.poster.PostCustomMessageWithAttachments(playbookRun.ChannelID, customPostType, attachments, "@channel Reminder to [fill out the retrospective](%s).", retrospectiveURL); err != nil { - return errors.Wrap(err, "failed to post retro reminder to channel") - } - - return nil -} - -// GetPlaybookRun gets a playbook run by ID. Returns error if it could not be found. -func (s *PlaybookRunServiceImpl) GetPlaybookRun(playbookRunID string) (*PlaybookRun, error) { - return s.store.GetPlaybookRun(playbookRunID) -} - -// GetPlaybookRunMetadata gets ancillary metadata about a playbook run. -func (s *PlaybookRunServiceImpl) GetPlaybookRunMetadata(playbookRunID string) (*Metadata, error) { - playbookRun, err := s.GetPlaybookRun(playbookRunID) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve playbook run '%s'", playbookRunID) - } - - // Get main channel details - channel, err := s.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve channel id '%s'", playbookRun.ChannelID) - } - team, err := s.api.GetTeam(channel.TeamId) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve team id '%s'", channel.TeamId) - } - - numParticipants, err := s.store.GetHistoricalPlaybookRunParticipantsCount(playbookRun.ChannelID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get the count of playbook run members for channel id '%s'", playbookRun.ChannelID) - } - - followers, err := s.GetFollowers(playbookRunID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get followers of playbook run %s", playbookRunID) - } - - return &Metadata{ - ChannelName: channel.Name, - ChannelDisplayName: channel.DisplayName, - TeamName: team.Name, - TotalPosts: channel.TotalMsgCount, - NumParticipants: numParticipants, - Followers: followers, - }, nil -} - -// GetPlaybookRunsForChannelByUser get the playbookRuns list associated with this channel and user. -func (s *PlaybookRunServiceImpl) GetPlaybookRunsForChannelByUser(channelID string, userID string) ([]PlaybookRun, error) { - result, err := s.store.GetPlaybookRuns( - RequesterInfo{ - UserID: userID, - }, - - PlaybookRunFilterOptions{ - ChannelID: channelID, - Statuses: []string{StatusInProgress}, - Page: 0, - PerPage: 1000, - Sort: SortByCreateAt, - Direction: DirectionDesc, - Types: []string{RunTypePlaybook}, - }, - ) - - if err != nil { - return nil, err - } - return result.Items, nil -} - -// GetOwners returns all the owners of the playbook runs selected by options -func (s *PlaybookRunServiceImpl) GetOwners(requesterInfo RequesterInfo, options PlaybookRunFilterOptions) ([]OwnerInfo, error) { - owners, err := s.store.GetOwners(requesterInfo, options) - if err != nil { - return nil, errors.Wrap(err, "can't get owners from the store") - } - - // System admin can see fullname no matter the settings - if IsSystemAdmin(requesterInfo.UserID, s.api) { - return owners, nil - } - // If ShowFullName is true return owners info unedited - showFullName := s.api.GetConfig().PrivacySettings.ShowFullName - if showFullName != nil && *showFullName { - return owners, nil - } - // Remove names otherwise - for k, o := range owners { - o.FirstName = "" - o.LastName = "" - owners[k] = o - } - return owners, nil -} - -// IsOwner returns true if the userID is the owner for playbookRunID. -func (s *PlaybookRunServiceImpl) IsOwner(playbookRunID, userID string) bool { - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return false - } - return playbookRun.OwnerUserID == userID -} - -// ChangeOwner processes a request from userID to change the owner for playbookRunID -// to ownerID. Changing to the same ownerID is a no-op. -func (s *PlaybookRunServiceImpl) ChangeOwner(playbookRunID, userID, ownerID string) error { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return err - } - - if playbookRunToModify.OwnerUserID == ownerID { - return nil - } - - oldOwner, err := s.api.GetUserByID(playbookRunToModify.OwnerUserID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", playbookRunToModify.OwnerUserID) - } - newOwner, err := s.api.GetUserByID(ownerID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", ownerID) - } - subjectUser, err := s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - // add owner as user - err = s.AddParticipants(playbookRunID, []string{ownerID}, userID, false) - if err != nil { - return errors.Wrap(err, "failed to add owner as a participant") - } - - playbookRunToModify.OwnerUserID = ownerID - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - // Do we send a DM to the new owner? - if ownerID != userID { - msg := fmt.Sprintf("@%s changed the owner for run: [%s](%s) from **@%s** to **@%s**", - subjectUser.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunToModify.ID), - oldOwner.Username, newOwner.Username) - if err = s.poster.DM(ownerID, &model.Post{Message: msg}); err != nil { - return errors.Wrapf(err, "failed to send DM in ChangeOwner") - } - } - - eventTime := model.GetMillis() - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: eventTime, - EventAt: eventTime, - EventType: OwnerChanged, - Summary: fmt.Sprintf("@%s to @%s", oldOwner.Username, newOwner.Username), - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.telemetry.ChangeOwner(playbookRunToModify, userID) - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// ModifyCheckedState checks or unchecks the specified checklist item. Idempotent, will not perform -// any action if the checklist item is already in the given checked state -func (s *PlaybookRunServiceImpl) ModifyCheckedState(playbookRunID, userID, newState string, checklistNumber, itemNumber int) error { - type Details struct { - Action string `json:"action,omitempty"` - Task string `json:"task,omitempty"` - } - - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indicies") - } - - itemToCheck := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - if newState == itemToCheck.State { - return nil - } - - details := Details{ - Action: "check", - Task: stripmd.Strip(itemToCheck.Title), - } - - modifyMessage := fmt.Sprintf("checked off checklist item **%v**", stripmd.Strip(itemToCheck.Title)) - if newState == ChecklistItemStateOpen { - details.Action = "uncheck" - modifyMessage = fmt.Sprintf("unchecked checklist item **%v**", stripmd.Strip(itemToCheck.Title)) - } - if newState == ChecklistItemStateSkipped { - details.Action = "skip" - modifyMessage = fmt.Sprintf("skipped checklist item **%v**", stripmd.Strip(itemToCheck.Title)) - } - if itemToCheck.State == ChecklistItemStateSkipped && newState == ChecklistItemStateOpen { - details.Action = "restore" - modifyMessage = fmt.Sprintf("restored checklist item **%v**", stripmd.Strip(itemToCheck.Title)) - } - - itemToCheck.State = newState - itemToCheck.StateModified = model.GetMillis() - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run, is now in inconsistent state") - } - - s.telemetry.ModifyCheckedState(playbookRunID, userID, itemToCheck, playbookRunToModify.OwnerUserID == userID) - - detailsJSON, err := json.Marshal(details) - if err != nil { - return errors.Wrap(err, "failed to encode timeline event details") - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: itemToCheck.StateModified, - EventAt: itemToCheck.StateModified, - EventType: TaskStateModified, - Summary: modifyMessage, - SubjectUserID: userID, - Details: string(detailsJSON), - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// ToggleCheckedState checks or unchecks the specified checklist item -func (s *PlaybookRunServiceImpl) ToggleCheckedState(playbookRunID, userID string, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indices") - } - - isOpen := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State == ChecklistItemStateOpen - newState := ChecklistItemStateOpen - if isOpen { - newState = ChecklistItemStateClosed - } - - return s.ModifyCheckedState(playbookRunID, userID, newState, checklistNumber, itemNumber) -} - -// SetAssignee sets the assignee for the specified checklist item -// Idempotent, will not perform any actions if the checklist item is already assigned to assigneeID -func (s *PlaybookRunServiceImpl) SetAssignee(playbookRunID, userID, assigneeID string, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indices") - } - - itemToCheck := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - if assigneeID == itemToCheck.AssigneeID { - return nil - } - - newAssigneeUserAtMention := noAssigneeName - if assigneeID != "" { - var newUser *model.User - newUser, err = s.api.GetUserByID(assigneeID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", assigneeID) - } - newAssigneeUserAtMention = "@" + newUser.Username - } - - oldAssigneeUserAtMention := noAssigneeName - if itemToCheck.AssigneeID != "" { - var oldUser *model.User - oldUser, err = s.api.GetUserByID(itemToCheck.AssigneeID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", assigneeID) - } - oldAssigneeUserAtMention = "@" + oldUser.Username - } - - itemToCheck.AssigneeID = assigneeID - itemToCheck.AssigneeModified = model.GetMillis() - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run; it is now in an inconsistent state") - } - - // add the user as run participant if they was not already - if assigneeID != "" && assigneeID != playbookRunToModify.OwnerUserID { - var isParticipant bool - for _, participantID := range playbookRunToModify.ParticipantIDs { - if participantID == assigneeID { - isParticipant = true - break - } - } - if !isParticipant { - err = s.AddParticipants(playbookRunID, []string{assigneeID}, userID, false) - if err != nil { - return errors.Wrapf(err, "failed to add assignee to run") - } - } - } - - // Do we send a DM to the new assignee? - if itemToCheck.AssigneeID != "" && itemToCheck.AssigneeID != userID { - var subjectUser *model.User - subjectUser, err = s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to to resolve user %s", assigneeID) - } - - runURL := fmt.Sprintf("[%s](%s?from=dm_assignedtask)\n", playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID)) - modifyMessage := fmt.Sprintf("@%s assigned you the task **%s** (previously assigned to %s) for the run: %s #taskassigned", - subjectUser.Username, stripmd.Strip(itemToCheck.Title), oldAssigneeUserAtMention, runURL) - - if err = s.poster.DM(itemToCheck.AssigneeID, &model.Post{Message: modifyMessage}); err != nil { - return errors.Wrapf(err, "failed to send DM in SetAssignee") - } - } - - s.telemetry.SetAssignee(playbookRunID, userID, itemToCheck) - - modifyMessage := fmt.Sprintf("changed assignee of checklist item **%s** from **%s** to **%s**", - stripmd.Strip(itemToCheck.Title), oldAssigneeUserAtMention, newAssigneeUserAtMention) - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: itemToCheck.AssigneeModified, - EventAt: itemToCheck.AssigneeModified, - EventType: AssigneeChanged, - Summary: modifyMessage, - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// SetCommandToChecklistItem sets command to checklist item -func (s *PlaybookRunServiceImpl) SetCommandToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, newCommand string) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indices") - } - - // CommandLastRun is reset to avoid misunderstandings when the command is changed but the date - // of the previous run is set (and show rerun in the UI) - if playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Command != newCommand { - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].CommandLastRun = 0 - } - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Command = newCommand - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - - return nil -} - -func (s *PlaybookRunServiceImpl) SetTaskActionsToChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, taskActions []TaskAction) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indices") - } - - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].TaskActions = taskActions - - if playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - - return nil -} - -// SetDueDate sets absolute due date timestamp for the specified checklist item -func (s *PlaybookRunServiceImpl) SetDueDate(playbookRunID, userID string, duedate int64, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indices") - } - - itemToCheck := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - itemToCheck.DueDate = duedate - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] = itemToCheck - - _, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run; it is now in an inconsistent state") - } - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// RunChecklistItemSlashCommand executes the slash command associated with the specified checklist -// item. -func (s *PlaybookRunServiceImpl) RunChecklistItemSlashCommand(playbookRunID, userID string, checklistNumber, itemNumber int) (string, error) { - playbookRun, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return "", err - } - - if !IsValidChecklistItemIndex(playbookRun.Checklists, checklistNumber, itemNumber) { - return "", errors.New("invalid checklist item indices") - } - - itemToRun := playbookRun.Checklists[checklistNumber].Items[itemNumber] - if strings.TrimSpace(itemToRun.Command) == "" { - return "", errors.New("no slash command associated with this checklist item") - } - - // parse playbook summary for variables and values - varsAndVals := parseVariablesAndValues(playbookRun.Summary) - - // parse slash command for variables - varsInCmd := parseVariables(itemToRun.Command) - - command := itemToRun.Command - for _, v := range varsInCmd { - if val, ok := varsAndVals[v]; !ok || val == "" { - s.poster.EphemeralPost(userID, playbookRun.ChannelID, &model.Post{Message: fmt.Sprintf("Found undefined or empty variable in slash command: %s", v)}) - return "", errors.Errorf("Found undefined or empty variable in slash command: %s", v) - } - command = strings.ReplaceAll(command, v, varsAndVals[v]) - } - - cmdResponse, err := s.api.Execute(&model.CommandArgs{ - Command: command, - UserId: userID, - TeamId: playbookRun.TeamID, - ChannelId: playbookRun.ChannelID, - }) - if err == ErrNotFound { - trigger := strings.Fields(command)[0] - s.poster.EphemeralPost(userID, playbookRun.ChannelID, &model.Post{Message: fmt.Sprintf("Failed to find slash command **%s**", trigger)}) - - return "", errors.Wrap(err, "failed to find slash command") - } else if err != nil { - s.poster.EphemeralPost(userID, playbookRun.ChannelID, &model.Post{Message: fmt.Sprintf("Failed to execute slash command **%s**", command)}) - - return "", errors.Wrap(err, "failed to run slash command") - } - - // Fetch the playbook run again, in case the slash command actually changed the run - // (e.g. `/playbook owner`). - playbookRun, err = s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return "", errors.Wrapf(err, "failed to retrieve playbook run after running slash command") - } - - // Record the last (successful) run time. - playbookRun.Checklists[checklistNumber].Items[itemNumber].CommandLastRun = model.GetMillis() - - _, err = s.store.UpdatePlaybookRun(playbookRun) - if err != nil { - return "", errors.Wrapf(err, "failed to update playbook run recording run of slash command") - } - - s.telemetry.RunTaskSlashCommand(playbookRunID, userID, itemToRun) - - eventTime := model.GetMillis() - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: eventTime, - EventAt: eventTime, - EventType: RanSlashCommand, - Summary: fmt.Sprintf("ran the slash command: `%s`", command), - SubjectUserID: userID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return "", errors.Wrap(err, "failed to create timeline event") - } - s.sendPlaybookRunUpdatedWS(playbookRunID) - return cmdResponse.TriggerId, nil -} - -func (s *PlaybookRunServiceImpl) DuplicateChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - if !IsValidChecklistItemIndex(playbookRunToModify.Checklists, checklistNumber, itemNumber) { - return errors.New("invalid checklist item indicies") - } - - checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - checklistItem.ID = "" - - playbookRunToModify.Checklists[checklistNumber].Items = append( - playbookRunToModify.Checklists[checklistNumber].Items[:itemNumber+1], - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber:]...) - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber+1] = checklistItem - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.AddTask(playbookRunID, userID, checklistItem) - - return nil -} - -// AddChecklist adds a checklist to the specified run -func (s *PlaybookRunServiceImpl) AddChecklist(playbookRunID, userID string, checklist Checklist) error { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrapf(err, "failed to retrieve playbook run") - } - - playbookRunToModify.Checklists = append(playbookRunToModify.Checklists, checklist) - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.AddChecklist(playbookRunID, userID, checklist) - - return nil -} - -// DuplicateChecklist duplicates a checklist -func (s *PlaybookRunServiceImpl) DuplicateChecklist(playbookRunID, userID string, checklistNumber int) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - duplicate := playbookRunToModify.Checklists[checklistNumber].Clone() - playbookRunToModify.Checklists = append(playbookRunToModify.Checklists, duplicate) - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.AddChecklist(playbookRunID, userID, duplicate) - - return nil -} - -// RemoveChecklist removes the specified checklist -func (s *PlaybookRunServiceImpl) RemoveChecklist(playbookRunID, userID string, checklistNumber int) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - oldChecklist := playbookRunToModify.Checklists[checklistNumber] - - playbookRunToModify.Checklists = append(playbookRunToModify.Checklists[:checklistNumber], playbookRunToModify.Checklists[checklistNumber+1:]...) - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.RemoveChecklist(playbookRunID, userID, oldChecklist) - - return nil -} - -// RenameChecklist adds a checklist to the specified run -func (s *PlaybookRunServiceImpl) RenameChecklist(playbookRunID, userID string, checklistNumber int, newTitle string) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - playbookRunToModify.Checklists[checklistNumber].Title = newTitle - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID) - s.telemetry.RenameChecklist(playbookRunID, userID, playbookRunToModify.Checklists[checklistNumber]) - - return nil -} - -// AddChecklistItem adds an item to the specified checklist -func (s *PlaybookRunServiceImpl) AddChecklistItem(playbookRunID, userID string, checklistNumber int, checklistItem ChecklistItem) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - playbookRunToModify.Checklists[checklistNumber].Items = append(playbookRunToModify.Checklists[checklistNumber].Items, checklistItem) - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.AddTask(playbookRunID, userID, checklistItem) - - return nil -} - -// RemoveChecklistItem removes the item at the given index from the given checklist -func (s *PlaybookRunServiceImpl) RemoveChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - playbookRunToModify.Checklists[checklistNumber].Items = append( - playbookRunToModify.Checklists[checklistNumber].Items[:itemNumber], - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber+1:]..., - ) - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.RemoveTask(playbookRunID, userID, checklistItem) - - return nil -} - -// SkipChecklist skips the checklist -func (s *PlaybookRunServiceImpl) SkipChecklist(playbookRunID, userID string, checklistNumber int) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - for itemNumber := 0; itemNumber < len(playbookRunToModify.Checklists[checklistNumber].Items); itemNumber++ { - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].LastSkipped = model.GetMillis() - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateSkipped - } - - checklist := playbookRunToModify.Checklists[checklistNumber] - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.SkipChecklist(playbookRunID, userID, checklist) - - return nil -} - -// RestoreChecklist restores the skipped checklist -func (s *PlaybookRunServiceImpl) RestoreChecklist(playbookRunID, userID string, checklistNumber int) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return err - } - - for itemNumber := 0; itemNumber < len(playbookRunToModify.Checklists[checklistNumber].Items); itemNumber++ { - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateOpen - } - - checklist := playbookRunToModify.Checklists[checklistNumber] - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.RestoreChecklist(playbookRunID, userID, checklist) - - return nil -} - -// SkipChecklistItem skips the item at the given index from the given checklist -func (s *PlaybookRunServiceImpl) SkipChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].LastSkipped = model.GetMillis() - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateSkipped - - checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.SkipTask(playbookRunID, userID, checklistItem) - - return nil -} - -// RestoreChecklistItem restores the item at the given index from the given checklist -func (s *PlaybookRunServiceImpl) RestoreChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].State = ChecklistItemStateOpen - - checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.RestoreTask(playbookRunID, userID, checklistItem) - - return nil -} - -// EditChecklistItem changes the title of a specified checklist item -func (s *PlaybookRunServiceImpl) EditChecklistItem(playbookRunID, userID string, checklistNumber, itemNumber int, newTitle, newCommand, newDescription string) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, checklistNumber, itemNumber) - if err != nil { - return err - } - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Title = newTitle - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Command = newCommand - playbookRunToModify.Checklists[checklistNumber].Items[itemNumber].Description = newDescription - - checklistItem := playbookRunToModify.Checklists[checklistNumber].Items[itemNumber] - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.RenameTask(playbookRunID, userID, checklistItem) - - return nil -} - -// MoveChecklist moves a checklist to a new location -func (s *PlaybookRunServiceImpl) MoveChecklist(playbookRunID, userID string, sourceChecklistIdx, destChecklistIdx int) error { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, sourceChecklistIdx) - if err != nil { - return err - } - - if destChecklistIdx < 0 || destChecklistIdx >= len(playbookRunToModify.Checklists) { - return errors.New("invalid destChecklist") - } - - // Get checklist to move - checklistMoved := playbookRunToModify.Checklists[sourceChecklistIdx] - - // Delete checklist to move - copy(playbookRunToModify.Checklists[sourceChecklistIdx:], playbookRunToModify.Checklists[sourceChecklistIdx+1:]) - playbookRunToModify.Checklists[len(playbookRunToModify.Checklists)-1] = Checklist{} - - // Insert checklist in new location - copy(playbookRunToModify.Checklists[destChecklistIdx+1:], playbookRunToModify.Checklists[destChecklistIdx:]) - playbookRunToModify.Checklists[destChecklistIdx] = checklistMoved - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.MoveChecklist(playbookRunID, userID, checklistMoved) - - return nil -} - -// MoveChecklistItem moves a checklist item to a new location -func (s *PlaybookRunServiceImpl) MoveChecklistItem(playbookRunID, userID string, sourceChecklistIdx, sourceItemIdx, destChecklistIdx, destItemIdx int) error { - playbookRunToModify, err := s.checklistItemParamsVerify(playbookRunID, userID, sourceChecklistIdx, sourceItemIdx) - if err != nil { - return err - } - - if destChecklistIdx < 0 || destChecklistIdx >= len(playbookRunToModify.Checklists) { - return errors.New("invalid destChecklist") - } - - lenDestItems := len(playbookRunToModify.Checklists[destChecklistIdx].Items) - if (destItemIdx < 0) || (sourceChecklistIdx == destChecklistIdx && destItemIdx >= lenDestItems) || (destItemIdx > lenDestItems) { - return errors.New("invalid destItem") - } - - // Moved item - sourceChecklist := playbookRunToModify.Checklists[sourceChecklistIdx].Items - itemMoved := sourceChecklist[sourceItemIdx] - - // Delete item to move - sourceChecklist = append(sourceChecklist[:sourceItemIdx], sourceChecklist[sourceItemIdx+1:]...) - - // Insert item in new location - destChecklist := playbookRunToModify.Checklists[destChecklistIdx].Items - if sourceChecklistIdx == destChecklistIdx { - destChecklist = sourceChecklist - } - - destChecklist = append(destChecklist, ChecklistItem{}) - copy(destChecklist[destItemIdx+1:], destChecklist[destItemIdx:]) - destChecklist[destItemIdx] = itemMoved - - // Update the playbookRunToModify checklists. If the source and destination indices - // are the same, we only need to update the checklist to its final state (destChecklist) - if sourceChecklistIdx == destChecklistIdx { - playbookRunToModify.Checklists[sourceChecklistIdx].Items = destChecklist - } else { - playbookRunToModify.Checklists[sourceChecklistIdx].Items = sourceChecklist - playbookRunToModify.Checklists[destChecklistIdx].Items = destChecklist - } - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID, withPlaybookRun(playbookRunToModify)) - s.telemetry.MoveTask(playbookRunID, userID, itemMoved) - - return nil -} - -// GetChecklistAutocomplete returns the list of checklist items for playbookRuns to be used in autocomplete -func (s *PlaybookRunServiceImpl) GetChecklistAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) { - ret := make([]model.AutocompleteListItem, 0) - multipleRuns := len(playbookRuns) > 1 - - for j, playbookRun := range playbookRuns { - runIndex := "" - runName := "" - // include run number and name only if there are multiple runs - if multipleRuns { - runIndex = fmt.Sprintf("%d ", j) - runName = fmt.Sprintf("\"%s\" - ", playbookRun.Name) - } - - for i, checklist := range playbookRun.Checklists { - ret = append(ret, model.AutocompleteListItem{ - Item: fmt.Sprintf("%s%d", runIndex, i), - Hint: fmt.Sprintf("%s\"%s\"", runName, stripmd.Strip(checklist.Title)), - }) - } - } - - return ret, nil -} - -// GetChecklistItemAutocomplete returns the list of checklist items for playbookRuns to be used in autocomplete -func (s *PlaybookRunServiceImpl) GetChecklistItemAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) { - ret := make([]model.AutocompleteListItem, 0) - multipleRuns := len(playbookRuns) > 1 - - for k, playbookRun := range playbookRuns { - runIndex := "" - runName := "" - // include run number and name only if there are multiple runs - if multipleRuns { - runIndex = fmt.Sprintf("%d ", k) - runName = fmt.Sprintf("\"%s\" - ", playbookRun.Name) - } - - for i, checklist := range playbookRun.Checklists { - for j, item := range checklist.Items { - ret = append(ret, model.AutocompleteListItem{ - Item: fmt.Sprintf("%s%d %d", runIndex, i, j), - Hint: fmt.Sprintf("%s\"%s\"", runName, stripmd.Strip(item.Title)), - }) - } - } - } - - return ret, nil -} - -// GetRunsAutocomplete returns the list of runs to be used in autocomplete -func (s *PlaybookRunServiceImpl) GetRunsAutocomplete(playbookRuns []PlaybookRun) ([]model.AutocompleteListItem, error) { - if len(playbookRuns) <= 1 { - return nil, nil - } - ret := make([]model.AutocompleteListItem, 0) - - for i, playbookRun := range playbookRuns { - ret = append(ret, model.AutocompleteListItem{ - Item: fmt.Sprintf("%d", i), - Hint: fmt.Sprintf("\"%s\"", playbookRun.Name), - }) - } - - return ret, nil -} - -type TodoDigestMessageItems struct { - overdueRuns []RunLink - assignedRuns []AssignedRun - inProgressRuns []RunLink -} - -func (s *PlaybookRunServiceImpl) getTodoDigestMessageItems(userID string) (*TodoDigestMessageItems, error) { - runsOverdue, err := s.GetOverdueUpdateRuns(userID) - if err != nil { - return nil, err - } - - runsAssigned, err := s.GetRunsWithAssignedTasks(userID) - if err != nil { - return nil, err - } - - runsInProgress, err := s.GetParticipatingRuns(userID) - if err != nil { - return nil, err - } - - return &TodoDigestMessageItems{ - overdueRuns: runsOverdue, - assignedRuns: runsAssigned, - inProgressRuns: runsInProgress, - }, nil - -} - -// buildTodoDigestMessage -// gathers the list of assigned tasks, participating runs, and overdue updates and builds a combined message with them -func (s *PlaybookRunServiceImpl) buildTodoDigestMessage(userID string, force bool, shouldSendFullData bool) (*model.Post, error) { - digestMessageItems, err := s.getTodoDigestMessageItems(userID) - if err != nil { - return nil, err - } - - // if we have no items to send and we're not forced to, return early - if len(digestMessageItems.assignedRuns) == 0 && - len(digestMessageItems.overdueRuns) == 0 && - len(digestMessageItems.inProgressRuns) == 0 && - !force { - return nil, nil - } - - user, err := s.api.GetUserByID(userID) - if err != nil { - return nil, err - } - - part1 := buildRunsOverdueMessage(digestMessageItems.overdueRuns, user.Locale) - - timezone, err := timeutils.GetUserTimezone(user) - if err != nil { - return nil, err - } - - part2 := buildAssignedTaskMessageSummary(digestMessageItems.assignedRuns, user.Locale, timezone, !force) - part3 := buildRunsInProgressMessage(digestMessageItems.inProgressRuns, user.Locale) - - var message string - if shouldSendFullData || len(digestMessageItems.overdueRuns) > 0 { - message += part1 - } - if shouldSendFullData || len(digestMessageItems.assignedRuns) > 0 { - message += part2 - } - if shouldSendFullData || len(digestMessageItems.inProgressRuns) > 0 { - message += part3 - } - - return &model.Post{Message: message}, nil -} - -// EphemeralPostTodoDigestToUser -// builds todo digest message and sends an ephemeral post to userID, channelID. Use force = true to send post even if there are no items. -func (s *PlaybookRunServiceImpl) EphemeralPostTodoDigestToUser(userID string, channelID string, force bool, shouldSendFullData bool) error { - todoDigestMessage, err := s.buildTodoDigestMessage(userID, force, shouldSendFullData) - if err != nil { - return err - } - - if todoDigestMessage != nil { - s.poster.EphemeralPost(userID, channelID, todoDigestMessage) - return nil - } - - return nil -} - -// DMTodoDigestToUser -// DMs the message to userID. Use force = true to DM even if there are no items. -func (s *PlaybookRunServiceImpl) DMTodoDigestToUser(userID string, force bool, shouldSendFullData bool) error { - todoDigestMessage, err := s.buildTodoDigestMessage(userID, force, shouldSendFullData) - if err != nil { - return err - } - - if todoDigestMessage != nil { - return s.poster.DM(userID, todoDigestMessage) - } - - return nil -} - -// GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID -func (s *PlaybookRunServiceImpl) GetRunsWithAssignedTasks(userID string) ([]AssignedRun, error) { - return s.store.GetRunsWithAssignedTasks(userID) -} - -// GetParticipatingRuns returns the list of active runs with userID as a participant -func (s *PlaybookRunServiceImpl) GetParticipatingRuns(userID string) ([]RunLink, error) { - return s.store.GetParticipatingRuns(userID) -} - -// GetOverdueUpdateRuns returns the list of userID's runs that have overdue updates -func (s *PlaybookRunServiceImpl) GetOverdueUpdateRuns(userID string) ([]RunLink, error) { - return s.store.GetOverdueUpdateRuns(userID) -} - -func (s *PlaybookRunServiceImpl) checklistParamsVerify(playbookRunID, userID string, checklistNumber int) (*PlaybookRun, error) { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve playbook run") - } - - if checklistNumber < 0 || checklistNumber >= len(playbookRunToModify.Checklists) { - return nil, errors.New("invalid checklist number") - } - - return playbookRunToModify, nil -} - -func (s *PlaybookRunServiceImpl) checklistItemParamsVerify(playbookRunID, userID string, checklistNumber, itemNumber int) (*PlaybookRun, error) { - playbookRunToModify, err := s.checklistParamsVerify(playbookRunID, userID, checklistNumber) - if err != nil { - return nil, err - } - - if itemNumber < 0 || itemNumber >= len(playbookRunToModify.Checklists[checklistNumber].Items) { - return nil, errors.New("invalid item number") - } - - return playbookRunToModify, nil -} - -// NukeDB removes all playbook run related data. -func (s *PlaybookRunServiceImpl) NukeDB() error { - return s.store.NukeDB() -} - -// ChangeCreationDate changes the creation date of the playbook run. -func (s *PlaybookRunServiceImpl) ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error { - return s.store.ChangeCreationDate(playbookRunID, creationTimestamp) -} - -func (s *PlaybookRunServiceImpl) createPlaybookRunChannel(playbookRun *PlaybookRun, header string, public bool) (*model.Channel, error) { - channelType := model.ChannelTypePrivate - if public { - channelType = model.ChannelTypeOpen - } - - channel := &model.Channel{ - TeamId: playbookRun.TeamID, - Type: channelType, - DisplayName: playbookRun.Name, - Name: cleanChannelName(playbookRun.Name), - Header: header, - } - - if channel.Name == "" { - channel.Name = model.NewId() - } - - // Prefer the channel name the user chose. But if it already exists, add some random bits - // and try exactly once more. - err := s.api.CreateChannel(channel) - if err != nil { - if appErr, ok := err.(*model.AppError); ok { - // Let the user correct display name errors: - if appErr.Id == "model.channel.is_valid.display_name.app_error" || - appErr.Id == "model.channel.is_valid.1_or_more.app_error" { - return nil, ErrChannelDisplayNameInvalid - } - - // We can fix channel Name errors: - if appErr.Id == "store.sql_channel.save_channel.exists.app_error" { - channel.Name = addRandomBits(channel.Name) - // clean channel id - channel.Id = "" - err = s.api.CreateChannel(channel) - } - } - - if err != nil { - return nil, errors.Wrapf(err, "failed to create channel") - } - } - - return channel, nil -} - -// addPlaybookRunInitialMemberships creates the memberships in run and channels for the most core users: playbooksbot, reporter and owner -func (s *PlaybookRunServiceImpl) addPlaybookRunInitialMemberships(playbookRun *PlaybookRun, channel *model.Channel) error { - if _, err := s.api.CreateMember(channel.TeamId, s.configService.GetConfiguration().BotUserID); err != nil { - return errors.Wrapf(err, "failed to add bot to the team") - } - - // channel related - if _, err := s.api.AddMemberToChannel(channel.Id, s.configService.GetConfiguration().BotUserID); err != nil { - return errors.Wrapf(err, "failed to add bot to the channel") - } - - if _, err := s.api.AddUserToChannel(channel.Id, playbookRun.ReporterUserID, s.configService.GetConfiguration().BotUserID); err != nil { - return errors.Wrapf(err, "failed to add reporter to the channel") - } - - if playbookRun.OwnerUserID != playbookRun.ReporterUserID { - if _, err := s.api.AddUserToChannel(channel.Id, playbookRun.OwnerUserID, s.configService.GetConfiguration().BotUserID); err != nil { - return errors.Wrapf(err, "failed to add owner to channel") - } - } - - _, userRoleID, adminRoleID := s.GetSchemeRolesForChannel(channel) - if _, err := s.api.UpdateChannelMemberRoles(channel.Id, playbookRun.OwnerUserID, fmt.Sprintf("%s %s", userRoleID, adminRoleID)); err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "channel_id": channel.Id, - "owner_user_id": playbookRun.OwnerUserID, - }).Warn("failed to promote owner to admin") - } - - // run related - participants := []string{playbookRun.OwnerUserID} - if playbookRun.OwnerUserID != playbookRun.ReporterUserID { - participants = append(participants, playbookRun.ReporterUserID) - } - err := s.AddParticipants(playbookRun.ID, participants, playbookRun.ReporterUserID, false) - if err != nil { - return errors.Wrap(err, "failed to add owner/reporter as a participant") - } - return nil -} - -func (s *PlaybookRunServiceImpl) GetSchemeRolesForChannel(channel *model.Channel) (string, string, string) { - // get channel roles - if guestRole, userRole, adminRole, err := s.store.GetSchemeRolesForChannel(channel.Id); err == nil { - return guestRole, userRole, adminRole - } - - // get team roles if channel roles are not available - if guestRole, userRole, adminRole, err := s.store.GetSchemeRolesForTeam(channel.TeamId); err == nil { - return guestRole, userRole, adminRole - } - - // return default roles - return model.ChannelGuestRoleId, model.ChannelUserRoleId, model.ChannelAdminRoleId -} - -func (s *PlaybookRunServiceImpl) newFinishPlaybookRunDialog(playbookRun *PlaybookRun, outstanding int, locale string) *model.Dialog { - T := i18n.GetUserTranslations(locale) - - data := map[string]interface{}{ - "RunName": playbookRun.Name, - "Count": outstanding, - } - message := T("app.user.run.confirm_finish.num_outstanding", data) - - return &model.Dialog{ - Title: T("app.user.run.confirm_finish.title"), - IntroductionText: message, - SubmitLabel: T("app.user.run.confirm_finish.submit_label"), - NotifyOnCancel: false, - } -} - -func (s *PlaybookRunServiceImpl) newPlaybookRunDialog(teamID, requesterID, postID, clientID string, playbooks []Playbook) (*model.Dialog, error) { - user, err := s.api.GetUserByID(requesterID) - if err != nil { - return nil, errors.Wrapf(err, "failed to fetch owner user") - } - - T := i18n.GetUserTranslations(user.Locale) - - state, err := json.Marshal(DialogState{ - PostID: postID, - ClientID: clientID, - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal DialogState") - } - - var options []*model.PostActionOptions - for _, playbook := range playbooks { - options = append(options, &model.PostActionOptions{ - Text: playbook.Title, - Value: playbook.ID, - }) - } - - data := map[string]interface{}{ - "Username": getUserDisplayName(user), - } - introText := T("app.user.new_run.intro", data) - - defaultPlaybookID := "" - defaultChannelNameTemplate := "" - if len(playbooks) == 1 { - defaultPlaybookID = playbooks[0].ID - defaultChannelNameTemplate = playbooks[0].ChannelNameTemplate - } - - return &model.Dialog{ - Title: T("app.user.new_run.title"), - IntroductionText: introText, - Elements: []model.DialogElement{ - { - DisplayName: T("app.user.new_run.playbook"), - Name: DialogFieldPlaybookIDKey, - Type: "select", - Options: options, - Default: defaultPlaybookID, - }, - { - DisplayName: T("app.user.new_run.run_name"), - Name: DialogFieldNameKey, - Type: "text", - MinLength: 1, - MaxLength: 64, - Default: defaultChannelNameTemplate, - }, - }, - SubmitLabel: T("app.user.new_run.submit_label"), - NotifyOnCancel: false, - State: string(state), - }, nil -} - -func (s *PlaybookRunServiceImpl) newUpdatePlaybookRunDialog(description, message string, broadcastChannelNum int, reminderTimer time.Duration, locale string) (*model.Dialog, error) { - T := i18n.GetUserTranslations(locale) - - data := map[string]interface{}{ - "Count": broadcastChannelNum, - } - introductionText := T("app.user.run.update_status.num_channel", data) - - reminderOptions := []*model.PostActionOptions{ - { - Text: "15min", - Value: "900", - }, - { - Text: "30min", - Value: "1800", - }, - { - Text: "60min", - Value: "3600", - }, - { - Text: "4hr", - Value: "14400", - }, - { - Text: "24hr", - Value: "86400", - }, - { - Text: "1Week", - Value: "604800", - }, - } - - if s.configService.IsConfiguredForDevelopmentAndTesting() { - reminderOptions = append(reminderOptions, nil) - copy(reminderOptions[2:], reminderOptions[1:]) - reminderOptions[1] = &model.PostActionOptions{ - Text: "10sec", - Value: "10", - } - } - - return &model.Dialog{ - Title: T("app.user.run.update_status.title"), - IntroductionText: introductionText, - Elements: []model.DialogElement{ - { - DisplayName: T("app.user.run.update_status.change_since_last_update"), - Name: DialogFieldMessageKey, - Type: "textarea", - Default: message, - }, - { - DisplayName: T("app.user.run.update_status.reminder_for_next_update"), - Name: DialogFieldReminderInSecondsKey, - Type: "select", - Options: reminderOptions, - Optional: true, - Default: fmt.Sprintf("%d", reminderTimer/time.Second), - }, - { - DisplayName: T("app.user.run.update_status.finish_run"), - Name: DialogFieldFinishRun, - Placeholder: T("app.user.run.update_status.finish_run.placeholder"), - Type: "bool", - Optional: true, - }, - }, - SubmitLabel: T("app.user.run.update_status.submit_label"), - NotifyOnCancel: false, - }, nil -} - -func (s *PlaybookRunServiceImpl) newAddToTimelineDialog(playbookRuns []PlaybookRun, postID, userID string) (*model.Dialog, error) { - user, err := s.api.GetUserByID(userID) - if err != nil { - return nil, errors.Wrapf(err, "failed to to resolve user %s", userID) - } - - T := i18n.GetUserTranslations(user.Locale) - - var options []*model.PostActionOptions - for _, i := range playbookRuns { - options = append(options, &model.PostActionOptions{ - Text: i.Name, - Value: i.ID, - }) - } - - state, err := json.Marshal(DialogStateAddToTimeline{ - PostID: postID, - }) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal DialogState") - } - - post, err := s.api.GetPost(postID) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal DialogState") - } - defaultSummary := "" - if post.Message != "" { - end := min(40, len(post.Message)) - defaultSummary = post.Message[:end] - if len(post.Message) > end { - defaultSummary += "..." - } - } - - defaultPlaybookRuns, err := s.GetPlaybookRunsForChannelByUser(post.ChannelId, userID) - if err != nil && !errors.Is(err, ErrNotFound) { - return nil, errors.Wrapf(err, "failed to get playbookRunID for channel") - } - - defaultRunID := "" - if len(defaultPlaybookRuns) == 1 { - defaultRunID = defaultPlaybookRuns[0].ID - } - - return &model.Dialog{ - Title: T("app.user.run.add_to_timeline.title"), - Elements: []model.DialogElement{ - { - DisplayName: T("app.user.run.add_to_timeline.playbook_run"), - Name: DialogFieldPlaybookRunKey, - Type: "select", - Options: options, - Default: defaultRunID, - }, - { - DisplayName: T("app.user.run.add_to_timeline.summary"), - Name: DialogFieldSummary, - Type: "text", - MaxLength: 64, - Placeholder: T("app.user.run.add_to_timeline.summary.placeholder"), - Default: defaultSummary, - HelpText: T("app.user.run.add_to_timeline.summary.help"), - }, - }, - SubmitLabel: T("app.user.run.add_to_timeline.submit_label"), - NotifyOnCancel: false, - State: string(state), - }, nil -} - -// structure to handle optional parameters for sendPlaybookRunUpdatedWS -type RunWSOptions struct { - AdditionalUserIDs []string - PlaybookRun *PlaybookRun -} -type RunWSOption func(options *RunWSOptions) - -func withAdditionalUserIDs(additionalUserIDs []string) RunWSOption { - return func(options *RunWSOptions) { - options.AdditionalUserIDs = append(options.AdditionalUserIDs, additionalUserIDs...) - } -} - -func withPlaybookRun(playbookRun *PlaybookRun) RunWSOption { - return func(options *RunWSOptions) { - options.PlaybookRun = playbookRun - } -} - -// sendPlaybookRunUpdatedWS send run updates to users via websocket -// Individual Websocket messages will be sent to the owner/participants and users -// (optionally passed as parameter) -func (s *PlaybookRunServiceImpl) sendPlaybookRunUpdatedWS(playbookRunID string, options ...RunWSOption) { - var err error - - sendWSOptions := RunWSOptions{} - for _, option := range options { - option(&sendWSOptions) - } - - // Get playbookRun if not provided - playbookRun := sendWSOptions.PlaybookRun - if playbookRun == nil { - playbookRun, err = s.store.GetPlaybookRun(playbookRunID) - if err != nil { - logrus.WithError(err).WithField("playbookRunID", playbookRunID).Error("failed to retrieve playbook run when sending websocket") - return - } - } - - // create a unique list of user ids to send the message to - uniqueUserIDs := make(map[string]bool, len(sendWSOptions.AdditionalUserIDs)+len(playbookRun.ParticipantIDs)+1) - uniqueUserIDs[playbookRun.OwnerUserID] = true - for _, u := range sendWSOptions.AdditionalUserIDs { - uniqueUserIDs[u] = true - } - for _, u := range playbookRun.ParticipantIDs { - uniqueUserIDs[u] = true - } - - // send the websocket message - for userID := range uniqueUserIDs { - s.poster.PublishWebsocketEventToUser(playbookRunUpdatedWSEvent, playbookRun, userID) - } -} - -func (s *PlaybookRunServiceImpl) UpdateRetrospective(playbookRunID, updaterID string, newRetrospective RetrospectiveUpdate) error { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - playbookRunToModify.Retrospective = newRetrospective.Text - playbookRunToModify.MetricsData = newRetrospective.Metrics - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrap(err, "failed to update playbook run") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID) - s.telemetry.UpdateRetrospective(playbookRunToModify, updaterID) - - return nil -} - -func (s *PlaybookRunServiceImpl) PublishRetrospective(playbookRunID, publisherID string, retrospective RetrospectiveUpdate) error { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRunToPublish, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - now := model.GetMillis() - - // Update the text to keep syncronized - playbookRunToPublish.Retrospective = retrospective.Text - playbookRunToPublish.MetricsData = retrospective.Metrics - playbookRunToPublish.RetrospectivePublishedAt = now - playbookRunToPublish.RetrospectiveWasCanceled = false - - playbookRunToPublish, err = s.store.UpdatePlaybookRun(playbookRunToPublish) - if err != nil { - return errors.Wrap(err, "failed to update playbook run") - } - - publisherUser, err := s.api.GetUserByID(publisherID) - if err != nil { - return errors.Wrap(err, "failed to get publisher user") - } - - retrospectiveURL := getRunRetrospectiveURL("", playbookRunToPublish.ID) - post, err := s.buildRetrospectivePost(playbookRunToPublish, publisherUser, retrospectiveURL) - if err != nil { - return err - } - - if err = s.poster.Post(post); err != nil { - return errors.Wrap(err, "failed to post to channel") - } - - telemetryString := fmt.Sprintf("?telem_action=follower_clicked_retrospective_dm&telem_run_id=%s", playbookRunToPublish.ID) - retrospectivePublishedMessage := fmt.Sprintf("@%s published the retrospective report for [%s](%s%s).\n%s", publisherUser.Username, playbookRunToPublish.Name, retrospectiveURL, telemetryString, retrospective.Text) - err = s.dmPostToRunFollowers(&model.Post{Message: retrospectivePublishedMessage}, retroMessage, playbookRunToPublish.ID, publisherID) - if err != nil { - logger.WithError(err).Error("failed to dm post to run followers") - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: now, - EventAt: now, - EventType: PublishedRetrospective, - SubjectUserID: publisherID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID) - s.telemetry.PublishRetrospective(playbookRunToPublish, publisherID) - - return nil -} - -func (s *PlaybookRunServiceImpl) buildRetrospectivePost(playbookRunToPublish *PlaybookRun, publisherUser *model.User, retrospectiveURL string) (*model.Post, error) { - props := map[string]interface{}{ - "metricsData": "null", - "metricsConfigs": "null", - "retrospectiveText": playbookRunToPublish.Retrospective, - } - - // If run has metrics data, get playbooks metrics configs and include them in custom post - if len(playbookRunToPublish.MetricsData) > 0 { - playbook, err := s.playbookService.Get(playbookRunToPublish.PlaybookID) - if err != nil { - return nil, errors.Wrap(err, "failed to get playbook") - } - - metricsConfigs, err := json.Marshal(playbook.Metrics) - if err != nil { - return nil, errors.Wrap(err, "unable to marshal metrics configs") - } - - metricsData, err := json.Marshal(playbookRunToPublish.MetricsData) - if err != nil { - return nil, errors.Wrap(err, "cannot post retro, unable to marshal metrics data") - } - props["metricsData"] = string(metricsData) - props["metricsConfigs"] = string(metricsConfigs) - } - - return &model.Post{ - Message: fmt.Sprintf("@channel Retrospective for [%s](%s) has been published by @%s\n[See the full retrospective](%s)\n", playbookRunToPublish.Name, GetRunDetailsRelativeURL(playbookRunToPublish.ID), publisherUser.Username, retrospectiveURL), - Type: "custom_retro", - ChannelId: playbookRunToPublish.ChannelID, - Props: props, - }, nil -} - -func (s *PlaybookRunServiceImpl) CancelRetrospective(playbookRunID, cancelerID string) error { - playbookRunToCancel, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - now := model.GetMillis() - - // Update the text to keep syncronized - playbookRunToCancel.Retrospective = "No retrospective for this run." - playbookRunToCancel.RetrospectivePublishedAt = now - playbookRunToCancel.RetrospectiveWasCanceled = true - - playbookRunToCancel, err = s.store.UpdatePlaybookRun(playbookRunToCancel) - if err != nil { - return errors.Wrap(err, "failed to update playbook run") - } - - cancelerUser, err := s.api.GetUserByID(cancelerID) - if err != nil { - return errors.Wrap(err, "failed to get canceler user") - } - - if _, err = s.poster.PostMessage(playbookRunToCancel.ChannelID, "@channel Retrospective for [%s](%s) has been canceled by @%s\n", playbookRunToCancel.Name, GetRunDetailsRelativeURL(playbookRunID), cancelerUser.Username); err != nil { - return errors.Wrap(err, "failed to post to channel") - } - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: now, - EventAt: now, - EventType: CanceledRetrospective, - SubjectUserID: cancelerID, - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// RequestJoinChannel posts a channel-join request message in the run's channel -func (s *PlaybookRunServiceImpl) RequestJoinChannel(playbookRunID, requesterID string) error { - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - // avoid sending request if user is already a member of the channel - if s.api.HasPermissionToChannel(requesterID, playbookRun.ChannelID, model.PermissionReadChannel) { - return fmt.Errorf("user %s is already a member of the channel %s", requesterID, playbookRunID) - } - - requesterUser, err := s.api.GetUserByID(requesterID) - if err != nil { - return errors.Wrap(err, "failed to get requester user") - } - - T := i18n.GetUserTranslations(requesterUser.Locale) - data := map[string]interface{}{ - "Name": requesterUser.Username, - } - - _, err = s.poster.PostMessage(playbookRun.ChannelID, T("app.user.run.request_join_channel", data)) - if err != nil { - return errors.Wrap(err, "failed to post to channel") - } - return nil -} - -// RequestUpdate posts a status update request message in the run's channel -func (s *PlaybookRunServiceImpl) RequestUpdate(playbookRunID, requesterID string) error { - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - requesterUser, err := s.api.GetUserByID(requesterID) - if err != nil { - return errors.Wrap(err, "failed to get requester user") - } - - T := i18n.GetUserTranslations(requesterUser.Locale) - data := map[string]interface{}{ - "RunName": playbookRun.Name, - "RunURL": GetRunDetailsRelativeURL(playbookRunID), - "Name": requesterUser.Username, - } - - post, err := s.poster.PostMessage(playbookRun.ChannelID, T("app.user.run.request_update", data)) - if err != nil { - return errors.Wrap(err, "failed to post to channel") - } - - // create timeline event - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: post.CreateAt, - EventAt: post.CreateAt, - EventType: StatusUpdateRequested, - PostID: post.Id, - SubjectUserID: requesterID, - CreatorUserID: requesterID, - Summary: fmt.Sprintf("@%s requested a status update", requesterUser.Username), - } - - if _, err = s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - // send updated run through websocket - s.sendPlaybookRunUpdatedWS(playbookRunID) - - return nil -} - -// Leave removes user from the run's participants -func (s *PlaybookRunServiceImpl) RemoveParticipants(playbookRunID string, userIDs []string, requesterUserID string) error { - if len(userIDs) == 0 { - return nil - } - - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - - // Check if any user is the owner - for _, userID := range userIDs { - if playbookRun.OwnerUserID == userID { - return errors.New("owner user can't leave the run") - } - } - - if err = s.store.RemoveParticipants(playbookRunID, userIDs); err != nil { - return errors.Wrapf(err, "users `%+v` failed to remove participation in run `%s`", userIDs, playbookRunID) - } - - requesterUser, err := s.api.GetUserByID(requesterUserID) - if err != nil { - return errors.Wrap(err, "failed to get requester user") - } - - users := make([]*model.User, 0) - for _, userID := range userIDs { - user := requesterUser - if userID != requesterUserID { - user, err = s.api.GetUserByID(userID) - if err != nil { - return errors.Wrap(err, "failed to get user") - } - } - users = append(users, user) - s.leaveActions(playbookRun, userID) - } - - err = s.changeParticipantsTimeline(playbookRunID, requesterUser, users, "left") - if err != nil { - return err - } - - // ws send run - userIDs = append(userIDs, requesterUserID) - s.sendPlaybookRunUpdatedWS(playbookRunID, withAdditionalUserIDs(userIDs)) - - return nil -} - -func (s *PlaybookRunServiceImpl) leaveActions(playbookRun *PlaybookRun, userID string) { - - if !playbookRun.RemoveChannelMemberOnRemovedParticipant { - return - } - - // Don't do anything if the user not a channel member - member, _ := s.api.GetChannelMember(playbookRun.ChannelID, userID) - if member == nil { - return - } - - // To be added to the UI as an optional action - if err := s.api.DeleteChannelMember(playbookRun.ChannelID, userID); err != nil { - logrus.WithError(err).WithField("user_id", userID).Error("failed to remove user from linked channel") - } -} - -func (s *PlaybookRunServiceImpl) AddParticipants(playbookRunID string, userIDs []string, requesterUserID string, forceAddToChannel bool) error { - usersFailedToInvite := make([]string, 0) - usersToInvite := make([]string, 0) - - if len(userIDs) == 0 { - return nil - } - - playbookRun, err := s.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrapf(err, "failed to get run %s", playbookRunID) - } - - // Ensure new participants are team members - for _, userID := range userIDs { - var member *model.TeamMember - member, err = s.api.GetTeamMember(playbookRun.TeamID, userID) - if err != nil || member.DeleteAt != 0 { - usersFailedToInvite = append(usersFailedToInvite, userID) - continue - } - usersToInvite = append(usersToInvite, userID) - } - - if err = s.store.AddParticipants(playbookRun.ID, usersToInvite); err != nil { - return errors.Wrapf(err, "users `%+v` failed to participate the run `%s`", usersToInvite, playbookRun.ID) - } - - channel, err := s.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - logrus.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("failed to get channel") - } - - s.failedInvitedUserActions(usersFailedToInvite, channel) - - requesterUser, err := s.api.GetUserByID(requesterUserID) - if err != nil { - return errors.Wrap(err, "failed to get requester user") - } - - users := make([]*model.User, 0) - for _, userID := range usersToInvite { - user := requesterUser - if userID != requesterUserID { - user, err = s.api.GetUserByID(userID) - if err != nil { - return errors.Wrapf(err, "failed to get user %s", userID) - } - } - users = append(users, user) - - // Configured actions - s.participateActions(playbookRun, channel, user, requesterUser, forceAddToChannel) - - // Participate implies following the run - if err = s.Follow(playbookRunID, userID); err != nil { - return errors.Wrap(err, "failed to make participant to follow run") - } - } - - err = s.changeParticipantsTimeline(playbookRun.ID, requesterUser, users, "joined") - if err != nil { - return err - } - - // ws send run - if len(usersToInvite) > 0 { - s.sendPlaybookRunUpdatedWS(playbookRun.ID, withAdditionalUserIDs(usersToInvite), withAdditionalUserIDs([]string{requesterUserID})) - } - - return nil -} - -// changeParticipantsTimeline handles timeline event creation for run participation change triggers: -// participate/leave events and add/remove participants (multiple allowed) -func (s *PlaybookRunServiceImpl) changeParticipantsTimeline(playbookRunID string, requesterUser *model.User, users []*model.User, action string) error { - type Details struct { - Action string `json:"action,omitempty"` - Requester string `json:"requester,omitempty"` - Users []string `json:"users,omitempty"` - } - var details Details - if len(users) == 0 { - return nil - } - - now := model.GetMillis() - - event := &TimelineEvent{ - PlaybookRunID: playbookRunID, - CreateAt: now, - EventAt: now, - Summary: "", // copies managed in webapp using the injected data - CreatorUserID: requesterUser.Id, - SubjectUserID: requesterUser.Id, - } - - event.EventType = ParticipantsChanged - if len(users) == 1 && users[0].Id == requesterUser.Id { - event.EventType = UserJoinedLeft - } - if len(users) == 1 { - event.SubjectUserID = users[0].Id - } - - details.Action = action - details.Requester = requesterUser.Username - details.Users = make([]string, 0) - for _, u := range users { - details.Users = append(details.Users, u.Username) - } - detailsJSON, err := json.Marshal(details) - if err != nil { - return errors.Wrap(err, "failed to encode timeline event details") - } - event.Details = string(detailsJSON) - - if _, err := s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrap(err, "failed to create timeline event") - } - - return nil -} - -func (s *PlaybookRunServiceImpl) participateActions(playbookRun *PlaybookRun, channel *model.Channel, user *model.User, requesterUser *model.User, forceAddToChannel bool) { - - if !playbookRun.CreateChannelMemberOnNewParticipant && !forceAddToChannel { - return - } - - // Don't do anything if the user is a channel member - member, _ := s.api.GetChannelMember(playbookRun.ChannelID, user.Id) - if member != nil { - return - } - - // Add user to the channel - if _, err := s.api.AddChannelMember(playbookRun.ChannelID, user.Id); err != nil { - logrus.WithError(err).WithField("user_id", user.Id).Error("participateActions: failed to add user to linked channel") - } -} - -func (s *PlaybookRunServiceImpl) postMessageToThreadAndSaveRootID(playbookRunID, channelID string, post *model.Post) error { - channelIDsToRootIDs, err := s.store.GetBroadcastChannelIDsToRootIDs(playbookRunID) - if err != nil { - return errors.Wrapf(err, "error when trying to retrieve ChannelIDsToRootIDs map for playbookRunId '%s'", playbookRunID) - } - - err = s.poster.PostMessageToThread(channelIDsToRootIDs[channelID], post) - if err != nil { - return errors.Wrapf(err, "failed to PostMessageToThread for channelID '%s'", channelID) - } - - newRootID := post.RootId - if newRootID == "" { - newRootID = post.Id - } - - if newRootID != channelIDsToRootIDs[channelID] { - channelIDsToRootIDs[channelID] = newRootID - if err = s.store.SetBroadcastChannelIDsToRootID(playbookRunID, channelIDsToRootIDs); err != nil { - return errors.Wrapf(err, "failed to SetBroadcastChannelIDsToRootID for playbookID '%s'", playbookRunID) - } - } - - return nil -} - -// Follow method lets user follow a specific playbook run -func (s *PlaybookRunServiceImpl) Follow(playbookRunID, userID string) error { - if err := s.store.Follow(playbookRunID, userID); err != nil { - return errors.Wrapf(err, "user `%s` failed to follow the run `%s`", userID, playbookRunID) - } - - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - s.telemetry.Follow(playbookRun, userID) - s.sendPlaybookRunUpdatedWS(playbookRunID, withAdditionalUserIDs([]string{userID})) - - return nil -} - -// UnFollow method lets user unfollow a specific playbook run -func (s *PlaybookRunServiceImpl) Unfollow(playbookRunID, userID string) error { - if err := s.store.Unfollow(playbookRunID, userID); err != nil { - return errors.Wrapf(err, "user `%s` failed to unfollow the run `%s`", userID, playbookRunID) - } - - playbookRun, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - s.telemetry.Unfollow(playbookRun, userID) - - s.sendPlaybookRunUpdatedWS(playbookRunID, withAdditionalUserIDs([]string{userID})) - - return nil -} - -// GetFollowers returns list of followers for a specific playbook run -func (s *PlaybookRunServiceImpl) GetFollowers(playbookRunID string) ([]string, error) { - var followers []string - var err error - if followers, err = s.store.GetFollowers(playbookRunID); err != nil { - return nil, errors.Wrapf(err, "failed to get followers for the run `%s`", playbookRunID) - } - - return followers, nil -} - -func getUserDisplayName(user *model.User) string { - if user == nil { - return "" - } - - if user.FirstName != "" && user.LastName != "" { - return fmt.Sprintf("%s %s", user.FirstName, user.LastName) - } - - return fmt.Sprintf("@%s", user.Username) -} - -func cleanChannelName(channelName string) string { - // Lower case only - channelName = strings.ToLower(channelName) - // Trim spaces - channelName = strings.TrimSpace(channelName) - // Change all dashes to whitespace, remove everything that's not a word or whitespace, all space becomes dashes - channelName = strings.ReplaceAll(channelName, "-", " ") - channelName = allNonSpaceNonWordRegex.ReplaceAllString(channelName, "") - channelName = strings.ReplaceAll(channelName, " ", "-") - // Remove all leading and trailing dashes - channelName = strings.Trim(channelName, "-") - - return channelName -} - -func addRandomBits(name string) string { - // Fix too long names (we're adding 5 chars): - if len(name) > 59 { - name = name[:59] - } - randBits := model.NewId() - return fmt.Sprintf("%s-%s", name, randBits[:4]) -} - -func findNewestNonDeletedStatusPost(posts []StatusPost) *StatusPost { - var newest *StatusPost - for i, p := range posts { - if p.DeleteAt == 0 && (newest == nil || p.CreateAt > newest.CreateAt) { - newest = &posts[i] - } - } - return newest -} - -func findNewestNonDeletedPostID(posts []StatusPost) string { - newest := findNewestNonDeletedStatusPost(posts) - if newest == nil { - return "" - } - - return newest.ID -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -// Helper function to Trigger webhooks -func triggerWebhooks(s *PlaybookRunServiceImpl, webhooks []string, body []byte) { - for i := range webhooks { - url := webhooks[i] - - go func() { - req, err := http.NewRequest("POST", url, bytes.NewReader(body)) - - if err != nil { - logrus.WithError(err).WithField("webhook_url", url).Error("failed to create a POST request to webhook URL") - return - } - - req.Header.Set("Content-Type", "application/json") - - resp, err := s.httpClient.Do(req) - if err != nil { - logrus.WithError(err).WithField("webhook_url", url).Warn("failed to send a POST request to webhook URL") - return - } - - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - err := errors.Errorf("response code is %d; expected a status code in the 2xx range", resp.StatusCode) - logrus.WithError(err).WithField("webhook_url", url).Warn("failed to finish a POST request to webhook URL") - } - }() - } - -} - -func buildAssignedTaskMessageSummary(runs []AssignedRun, locale string, timezone *time.Location, onlyTasksDueUntilToday bool) string { - var msg strings.Builder - - T := i18n.GetUserTranslations(locale) - total := 0 - for _, run := range runs { - total += len(run.Tasks) - } - - msg.WriteString("##### ") - msg.WriteString(T("app.user.digest.tasks.heading")) - msg.WriteString("\n") - - if total == 0 { - msg.WriteString(T("app.user.digest.tasks.zero_assigned")) - msg.WriteString("\n") - return msg.String() - } - - var tasksNoDueDate, tasksDoAfterToday int - currentTime := timeutils.GetTimeForMillis(model.GetMillis()).In(timezone) - yesterday := currentTime.Add(-24 * time.Hour) - - var runsInfo strings.Builder - for _, run := range runs { - var tasksInfo strings.Builder - - for _, task := range run.Tasks { - // no due date - if task.ChecklistItem.DueDate == 0 { - // add information about tasks without due date only if the full list was requested - if !onlyTasksDueUntilToday { - tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s\n", task.ChecklistTitle, task.Title)) - } - tasksNoDueDate++ - continue - } - dueTime := time.Unix(task.ChecklistItem.DueDate/1000, 0).In(timezone) - // due today - if timeutils.IsSameDay(dueTime, currentTime) { - tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s **`%s`**\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_today"))) - continue - } - // due yesterday - if timeutils.IsSameDay(dueTime, yesterday) { - tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s **`%s`**\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_yesterday"))) - continue - } - // due before yesterday - if dueTime.Before(currentTime) { - days := timeutils.GetDaysDiff(dueTime, currentTime) - tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s **`%s`**\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_x_days_ago", days))) - continue - } - // due after today - if !onlyTasksDueUntilToday { - days := timeutils.GetDaysDiff(currentTime, dueTime) - tasksInfo.WriteString(fmt.Sprintf(" - [ ] %s: %s `%s`\n", task.ChecklistTitle, task.Title, T("app.user.digest.tasks.due_in_x_days", days))) - } - tasksDoAfterToday++ - } - - // omit run's title if tasks info is empty - if tasksInfo.String() != "" { - runsInfo.WriteString(fmt.Sprintf("[%s](%s?from=digest_assignedtask)\n", run.Name, GetRunDetailsRelativeURL(run.PlaybookRunID))) - runsInfo.WriteString(tasksInfo.String()) - } - } - - // if we need tasks due now and there are only tasks that are due after today or without due date, skip a message - if onlyTasksDueUntilToday && tasksDoAfterToday+tasksNoDueDate == total { - return "" - } - - // add title - if onlyTasksDueUntilToday { - msg.WriteString(T("app.user.digest.tasks.num_assigned_due_until_today", total-tasksDoAfterToday)) - } else { - msg.WriteString(T("app.user.digest.tasks.num_assigned", total)) - } - - // add info about tasks - msg.WriteString("\n\n") - msg.WriteString(runsInfo.String()) - - // add summary info for tasks without a due date or due date after today - if tasksDoAfterToday > 0 && onlyTasksDueUntilToday { - msg.WriteString(":information_source: ") - msg.WriteString(T("app.user.digest.tasks.due_after_today", tasksDoAfterToday)) - msg.WriteString(" ") - msg.WriteString(T("app.user.digest.tasks.all_tasks_command")) - } - return msg.String() -} - -func buildRunsInProgressMessage(runs []RunLink, locale string) string { - T := i18n.GetUserTranslations(locale) - total := len(runs) - - msg := "\n" - - msg += "##### " + T("app.user.digest.runs_in_progress.heading") + "\n" - if total == 0 { - return msg + T("app.user.digest.runs_in_progress.zero_in_progress") + "\n" - } - - msg += T("app.user.digest.runs_in_progress.num_in_progress", total) + "\n" - - for _, run := range runs { - msg += fmt.Sprintf("- [%s](%s?from=digest_runsinprogress)\n", run.Name, GetRunDetailsRelativeURL(run.PlaybookRunID)) - } - - return msg -} - -func buildRunsOverdueMessage(runs []RunLink, locale string) string { - T := i18n.GetUserTranslations(locale) - total := len(runs) - msg := "\n" - msg += "##### " + T("app.user.digest.overdue_status_updates.heading") + "\n" - if total == 0 { - return msg + T("app.user.digest.overdue_status_updates.zero_overdue") + "\n" - } - - msg += T("app.user.digest.overdue_status_updates.num_overdue", total) + "\n" - - for _, run := range runs { - msg += fmt.Sprintf("- [%s](%s?from=digest_overduestatus)\n", run.Name, GetRunDetailsRelativeURL(run.PlaybookRunID)) - } - - return msg -} - -type messageType string - -const ( - creationMessage messageType = "creation" - finishMessage messageType = "finish" - overdueStatusUpdateMessage messageType = "overdue status update" - restoreMessage messageType = "restore" - retroMessage messageType = "retrospective" - statusUpdateMessage messageType = "status update" -) - -// broadcasting to channels -func (s *PlaybookRunServiceImpl) broadcastPlaybookRunMessageToChannels(channelIDs []string, post *model.Post, mType messageType, playbookRun *PlaybookRun, logger logrus.FieldLogger) { - logger = logger.WithField("message_type", mType) - - for _, broadcastChannelID := range channelIDs { - post.Id = "" // Reset the ID so we avoid cloning the whole object - if err := s.broadcastPlaybookRunMessage(broadcastChannelID, post, mType, playbookRun); err != nil { - logger.WithError(err).Error("failed to broadcast run to channel") - - if _, err = s.poster.PostMessage(playbookRun.ChannelID, fmt.Sprintf("Failed to broadcast run %s to the configured channel.", mType)); err != nil { - logger.WithError(err).WithField("channel_id", playbookRun.ChannelID).Error("failed to post failure message to the channel") - } - } - } -} - -func (s *PlaybookRunServiceImpl) broadcastPlaybookRunMessage(broadcastChannelID string, post *model.Post, mType messageType, playbookRun *PlaybookRun) error { - post.ChannelId = broadcastChannelID - if err := IsChannelActiveInTeam(post.ChannelId, playbookRun.TeamID, s.api); err != nil { - return errors.Wrap(err, "announcement channel is not active") - } - - if err := s.postMessageToThreadAndSaveRootID(playbookRun.ID, post.ChannelId, post); err != nil { - return errors.Wrapf(err, "error posting '%s' message, for playbook '%s', to channelID '%s'", mType, playbookRun.ID, post.ChannelId) - } - - return nil -} - -// dm to users who follow - -func (s *PlaybookRunServiceImpl) dmPostToRunFollowers(post *model.Post, mType messageType, playbookRunID, authorID string) error { - followers, err := s.GetFollowers(playbookRunID) - if err != nil { - return errors.Wrap(err, "failed to get followers") - } - - s.dmPostToUsersWithPermission(followers, post, playbookRunID, authorID) - return nil -} - -func (s *PlaybookRunServiceImpl) dmPostToAutoFollows(post *model.Post, playbookID, playbookRunID, authorID string) error { - autoFollows, err := s.playbookService.GetAutoFollows(playbookID) - if err != nil { - return errors.Wrap(err, "failed to get auto follows") - } - - s.dmPostToUsersWithPermission(autoFollows, post, playbookRunID, authorID) - return nil -} - -func (s *PlaybookRunServiceImpl) dmPostToUsersWithPermission(users []string, post *model.Post, playbookRunID, authorID string) { - logger := logrus.WithFields(logrus.Fields{"playbook_run_id": playbookRunID}) - - for _, user := range users { - // Do not send update to the author - if user == authorID { - continue - } - - // Check for access permissions - if err := s.permissions.RunView(user, playbookRunID); err != nil { - continue - } - - post.Id = "" // Reset the ID so we avoid cloning the whole object - post.RootId = "" - if err := s.poster.DM(user, post); err != nil { - logger.WithError(err).WithField("user_id", user).Warn("failed to broadcast post to the user") - } - } -} - -func (s *PlaybookRunServiceImpl) MessageHasBeenPosted(post *model.Post) { - runIDs, err := s.store.GetPlaybookRunIDsForChannel(post.ChannelId) - if err != nil { - if errors.Is(err, ErrNotFound) { - return - } - logrus.WithError(err).WithFields(logrus.Fields{ - "post_id": post.Id, - "channel_id": post.ChannelId, - }).Error("unable retrieve run ID from post") - return - } - - for _, runID := range runIDs { - // Get run - run, err := s.GetPlaybookRun(runID) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "run_id": runID, - }).Error("unable retrieve run from ID") - return - } - - for checklistNum, checklist := range run.Checklists { - for itemNum, item := range checklist.Items { - for _, ta := range item.TaskActions { - if ta.Trigger.Type == KeywordsByUsersTriggerType { - t, err := NewKeywordsByUsersTrigger(ta.Trigger) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "type": ta.Trigger.Type, - "checklistNum": checklistNum, - "itemNum": itemNum, - }).Error("unable to decode trigger") - return - } - if t.IsTriggered(post) { - s.genericTelemetry.Track( - telemetryTaskActionsTriggered, - map[string]any{ - "trigger": ta.Trigger.Type, - "playbookrun_id": runID, - }, - ) - err := s.doActions(ta.Actions, runID, post.UserId, ChecklistItemStateClosed, checklistNum, itemNum) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "checklistNum": checklistNum, - "itemNum": itemNum, - }).Error("can't process task actions") - return - } - } - } - } - } - } - } -} - -func (s *PlaybookRunServiceImpl) doActions(taskActions []Action, runID string, userID string, ChecklistItemStateClosed string, checklistNum int, itemNum int) error { - for _, action := range taskActions { - if action.Type == MarkItemAsDoneActionType { - a, err := NewMarkItemAsDoneAction(action) - if err != nil { - return errors.Wrapf(err, "unable to decode action") - } - if a.Payload.Enabled { - if err := s.ModifyCheckedState(runID, userID, ChecklistItemStateClosed, checklistNum, itemNum); err != nil { - return errors.Wrapf(err, "can't mark item as done") - } - } - } - s.genericTelemetry.Track( - telemetryTaskActionsActionExecuted, - map[string]any{ - "action": action.Type, - "playbookrun_id": runID, - }, - ) - } - return nil -} - -// GetPlaybookRunIDsForUser returns run ids where user is a participant or is following -func (s *PlaybookRunServiceImpl) GetPlaybookRunIDsForUser(userID string) ([]string, error) { - return s.store.GetPlaybookRunIDsForUser(userID) -} - -// GetRunMetadataByIDs returns playbook runs metadata by passed run IDs. -func (s *PlaybookRunServiceImpl) GetRunMetadataByIDs(runIDs []string) ([]RunMetadata, error) { - return s.store.GetRunMetadataByIDs(runIDs) -} - -// GetTaskMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs -func (s *PlaybookRunServiceImpl) GetTaskMetadataByIDs(taskIDs []string) ([]TopicMetadata, error) { - return s.store.GetTaskAsTopicMetadataByIDs(taskIDs) -} - -// GetStatusMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs -func (s *PlaybookRunServiceImpl) GetStatusMetadataByIDs(statusIDs []string) ([]TopicMetadata, error) { - return s.store.GetStatusAsTopicMetadataByIDs(statusIDs) -} diff --git a/server/playbooks/server/app/playbook_run_test.go b/server/playbooks/server/app/playbook_run_test.go deleted file mode 100644 index a4b2b5ddaf4..00000000000 --- a/server/playbooks/server/app/playbook_run_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/stretchr/testify/require" -) - -func TestPlaybookRun_MarshalJSON(t *testing.T) { - t.Run("marshal pointer", func(t *testing.T) { - testPlaybookRun := &PlaybookRun{} - result, err := json.Marshal(testPlaybookRun) - require.NoError(t, err) - require.NotContains(t, string(result), "null", "update MarshalJSON to initialize nil slices") - }) - - t.Run("marshal value", func(t *testing.T) { - testPlaybookRun := PlaybookRun{} - result, err := json.Marshal(testPlaybookRun) - require.NoError(t, err) - require.NotContains(t, string(result), "null", "update MarshalJSON to initialize nil slices") - }) -} - -func TestPlaybookRunFilterOptions_Clone(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: "team_id", - Page: 1, - PerPage: 10, - Sort: SortByID, - Direction: DirectionAsc, - Statuses: []string{"InProgress", "Finished"}, - OwnerID: "owner_id", - ParticipantID: "participant_id", - SearchTerm: "search_term", - PlaybookID: "playbook_id", - } - marshalledOptions, err := json.Marshal(options) - require.NoError(t, err) - - clone := options.Clone() - clone.TeamID = "team_id_clone" - clone.Page = 2 - clone.PerPage = 20 - clone.Sort = SortByName - clone.Direction = DirectionDesc - clone.Statuses[0] = "Finished" - clone.OwnerID = "owner_id_clone" - clone.ParticipantID = "participant_id_clone" - clone.SearchTerm = "search_term_clone" - clone.PlaybookID = "playbook_id_clone" - - var unmarshalledOptions PlaybookRunFilterOptions - err = json.Unmarshal(marshalledOptions, &unmarshalledOptions) - require.NoError(t, err) - require.Equal(t, options, unmarshalledOptions) - require.NotEqual(t, clone, unmarshalledOptions) -} - -func TestPlaybookRunFilterOptions_Validate(t *testing.T) { - t.Run("non-positive PerPage", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - PerPage: -1, - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options.TeamID, validOptions.TeamID) - require.Equal(t, PerPageDefault, validOptions.PerPage) - }) - - t.Run("invalid sort option", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - Sort: SortField("invalid"), - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("valid, but wrong case sort option", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - Sort: SortField("END_at"), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options.TeamID, validOptions.TeamID) - require.Equal(t, SortByEndAt, validOptions.Sort) - }) - - t.Run("valid, no explicit sort option", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options.TeamID, validOptions.TeamID) - require.Equal(t, SortByCreateAt, validOptions.Sort) - }) - - t.Run("invalid sort direction", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - Direction: SortDirection("invalid"), - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("valid, but wrong case direction option", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - Direction: SortDirection("DEsC"), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options.TeamID, validOptions.TeamID) - require.Equal(t, DirectionDesc, validOptions.Direction) - }) - - t.Run("valid, no explicit direction", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options.TeamID, validOptions.TeamID) - require.Equal(t, DirectionAsc, validOptions.Direction) - }) - - t.Run("invalid team id", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: "invalid", - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("invalid owner id", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - OwnerID: "invalid", - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("invalid participant id", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - ParticipantID: "invalid", - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("invalid playbook id", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - PlaybookID: "invalid", - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("invalid statuses", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - Page: 1, - PerPage: 10, - Sort: SortByID, - Direction: DirectionAsc, - Statuses: []string{"active", "Finished"}, - OwnerID: model.NewId(), - ParticipantID: model.NewId(), - SearchTerm: "search_term", - PlaybookID: model.NewId(), - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("valid status", func(t *testing.T) { - options := PlaybookRunFilterOptions{ - TeamID: model.NewId(), - Page: 1, - PerPage: 10, - Sort: SortByID, - Direction: DirectionAsc, - Statuses: []string{"InProgress", "Finished"}, - OwnerID: model.NewId(), - ParticipantID: model.NewId(), - SearchTerm: "search_term", - PlaybookID: model.NewId(), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options, validOptions) - }) -} diff --git a/server/playbooks/server/app/playbook_service.go b/server/playbooks/server/app/playbook_service.go deleted file mode 100644 index 551ea1c2018..00000000000 --- a/server/playbooks/server/app/playbook_service.go +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/metrics" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -const ( - playbookCreatedWSEvent = "playbook_created" - playbookArchivedWSEvent = "playbook_archived" - playbookRestoredWSEvent = "playbook_restored" -) - -type playbookService struct { - store PlaybookStore - poster bot.Poster - telemetry PlaybookTelemetry - api playbooks.ServicesAPI - metricsService *metrics.Metrics -} - -// NewPlaybookService returns a new playbook service -func NewPlaybookService(store PlaybookStore, poster bot.Poster, telemetry PlaybookTelemetry, api playbooks.ServicesAPI, metricsService *metrics.Metrics) PlaybookService { - return &playbookService{ - store: store, - poster: poster, - telemetry: telemetry, - api: api, - metricsService: metricsService, - } -} - -func (s *playbookService) Create(playbook Playbook, userID string) (string, error) { - playbook.CreateAt = model.GetMillis() - playbook.UpdateAt = playbook.CreateAt - - newID, err := s.store.Create(playbook) - if err != nil { - return "", err - } - playbook.ID = newID - - s.telemetry.CreatePlaybook(playbook, userID) - - s.poster.PublishWebsocketEventToTeam(playbookCreatedWSEvent, map[string]interface{}{ - "teamID": playbook.TeamID, - }, playbook.TeamID) - - s.metricsService.IncrementPlaybookCreatedCount(1) - return newID, nil -} - -func (s *playbookService) Import(playbook Playbook, userID string) (string, error) { - newID, err := s.Create(playbook, userID) - if err != nil { - return "", err - } - playbook.ID = newID - s.telemetry.ImportPlaybook(playbook, userID) - return newID, nil -} - -func (s *playbookService) Get(id string) (Playbook, error) { - return s.store.Get(id) -} - -func (s *playbookService) GetPlaybooks() ([]Playbook, error) { - return s.store.GetPlaybooks() -} - -func (s *playbookService) GetPlaybooksForTeam(requesterInfo RequesterInfo, teamID string, opts PlaybookFilterOptions) (GetPlaybooksResults, error) { - return s.store.GetPlaybooksForTeam(requesterInfo, teamID, opts) -} - -func (s *playbookService) Update(playbook Playbook, userID string) error { - if playbook.DeleteAt != 0 { - return errors.New("cannot update a playbook that is archived") - } - - playbook.UpdateAt = model.GetMillis() - - if err := s.store.Update(playbook); err != nil { - return err - } - - s.telemetry.UpdatePlaybook(playbook, userID) - - return nil -} - -func (s *playbookService) Archive(playbook Playbook, userID string) error { - if playbook.ID == "" { - return errors.New("can't archive a playbook without an ID") - } - - if err := s.store.Archive(playbook.ID); err != nil { - return err - } - - s.telemetry.DeletePlaybook(playbook, userID) - s.metricsService.IncrementPlaybookArchivedCount(1) - - s.poster.PublishWebsocketEventToTeam(playbookArchivedWSEvent, map[string]interface{}{ - "teamID": playbook.TeamID, - }, playbook.TeamID) - - return nil -} - -func (s *playbookService) Restore(playbook Playbook, userID string) error { - if playbook.ID == "" { - return errors.New("can't restore a playbook without an ID") - } - - if playbook.DeleteAt == 0 { - return nil - } - - if err := s.store.Restore(playbook.ID); err != nil { - return err - } - - s.telemetry.RestorePlaybook(playbook, userID) - s.metricsService.IncrementPlaybookRestoredCount(1) - - s.poster.PublishWebsocketEventToTeam(playbookRestoredWSEvent, map[string]interface{}{ - "teamID": playbook.TeamID, - }, playbook.TeamID) - - return nil -} - -// AutoFollow method lets user to auto-follow all runs of a specific playbook -func (s *playbookService) AutoFollow(playbookID, userID string) error { - if err := s.store.AutoFollow(playbookID, userID); err != nil { - return errors.Wrapf(err, "user `%s` failed to auto-follow the playbook `%s`", userID, playbookID) - } - - playbook, err := s.store.Get(playbookID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - s.telemetry.AutoFollowPlaybook(playbook, userID) - return nil -} - -// AutoUnfollow method lets user to not auto-follow the newly created playbook runs -func (s *playbookService) AutoUnfollow(playbookID, userID string) error { - if err := s.store.AutoUnfollow(playbookID, userID); err != nil { - return errors.Wrapf(err, "user `%s` failed to auto-unfollow the playbook `%s`", userID, playbookID) - } - - playbook, err := s.store.Get(playbookID) - if err != nil { - return errors.Wrap(err, "failed to retrieve playbook run") - } - s.telemetry.AutoUnfollowPlaybook(playbook, userID) - return nil -} - -// GetAutoFollows returns list of users who auto-follow a playbook -func (s *playbookService) GetAutoFollows(playbookID string) ([]string, error) { - autoFollows, err := s.store.GetAutoFollows(playbookID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get auto-follows for the playbook `%s`", playbookID) - } - - return autoFollows, nil -} - -// Duplicate duplicates a playbook -func (s *playbookService) Duplicate(playbook Playbook, userID string) (string, error) { - logger := logrus.WithFields(logrus.Fields{ - "original_playbook_id": playbook.ID, - "user_id": userID, - }) - - newPlaybook := playbook.Clone() - newPlaybook.ID = "" - // Empty metric IDs if there are such. Otherwise, metrics will not be saved in the database. - for i := range newPlaybook.Metrics { - newPlaybook.Metrics[i].ID = "" - } - newPlaybook.Title = "Copy of " + playbook.Title - - // On duplicating, make the current user the administrator. - newPlaybook.Members = []PlaybookMember{{ - UserID: userID, - Roles: []string{PlaybookRoleMember, PlaybookRoleAdmin}, - }} - - playbookID, err := s.Create(newPlaybook, userID) - if err != nil { - return "", err - } - - logger.WithField("playbook_id", playbookID).Debug("Duplicated playbook") - return playbookID, nil -} - -// get top playbooks for teams -func (s *playbookService) GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) { - permissionFlag, err := licenseAndGuestCheck(s, userID, false) - if err != nil { - return nil, err - } - if !permissionFlag { - return nil, errors.New("User cannot access playbooks insights") - } - - return s.store.GetTopPlaybooksForTeam(teamID, userID, opts) -} - -// get top playbooks for users -func (s *playbookService) GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*PlaybooksInsightsList, error) { - permissionFlag, err := licenseAndGuestCheck(s, userID, true) - if err != nil { - return nil, err - } - if !permissionFlag { - return nil, errors.New("User cannot access playbooks insights") - } - - return s.store.GetTopPlaybooksForUser(teamID, userID, opts) -} - -func licenseAndGuestCheck(s *playbookService, userID string, isMyInsights bool) (bool, error) { - licenseError := errors.New("invalid license/authorization to use insights API") - guestError := errors.New("Guests aren't authorized to use insights API") - lic := s.api.GetLicense() - - user, err := s.api.GetUserByID(userID) - if err != nil { - return false, err - } - - if user.IsGuest() { - return false, guestError - } - - if lic == nil && !isMyInsights { - return false, licenseError - } - - if !isMyInsights && (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) { - return false, licenseError - } - - return true, nil -} diff --git a/server/playbooks/server/app/playbook_test.go b/server/playbooks/server/app/playbook_test.go deleted file mode 100644 index c8d79650fed..00000000000 --- a/server/playbooks/server/app/playbook_test.go +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestPlaybook_MarshalJSON(t *testing.T) { - tests := []struct { - name string - original Playbook - expected []byte - wantErr bool - }{ - { - name: "marshals a struct with nil slices into empty arrays", - original: Playbook{ - ID: "playbookid", - Title: "the playbook title", - Description: "the playbook's description", - TeamID: "theteamid", - CreatePublicPlaybookRun: true, - CreateAt: 4503134, - DeleteAt: 0, - NumStages: 0, - NumSteps: 0, - Checklists: nil, - Members: nil, - BroadcastChannelIDs: []string{"channelid"}, - ReminderMessageTemplate: "This is a message", - ReminderTimerDefaultSeconds: 0, - InvitedUserIDs: nil, - InvitedGroupIDs: nil, - }, - expected: []byte(`"checklists":[]`), - wantErr: false, - }, - { - name: "marshals a struct with nil []checklistItems into an empty array", - original: Playbook{ - ID: "playbookid", - Title: "the playbook title", - Description: "the playbook's description", - TeamID: "theteamid", - CreatePublicPlaybookRun: true, - CreateAt: 4503134, - DeleteAt: 0, - NumStages: 0, - NumSteps: 0, - Checklists: []Checklist{ - { - ID: "checklist1", - Title: "checklist 1", - Items: nil, - }, - }, - BroadcastChannelIDs: []string{}, - ReminderMessageTemplate: "This is a message", - ReminderTimerDefaultSeconds: 0, - InvitedUserIDs: nil, - InvitedGroupIDs: nil, - WebhookOnStatusUpdateURLs: []string{"testurl"}, - WebhookOnStatusUpdateEnabled: true, - }, - expected: []byte(`"checklists":[{"id":"checklist1","title":"checklist 1","items":[]}]`), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := json.Marshal(tt.original) - if (err != nil) != tt.wantErr { - t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr) - return - } - require.Contains(t, string(got), string(tt.expected)) - }) - } -} - -func TestPlaybookFilterOptions_Clone(t *testing.T) { - options := PlaybookFilterOptions{ - Page: 1, - PerPage: 10, - Sort: SortByID, - Direction: DirectionAsc, - } - marshalledOptions, err := json.Marshal(options) - require.NoError(t, err) - - clone := options.Clone() - clone.Page = 2 - clone.PerPage = 20 - clone.Sort = SortByName - clone.Direction = DirectionDesc - - var unmarshalledOptions PlaybookFilterOptions - err = json.Unmarshal(marshalledOptions, &unmarshalledOptions) - require.NoError(t, err) - require.Equal(t, options, unmarshalledOptions) - require.NotEqual(t, clone, unmarshalledOptions) -} - -func TestPlaybookFilterOptions_Validate(t *testing.T) { - t.Run("non-positive PerPage", func(t *testing.T) { - options := PlaybookFilterOptions{ - PerPage: -1, - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, PerPageDefault, validOptions.PerPage) - }) - - t.Run("invalid sort option", func(t *testing.T) { - options := PlaybookFilterOptions{ - Sort: SortField("invalid"), - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("valid, but wrong case sort option", func(t *testing.T) { - options := PlaybookFilterOptions{ - Sort: SortField("STAges"), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, SortByStages, validOptions.Sort) - }) - - t.Run("valid, no explicit sort option", func(t *testing.T) { - options := PlaybookFilterOptions{} - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, SortByID, validOptions.Sort) - }) - - t.Run("invalid sort direction", func(t *testing.T) { - options := PlaybookFilterOptions{ - Direction: SortDirection("invalid"), - } - - _, err := options.Validate() - require.Error(t, err) - }) - - t.Run("valid, but wrong case direction option", func(t *testing.T) { - options := PlaybookFilterOptions{ - Direction: SortDirection("DEsC"), - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, DirectionDesc, validOptions.Direction) - }) - - t.Run("valid, no explicit direction", func(t *testing.T) { - options := PlaybookFilterOptions{} - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, DirectionAsc, validOptions.Direction) - }) - - t.Run("valid", func(t *testing.T) { - options := PlaybookFilterOptions{ - Page: 1, - PerPage: 10, - Sort: SortByTitle, - Direction: DirectionAsc, - } - - validOptions, err := options.Validate() - require.NoError(t, err) - require.Equal(t, options, validOptions) - }) -} diff --git a/server/playbooks/server/app/plugin_api_tools.go b/server/playbooks/server/app/plugin_api_tools.go deleted file mode 100644 index d196d3c1093..00000000000 --- a/server/playbooks/server/app/plugin_api_tools.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/pkg/errors" -) - -var ( - ErrChannelNotFound = errors.Errorf("channel not found") - ErrChannelDeleted = errors.Errorf("channel deleted") - ErrChannelNotInExpectedTeam = errors.Errorf("channel in different team") -) - -func IsChannelActiveInTeam(channelID string, expectedTeamID string, api playbooks.ServicesAPI) error { - channel, err := api.GetChannelByID(channelID) - if err != nil { - return errors.Wrapf(ErrChannelNotFound, "channel with ID %s does not exist", channelID) - } - - if channel.DeleteAt != 0 { - return errors.Wrapf(ErrChannelDeleted, "channel with ID %s is archived", channelID) - } - - if channel.TeamId != expectedTeamID { - return errors.Wrapf(ErrChannelNotInExpectedTeam, - "channel with ID %s is on team with ID %s; expected team ID is %s", - channelID, - channel.TeamId, - expectedTeamID, - ) - } - - return nil -} diff --git a/server/playbooks/server/app/regular_digest_service.go b/server/playbooks/server/app/regular_digest_service.go deleted file mode 100644 index db0a51a4ff9..00000000000 --- a/server/playbooks/server/app/regular_digest_service.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "time" -) - -func ShouldSendWeeklyDigestMessage(userInfo UserInfo, timezone *time.Location, currentTime time.Time) bool { - if userInfo.DigestNotificationSettings.DisableWeeklyDigest { - return false - } - - lastSentTime := time.UnixMilli(userInfo.LastDailyTodoDMAt).In(timezone) - - currentYear, currentWeek := currentTime.ISOWeek() - lastSentYear, lastSentWeek := lastSentTime.ISOWeek() - isFirstLoginOfTheWeek := currentYear != lastSentYear || currentWeek != lastSentWeek - - return isFirstLoginOfTheWeek -} - -func ShouldSendDailyDigestMessage(userInfo UserInfo, timezone *time.Location, currentTime time.Time) bool { - if userInfo.DigestNotificationSettings.DisableDailyDigest { - return false - } - // DM message if it's the next day and been more than an hour since the last post - // Hat tip to Github plugin for the logic. - lastSentTime := time.UnixMilli(userInfo.LastDailyTodoDMAt).In(timezone) - - isMoreThanOneHourPassed := currentTime.Sub(lastSentTime).Hours() >= 1 - - isDifferentDay := currentTime.Day() != lastSentTime.Day() || - currentTime.Month() != lastSentTime.Month() || - currentTime.Year() != lastSentTime.Year() - - return isMoreThanOneHourPassed && isDifferentDay -} diff --git a/server/playbooks/server/app/regular_digest_service_test.go b/server/playbooks/server/app/regular_digest_service_test.go deleted file mode 100644 index fe9ae4a9963..00000000000 --- a/server/playbooks/server/app/regular_digest_service_test.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - "time" -) - -func TestShouldSendWeeklyDigestMessage(t *testing.T) { - now, ok := time.Parse("2006-01-02", "2022-10-08") - if ok != nil { - t.Error("Could not parse current time") - } - - type args struct { - userInfo UserInfo - timezone *time.Location - currentTime time.Time - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "Should not send a weekly digest if the user has configured it so", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: now.AddDate(0, 0, -6).UnixMilli(), - DigestNotificationSettings: DigestNotificationSettings{ - DisableWeeklyDigest: true, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: now, - }, - want: false, - }, - { - name: "Should not send a weekly digest if we have already sent a digest this week", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: now.AddDate(0, 0, -1).UnixMilli(), - DigestNotificationSettings: DigestNotificationSettings{ - DisableDailyDigest: false, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: now, - }, - want: false, - }, - { - name: "Should send a weekly digest if we have not sent a digest this week", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: now.AddDate(0, 0, -6).UnixMilli(), - DigestNotificationSettings: DigestNotificationSettings{ - DisableDailyDigest: false, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: now, - }, - want: true, - }, - { - name: "Should send a weekly digest if we have not sent a digest ever", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: 0, - DigestNotificationSettings: DigestNotificationSettings{ - DisableDailyDigest: false, - DisableWeeklyDigest: false, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: now, - }, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ShouldSendWeeklyDigestMessage(tt.args.userInfo, tt.args.timezone, tt.args.currentTime); got != tt.want { - t.Errorf("ShouldSendWeeklyDigestMessage() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestShouldSendDailyDigestMessage(t *testing.T) { - now, ok := time.Parse("Jan 2, 2006 at 3:04pm", "Oct 8, 2022 at 3:04pm") - lateNow, lateOk := time.Parse("Jan 2, 2006 at 3:04pm", "Oct 8, 2022 at 12:10am") - if ok != nil || lateOk != nil { - t.Error("Could not parse current time") - } - - type args struct { - userInfo UserInfo - timezone *time.Location - currentTime time.Time - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "Should not send a daily digest if we have already sent a digest today", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: now.Add(-((time.Hour * 1) + (time.Minute * 2))).UnixMilli(), - DigestNotificationSettings: DigestNotificationSettings{ - DisableDailyDigest: false, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: now, - }, - want: false, - }, - { - name: "Should send a daily digest if we have not sent a digest today", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: now.Add(-(time.Hour * 25)).UnixMilli(), - DigestNotificationSettings: DigestNotificationSettings{ - DisableDailyDigest: false, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: now, - }, - want: true, - }, - { - name: "Should not send a daily digest if we have sent one within the last hour", - args: args{ - userInfo: UserInfo{ - ID: "testUser", - LastDailyTodoDMAt: lateNow.Add(-(time.Minute * 40)).UnixMilli(), - DigestNotificationSettings: DigestNotificationSettings{ - DisableDailyDigest: false, - }, - }, - timezone: time.FixedZone("local", 0), - currentTime: lateNow, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ShouldSendDailyDigestMessage(tt.args.userInfo, tt.args.timezone, tt.args.currentTime); got != tt.want { - t.Errorf("ShouldSendDailyDigestMessage() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/server/playbooks/server/app/reminder.go b/server/playbooks/server/app/reminder.go deleted file mode 100644 index 091591cad89..00000000000 --- a/server/playbooks/server/app/reminder.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "fmt" - "strings" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const RetrospectivePrefix = "retro_" - -// HandleReminder is the handler for all reminder events. -func (s *PlaybookRunServiceImpl) HandleReminder(key string) { - if strings.HasPrefix(key, RetrospectivePrefix) { - s.handleReminderToFillRetro(strings.TrimPrefix(key, RetrospectivePrefix)) - } else { - s.handleStatusUpdateReminder(key) - } -} - -func (s *PlaybookRunServiceImpl) handleReminderToFillRetro(playbookRunID string) { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRunToRemind, err := s.GetPlaybookRun(playbookRunID) - if err != nil { - logger.WithError(err).Errorf("handleReminderToFillRetro failed to get playbook run") - return - } - - // In the meantime we did publish a retrospective, so no reminder. - if playbookRunToRemind.RetrospectivePublishedAt != 0 { - return - } - - // If we are not in the finished state then don't remind - if playbookRunToRemind.CurrentStatus != StatusFinished { - return - } - - if err = s.postRetrospectiveReminder(playbookRunToRemind, false); err != nil { - logger.WithError(err).Errorf("couldn't post reminder") - return - } - - // Jobs can't be rescheduled within themselves with the same key. As a temporary workaround do it in a delayed goroutine - go func() { - time.Sleep(time.Second * 2) - if err = s.SetReminder(RetrospectivePrefix+playbookRunID, time.Duration(playbookRunToRemind.RetrospectiveReminderIntervalSeconds)*time.Second); err != nil { - logger.WithError(err).Errorf("failed to reocurr retrospective reminder") - return - } - }() -} - -func (s *PlaybookRunServiceImpl) handleStatusUpdateReminder(playbookRunID string) { - logger := logrus.WithField("playbook_run_id", playbookRunID) - - playbookRunToModify, err := s.GetPlaybookRun(playbookRunID) - if err != nil { - logger.WithError(err).Error("HandleReminder failed to get playbook run") - return - } - - owner, err := s.api.GetUserByID(playbookRunToModify.OwnerUserID) - if err != nil { - logger.WithError(err).WithField("user_id", playbookRunToModify.OwnerUserID).Error("HandleReminder failed to get owner") - return - } - - attachments := []*model.SlackAttachment{ - { - Actions: []*model.PostAction{ - { - Type: "button", - Name: "Update status", - Integration: &model.PostActionIntegration{ - URL: fmt.Sprintf("/plugins/%s/api/v0/runs/%s/reminder/button-update", - "playbooks", - playbookRunToModify.ID), - }, - }, - }, - }, - } - - post := &model.Post{ - Message: fmt.Sprintf("@%s, please provide a status update for [%s](%s).", owner.Username, playbookRunToModify.Name, GetRunDetailsRelativeURL(playbookRunID)), - ChannelId: playbookRunToModify.ChannelID, - Type: "custom_update_status", - Props: map[string]any{ - "targetUsername": owner.Username, - "playbookRunId": playbookRunToModify.ID, - }, - } - model.ParseSlackAttachment(post, attachments) - - if err = s.poster.PostMessageToThread("", post); err != nil { - logger.WithError(err).Errorf("HandleReminder error posting reminder message") - return - } - - // broadcast to followers - message, err := s.buildOverdueStatusUpdateMessage(playbookRunToModify, owner.Username) - if err != nil { - logger.WithError(err).Error("failed to build overdue status update message") - } else { - err = s.dmPostToRunFollowers(&model.Post{Message: message}, overdueStatusUpdateMessage, playbookRunToModify.ID, "") - if err != nil { - logger.WithError(err).Error("failed to dm post to run followers") - } - } - - playbookRunToModify.ReminderPostID = post.Id - if _, err = s.store.UpdatePlaybookRun(playbookRunToModify); err != nil { - logger.WithError(err).Error("error updating with reminder post id") - } -} - -func (s *PlaybookRunServiceImpl) buildOverdueStatusUpdateMessage(playbookRun *PlaybookRun, ownerUserName string) (string, error) { - channel, err := s.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - return "", errors.Wrapf(err, "can't get channel - %s", playbookRun.ChannelID) - } - - team, err := s.api.GetTeam(channel.TeamId) - if err != nil { - return "", errors.Wrapf(err, "can't get team - %s", channel.TeamId) - } - - message := fmt.Sprintf("Status update is overdue for [%s](/%s/channels/%s?telem_action=todo_overduestatus_clicked&telem_run_id=%s&forceRHSOpen) (Owner: @%s)\n", - channel.DisplayName, team.Name, channel.Name, playbookRun.ID, ownerUserName) - - return message, nil -} - -// SetReminder sets a reminder. After timeInMinutes in the future, the owner will be -// reminded to update the playbook run's status. -func (s *PlaybookRunServiceImpl) SetReminder(playbookRunID string, fromNow time.Duration) error { - if _, err := s.scheduler.ScheduleOnce(playbookRunID, time.Now().Add(fromNow)); err != nil { - return errors.Wrap(err, "unable to schedule reminder") - } - - return nil -} - -// RemoveReminder removes the pending reminder for the given playbook run, if any. -func (s *PlaybookRunServiceImpl) RemoveReminder(playbookRunID string) { - s.scheduler.Cancel(playbookRunID) -} - -// resetReminderTimer sets the previous reminder timer to 0. -func (s *PlaybookRunServiceImpl) resetReminderTimer(playbookRunID string) error { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrapf(err, "failed to retrieve playbook run") - } - - playbookRunToModify.PreviousReminder = 0 - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run after resetting reminder timer") - } - - s.poster.PublishWebsocketEventToChannel(playbookRunUpdatedWSEvent, playbookRunToModify, playbookRunToModify.ChannelID) - - return nil -} - -// ResetReminder creates a timeline event for a reminder being reset and then creates a new reminder -func (s *PlaybookRunServiceImpl) ResetReminder(playbookRunID string, newReminder time.Duration) error { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrapf(err, "failed to retrieve playbook run") - } - - eventTime := model.GetMillis() - event := &TimelineEvent{ - PlaybookRunID: playbookRunToModify.ID, - CreateAt: eventTime, - EventAt: eventTime, - EventType: StatusUpdateSnoozed, - SubjectUserID: playbookRunToModify.ReporterUserID, - } - - if _, err := s.store.CreateTimelineEvent(event); err != nil { - return errors.Wrapf(err, "failed to create timeline event after resetting reminder timer") - } - - return s.SetNewReminder(playbookRunID, newReminder) -} - -// SetNewReminder sets a new reminder for playbookRunID, removes any pending reminder, removes the -// reminder post in the playbookRun's channel, and resets the PreviousReminder and -// LastStatusUpdateAt (so the countdown timer to "update due" shows the correct time) -func (s *PlaybookRunServiceImpl) SetNewReminder(playbookRunID string, newReminder time.Duration) error { - playbookRunToModify, err := s.store.GetPlaybookRun(playbookRunID) - if err != nil { - return errors.Wrapf(err, "failed to retrieve playbook run") - } - - // Remove pending reminder (if any) - s.RemoveReminder(playbookRunID) - - // Remove reminder post (if any) - if playbookRunToModify.ReminderPostID != "" { - if err = s.removePost(playbookRunToModify.ReminderPostID); err != nil { - return err - } - playbookRunToModify.ReminderPostID = "" - } - - playbookRunToModify.PreviousReminder = newReminder - playbookRunToModify.LastStatusUpdateAt = model.GetMillis() - - playbookRunToModify, err = s.store.UpdatePlaybookRun(playbookRunToModify) - if err != nil { - return errors.Wrapf(err, "failed to update playbook run after resetting reminder timer") - } - - if newReminder != 0 { - if err = s.SetReminder(playbookRunID, newReminder); err != nil { - return errors.Wrap(err, "failed to set the reminder for playbook run") - } - } - - s.poster.PublishWebsocketEventToChannel(playbookRunUpdatedWSEvent, playbookRunToModify, playbookRunToModify.ChannelID) - - return nil -} - -func (s *PlaybookRunServiceImpl) removePost(postID string) error { - post, err := s.api.GetPost(postID) - if err != nil { - return errors.Wrapf(err, "failed to retrieve reminder post %s", postID) - } - - if post.DeleteAt != 0 { - return nil - } - - if _, err = s.api.DeletePost(postID); err != nil { - return errors.Wrapf(err, "failed to delete reminder post %s", postID) - } - - return nil -} diff --git a/server/playbooks/server/app/sort.go b/server/playbooks/server/app/sort.go deleted file mode 100644 index 208a38f3b6d..00000000000 --- a/server/playbooks/server/app/sort.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -// SortField enumerates the available fields we can sort on. -type SortField string - -const ( - // SortByTitle sorts by the title field of a playbook. - SortByTitle SortField = "title" - - // SortByStages sorts by the number of checklists in a playbook. - SortByStages SortField = "stages" - - // SortBySteps sorts by the number of steps in a playbook. - SortBySteps SortField = "steps" - - // SortByRuns sorts by the number of times a playbook has been run. - SortByRuns SortField = "runs" - - // SortByCreateAt sorts by the created time of a playbook or playbook run. - SortByCreateAt SortField = "create_at" - - // SortByID sorts by the primary key of a playbook or playbook run. - SortByID SortField = "id" - - // SortByName sorts by the name of a playbook run. - SortByName SortField = "name" - - // SortByOwnerUserID sorts by the user id of the owner of a playbook run. - SortByOwnerUserID SortField = "owner_user_id" - - // SortByTeamID sorts by the team id of a playbook or playbook run. - SortByTeamID SortField = "team_id" - - // SortByEndAt sorts by the end time of a playbook run. - SortByEndAt SortField = "end_at" - - // SortByStatus sorts by the status of a playbook run. - SortByStatus SortField = "status" - - // SortByLastStatusUpdateAt sorts by when the playbook run was last updated. - SortByLastStatusUpdateAt SortField = "last_status_update_at" - - // SortByLastStatusUpdateAt sorts by when the playbook was last run. - SortByLastRunAt SortField = "last_run_at" - - // SortByActiveRuns sorts by number of active runs in the playbook. - SortByActiveRuns SortField = "active_runs" - - // SortByMetric0 ..3 sorts by the playbook's metric index - SortByMetric0 SortField = "metric0" - SortByMetric1 SortField = "metric1" - SortByMetric2 SortField = "metric2" - SortByMetric3 SortField = "metric3" -) - -// SortDirection is the type used to specify the ascending or descending order of returned results. -type SortDirection string - -const ( - // DirectionDesc is descending order. - DirectionDesc SortDirection = "DESC" - - // DirectionAsc is ascending order. - DirectionAsc SortDirection = "ASC" -) - -func IsValidDirection(direction SortDirection) bool { - return direction == DirectionAsc || direction == DirectionDesc -} diff --git a/server/playbooks/server/app/task_actions.go b/server/playbooks/server/app/task_actions.go deleted file mode 100644 index dc6ac8cd1ed..00000000000 --- a/server/playbooks/server/app/task_actions.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "encoding/json" - "strings" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -type TaskAction struct { - Trigger Trigger `json:"trigger"` - Actions []Action `json:"actions"` -} - -type TaskActionType string -type TaskTriggerType string - -type Trigger struct { - Type TaskTriggerType `json:"type"` - // Payload is the json payload that stores trigger specific settings or config. - // This should be unmarshalled into a concrete type during usage - Payload string `json:"payload"` -} - -type Action struct { - Type TaskActionType `json:"type"` - // Payload is the json payload that stores action specific settings or config. - // This should be unmarshalled into a concrete type during usage - Payload string `json:"payload"` -} - -// Known Types -const ( - KeywordsByUsersTriggerType TaskTriggerType = "keywords_by_users" - - MarkItemAsDoneActionType TaskActionType = "mark_item_as_done" -) - -var ( - ValidTaskActionTypes = []TaskActionType{ - MarkItemAsDoneActionType, - } -) - -// Triggers -type KeywordsByUsersTrigger struct { - typ TaskTriggerType - Payload KeywordsByUsersTriggerPayload -} - -type KeywordsByUsersTriggerPayload struct { - Keywords []string `json:"keywords" mapstructure:"keywords"` - UserIDs []string `json:"user_ids" mapstructure:"user_ids"` -} - -func NewKeywordsByUsersTrigger(trigger Trigger) (*KeywordsByUsersTrigger, error) { - if trigger.Type != KeywordsByUsersTriggerType { - return nil, errors.Errorf("Unexpected trigger type: %s, expected: %s", trigger.Type, KeywordsByUsersTriggerType) - } - var t KeywordsByUsersTrigger - t.typ = KeywordsByUsersTriggerType - - if err := json.Unmarshal([]byte(trigger.Payload), &t.Payload); err != nil { - return nil, errors.New("unable to decode payload from trigger") - } - return &t, nil -} - -func (t *KeywordsByUsersTrigger) IsValid() error { - return nil -} - -func (t *KeywordsByUsersTrigger) IsTriggered(post *model.Post) bool { - foundUser := false - if len(t.Payload.UserIDs) > 0 { - for _, userID := range t.Payload.UserIDs { - if post.UserId == userID { - foundUser = true - break - } - } - } else { - foundUser = true - } - if foundUser { - for _, keyword := range t.Payload.Keywords { - if strings.Contains(post.Message, keyword) { - logrus.WithField("keyword", keyword) - return true - } - } - } - return false -} - -// Actions -type MarkItemAsDoneAction struct { - typ TaskActionType - Payload MarkItemAsDoneActionPayload -} - -type MarkItemAsDoneActionPayload struct { - Enabled bool `json:"enabled"` -} - -func NewMarkItemAsDoneAction(action Action) (*MarkItemAsDoneAction, error) { - if action.Type != MarkItemAsDoneActionType { - return nil, errors.Errorf("Unexpected trigger type: %s, expected: %s", action.Type, MarkItemAsDoneActionType) - } - var a MarkItemAsDoneAction - a.typ = MarkItemAsDoneActionType - - if err := json.Unmarshal([]byte(action.Payload), &a.Payload); err != nil { - return nil, errors.New("unable to decode payload from trigger") - } - return &a, nil -} - -func (a *MarkItemAsDoneAction) IsValid() error { - return nil -} - -// Validators -func ValidateTrigger(t Trigger) error { - switch t.Type { - case KeywordsByUsersTriggerType: - trigger, err := NewKeywordsByUsersTrigger(t) - if err != nil { - return err - } - return trigger.IsValid() - default: - return errors.Errorf("Unknown task trigger type: %s", t.Type) - } -} - -func ValidateAction(a Action) error { - switch a.Type { - case MarkItemAsDoneActionType: - action, err := NewMarkItemAsDoneAction(a) - if err != nil { - return err - } - return action.IsValid() - default: - return errors.Errorf("Unknown task action type: %s", a.Type) - } -} diff --git a/server/playbooks/server/app/task_actions_test.go b/server/playbooks/server/app/task_actions_test.go deleted file mode 100644 index 07158668419..00000000000 --- a/server/playbooks/server/app/task_actions_test.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/stretchr/testify/require" -) - -func TestTaskActionActions(t *testing.T) { -} - -func TestTaskActionTriggers(t *testing.T) { - t.Run("Keywords by user trigger", func(t *testing.T) { - - t.Run("validator", func(t *testing.T) { - _, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "", - }) - require.Error(t, err) - - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"one\", \"two\"], \"user_ids\":[]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - - // Empty keywords and user_ids is valid. This means this trigger - // is triggered on every posted message. - // On the frontend, in the task actions modal, if the keywords input - // is made empty, then the action is marked as disabled. - trigger, err = NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[], \"user_ids\":[]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - }) - - t.Run("triggering", func(t *testing.T) { - t.Run("simple", func(t *testing.T) { - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"one\", \"two\"], \"user_ids\":[]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - require.True(t, trigger.IsTriggered(&model.Post{Message: "one is a trigger word"})) - }) - - t.Run("trigger words with with formatting matches, backticks", func(t *testing.T) { - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"phrase with `backticks`\", \"two\"], \"user_ids\":[]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - require.True(t, trigger.IsTriggered(&model.Post{Message: "post with a phrase with `backticks`"})) - }) - - t.Run("trigger words with with formatting matches, asterisks", func(t *testing.T) { - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"phrase with *asterisks*\", \"two\"], \"user_ids\":[]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - require.True(t, trigger.IsTriggered(&model.Post{Message: "post with a phrase with *asterisks*"})) - }) - - t.Run("simple, post does not contain trigger word", func(t *testing.T) { - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"one\", \"two\"], \"user_ids\":[]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - require.False(t, trigger.IsTriggered(&model.Post{Message: "three is NOT a trigger word"})) - }) - - t.Run("With user specified in the trigger", func(t *testing.T) { - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"one\", \"two\"], \"user_ids\":[\"abc\"]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - require.True(t, trigger.IsTriggered(&model.Post{Message: "one is a trigger word", UserId: "abc"})) - }) - - t.Run("With user specified in the trigger, but post is by other user", func(t *testing.T) { - trigger, err := NewKeywordsByUsersTrigger(Trigger{ - Type: KeywordsByUsersTriggerType, - Payload: "{\"keywords\":[\"one\", \"two\"], \"user_ids\":[\"abc\"]}", - }) - require.NoError(t, err) - require.NoError(t, trigger.IsValid()) - require.False(t, trigger.IsTriggered(&model.Post{Message: "one is a trigger word", UserId: "def"})) - }) - }) - }) -} diff --git a/server/playbooks/server/app/telemetry.go b/server/playbooks/server/app/telemetry.go deleted file mode 100644 index 835c6de0bf6..00000000000 --- a/server/playbooks/server/app/telemetry.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import "fmt" - -// GenericTelemetry is the generic interface for telemetry. -type GenericTelemetry interface { - Page(name TelemetryPage, properties map[string]interface{}) - Track(name TelemetryTrack, properties map[string]interface{}) -} - -// TelemetryType is the type for the different kinds of tracking we have -type TelemetryType string - -const ( - // TelemetryTypeTrack is for tracking events (click, submit, etc..) - TelemetryTypeTrack TelemetryType = "track" - // TelemetryTypePage is for tracking page views - TelemetryTypePage TelemetryType = "page" -) - -// TelemetryTrack is a type alias to hold all possible -// event tracking names in an enum-like -// -// Contained names should match the ones that are at webapp/src/types/telemetry.ts -// when they use generic tracking -type TelemetryTrack int - -const ( - telemetryRunFollow TelemetryTrack = iota - telemetryRunUnfollow - telemetryRunCreate - telemetryRunParticipate - telemetryRunLeave - telemetryRunUpdateActions - telemetryTaskActionsTriggered - telemetryTaskActionsActionExecuted - telemetryTaskActionsUpdated -) - -var trackTypes = [...]string{ - telemetryRunFollow: "playbookrun_follow", - telemetryRunUnfollow: "playbookrun_unfollow", - telemetryRunCreate: "playbookrun_create", - telemetryRunParticipate: "playbookrun_participate", - telemetryRunLeave: "playbookrun_leave", - telemetryRunUpdateActions: "playbookrun_update_actions", - telemetryTaskActionsUpdated: "taskactions_updated", - telemetryTaskActionsTriggered: "taskactions_triggered", - telemetryTaskActionsActionExecuted: "taskactions_action_executed", -} - -// String creates the string version of the TelemetryTrack -func (tt TelemetryTrack) String() string { - return trackTypes[tt] -} - -// TelemetryPage is a type alias to hold all possible -// page tracking names in an enum-like -// -// Contained names should match the ones that are at webapp/src/types/telemetry.ts -// when they use generic tracking -type TelemetryPage int - -const ( - telemetryRunStatusUpdate TelemetryPage = iota - telemetryRunDetails - telemetryTaskInbox - telemetryChannelsRunDetails - telemetryChannelsHome - telemetryChannelsRunList -) - -var pageTypes = [...]string{ - telemetryRunStatusUpdate: "run_status_update", - telemetryTaskInbox: "task_inbox", - telemetryRunDetails: "run_details", // Backstage RDP - telemetryChannelsRunDetails: "channels_rhs_rundetails", // RHS details - telemetryChannelsHome: "channels_rhs_home", // RHS templates list - telemetryChannelsRunList: "channels_rhs_runlist", // RHS runs list -} - -// String creates the string version of the Telemetrypage -func (tp TelemetryPage) String() string { - return pageTypes[tp] -} - -// NewTelemetryPage creates an instance of TelemetryPage from a string. -// It's useful to validate that the arbitrary string has a equivalent constant -// for what pages we want to track (and avoid typos). -func NewTelemetryPage(name string) (*TelemetryPage, error) { - for i, ct := range pageTypes { - if ct == name { - tp := TelemetryPage(i) - return &tp, nil - } - } - return nil, fmt.Errorf("unknown page type: %s", name) -} - -// NewTelemetryTrack creates an instance of TelemetryTrack from a string. -// It's useful to validate that the arbitrary string has a equivalent constant -// for what events we want to track (and avoid typos). -func NewTelemetryTrack(name string) (*TelemetryTrack, error) { - for i, ct := range trackTypes { - if ct == name { - tt := TelemetryTrack(i) - return &tt, nil - } - } - return nil, fmt.Errorf("unknown track type: %s", name) -} diff --git a/server/playbooks/server/app/urls.go b/server/playbooks/server/app/urls.go deleted file mode 100644 index 22dedaa0d02..00000000000 --- a/server/playbooks/server/app/urls.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import "fmt" - -const ( - PlaybooksPath = "/playbooks/playbooks" - RunsPath = "/playbooks/runs" -) - -// relative urls -func GetRunDetailsRelativeURL(playbookRunID string) string { - return fmt.Sprintf("%s/%s", RunsPath, playbookRunID) -} - -func GetPlaybookDetailsRelativeURL(playbookID string) string { - return fmt.Sprintf("%s/%s", PlaybooksPath, playbookID) -} - -// absolute urls -func getRunDetailsURL(siteURL string, playbookRunID string) string { - return fmt.Sprintf("%s%s", siteURL, GetRunDetailsRelativeURL(playbookRunID)) -} - -func getRunRetrospectiveURL(siteURL string, playbookRunID string) string { - return fmt.Sprintf("%s/retrospective", getRunDetailsURL(siteURL, playbookRunID)) -} - -func getPlaybooksURL(siteURL string) string { - return fmt.Sprintf("%s%s", siteURL, PlaybooksPath) -} - -func getPlaybooksNewURL(siteURL string) string { - return fmt.Sprintf("%s/new", getPlaybooksURL(siteURL)) -} - -func getPlaybookDetailsURL(siteURL string, playbookID string) string { - return fmt.Sprintf("%s%s", siteURL, GetPlaybookDetailsRelativeURL(playbookID)) -} - -func getChannelURL(siteURL string, teamName string, channelName string) string { - return fmt.Sprintf("%s/%s/channels/%s", - siteURL, - teamName, - channelName, - ) -} diff --git a/server/playbooks/server/app/urls_test.go b/server/playbooks/server/app/urls_test.go deleted file mode 100644 index d79c420c64d..00000000000 --- a/server/playbooks/server/app/urls_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGetPlaybookDetailsURL(t *testing.T) { - require.Equal(t, - "http://mattermost.com/playbooks/playbooks/playbookTestId", - getPlaybookDetailsURL("http://mattermost.com", "playbookTestId"), - ) -} - -func TestGetPlaybooksNewURL(t *testing.T) { - require.Equal(t, - "http://mattermost.com/playbooks/playbooks/new", - getPlaybooksNewURL("http://mattermost.com"), - ) -} - -func TestGetPlaybooksURL(t *testing.T) { - require.Equal(t, - "http://mattermost.com/playbooks/playbooks", - getPlaybooksURL("http://mattermost.com"), - ) -} - -func TestGetPlaybookDetailsRelativeURL(t *testing.T) { - require.Equal(t, - "/playbooks/playbooks/testPlaybookId", - GetPlaybookDetailsRelativeURL("testPlaybookId"), - ) -} - -func TestGetRunDetailsRelativeURL(t *testing.T) { - require.Equal(t, - "/playbooks/runs/testPlaybookRunId", - GetRunDetailsRelativeURL("testPlaybookRunId"), - ) -} - -func TestGetRunDetailsURL(t *testing.T) { - require.Equal(t, - "http://mattermost.com/playbooks/runs/testPlaybookRunId", - getRunDetailsURL("http://mattermost.com", "testPlaybookRunId"), - ) -} - -func TestGetRunRetrospectiveURL(t *testing.T) { - require.Equal(t, - "http://mattermost.com/playbooks/runs/testPlaybookRunId/retrospective", - getRunRetrospectiveURL("http://mattermost.com", "testPlaybookRunId"), - ) -} diff --git a/server/playbooks/server/app/user_info.go b/server/playbooks/server/app/user_info.go deleted file mode 100644 index 895c4e948e3..00000000000 --- a/server/playbooks/server/app/user_info.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -// DigestNotificationSettings is a separate type to make it easy to marshal/unmarshal it into JSON -// in the sqlstore. It is set by the user with the `/playbook settings digest [on/off]` slash command. -type DigestNotificationSettings struct { - DisableDailyDigest bool `json:"disable_daily_digest"` - DisableWeeklyDigest bool `json:"disable_weekly_digest"` -} - -type UserInfo struct { - ID string - LastDailyTodoDMAt int64 - DigestNotificationSettings -} - -type UserInfoStore interface { - // Get retrieves a UserInfo struct by the user's userID. - Get(userID string) (UserInfo, error) - - // Upsert inserts (creates) or updates the UserInfo in info. - Upsert(info UserInfo) error -} - -// UserInfoTelemetry defines the methods that the UserInfo store needs from the RudderTelemetry. -// userID is the user initiating the event. -type UserInfoTelemetry interface { - // ChangeDigestSettings tracks when a user changes one of the digest settings - ChangeDigestSettings(userID string, old DigestNotificationSettings, new DigestNotificationSettings) -} diff --git a/server/playbooks/server/app/variables.go b/server/playbooks/server/app/variables.go deleted file mode 100644 index f0827b24487..00000000000 --- a/server/playbooks/server/app/variables.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "regexp" - "strings" -) - -var varsReStr = `(\$[a-zA-Z0-9_]+)` - -var reVars = regexp.MustCompile(varsReStr) - -// reVarsAndVals is the regex use to match variables and their values. -var reVarsAndVals = regexp.MustCompile(`^\s*` + varsReStr + `=(.+)\s*$`) - -// parseVariables returns the variables parsed from the given text. -// Each variable must be defined on a separate line, and must match -// the `reVar` regex. -func parseVariablesAndValues(input string) map[string]string { - lines := strings.Split(input, "\n") - vars := make(map[string]string) - for _, line := range lines { - if !reVarsAndVals.MatchString(line) { - continue - } - match := reVarsAndVals.FindStringSubmatch(line) - vars[match[1]] = match[2] - } - return vars -} - -// parseVariables returns the variable names in the given input string. -func parseVariables(input string) []string { - return reVars.FindAllString(input, -1) -} diff --git a/server/playbooks/server/app/variables_test.go b/server/playbooks/server/app/variables_test.go deleted file mode 100644 index 048218b10fd..00000000000 --- a/server/playbooks/server/app/variables_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package app - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParseVariablesAndValues(t *testing.T) { - t.Run("Simple", func(t *testing.T) { - res := parseVariablesAndValues(` - $bar=one - $five=star - `) - require.Equal(t, 2, len(res)) - require.Equal(t, "one", res["$bar"]) - require.Equal(t, "star", res["$five"]) - }) - - t.Run("Variable Names: Match only lower case, upper case and underscore", func(t *testing.T) { - res := parseVariablesAndValues(` - This is a summary. This part of the summary will not be matched. - My variables are: - $a-to-z=NoMatch - $a space=NoMatch - $1_one=Match - $a_2_z=Match - `) - require.Equal(t, 2, len(res)) - require.Equal(t, "Match", res["$1_one"]) - require.Equal(t, "Match", res["$a_2_z"]) - }) - - t.Run("Variable Values", func(t *testing.T) { - res := parseVariablesAndValues(` - This is a summary. This part of the summary will not be matched. - My variables are: - $1_one=This is a match - $a_2_z=This-is-also-a-match - $version=v7.1.1 - $BRANCH=release/v7.1.1 - `) - require.Equal(t, 4, len(res)) - require.Equal(t, "This is a match", res["$1_one"]) - require.Equal(t, "This-is-also-a-match", res["$a_2_z"]) - require.Equal(t, "v7.1.1", res["$version"]) - require.Equal(t, "release/v7.1.1", res["$BRANCH"]) - }) -} - -func TestParseVariables(t *testing.T) { - t.Run("Simple", func(t *testing.T) { - res := parseVariables(`/agenda queue $topic-$DATE`) - require.Equal(t, []string{"$topic", "$DATE"}, res) - }) - - t.Run("Variable Names: Match only lower case, upper case and underscore", func(t *testing.T) { - res := parseVariables(`/echo $a-to-$z extra $1_one$a_2_z`) - require.Equal(t, []string{"$a", "$z", "$1_one", "$a_2_z"}, res) - }) -} diff --git a/server/playbooks/server/bot/bot.go b/server/playbooks/server/bot/bot.go deleted file mode 100644 index 6b1eec4129a..00000000000 --- a/server/playbooks/server/bot/bot.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package bot - -import ( - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -// Bot stores the information for the plugin configuration, and implements the Poster interfaces. -type Bot struct { - configService config.Service - serviceAdapter playbooks.ServicesAPI - botUserID string - telemetry Telemetry -} - -// Poster interface - a small subset of the plugin posting API. -type Poster interface { - // Post posts a custom post, which should provide the Message and ChannelId fields - Post(post *model.Post) error - - // PostMessage posts a simple message to channelID. Returns the post id if posting was successful. - PostMessage(channelID, format string, args ...interface{}) (*model.Post, error) - - // PostMessageToThread posts a message to a specified channel and thread identified by rootPostID. - // If the rootPostID is blank, or the rootPost is deleted, it will create a standalone post. The - // returned post's RootID (or ID, if there was no root post) should be used as the rootID for - // future use (i.e., save that if you want to continue the thread). - PostMessageToThread(rootPostID string, post *model.Post) error - - // PostMessageWithAttachments posts a message with slack attachments to channelID. Returns the post id if - // posting was successful. Often used to include post actions. - PostMessageWithAttachments(channelID string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error) - - // PostCustomMessageWithAttachments posts a custom message with the specified type. Falling back to attachments for mobile. - PostCustomMessageWithAttachments(channelID, customType string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error) - - // DM posts a DM from the plugin bot to the specified user - DM(userID string, post *model.Post) error - - // EphemeralPost sends an ephemeral message to a user. - EphemeralPost(userID, channelID string, post *model.Post) - - // SystemEphemeralPost sends an ephemeral message to a user authored by the System. - SystemEphemeralPost(userID, channelID string, post *model.Post) - - // EphemeralPostWithAttachments sends an ephemeral message to a user with Slack attachments. - EphemeralPostWithAttachments(userID, channelID, rootPostID string, attachments []*model.SlackAttachment, format string, args ...interface{}) - - // PublishWebsocketEventToTeam sends a websocket event with payload to teamID. - PublishWebsocketEventToTeam(event string, payload interface{}, teamID string) - - // PublishWebsocketEventToChannel sends a websocket event with payload to channelID. - PublishWebsocketEventToChannel(event string, payload interface{}, channelID string) - - // PublishWebsocketEventToUser sends a websocket event with payload to userID. - PublishWebsocketEventToUser(event string, payload interface{}, userID string) - - // NotifyAdmins sends a DM with the message to each admins - NotifyAdmins(message, authorUserID string, isTeamEdition bool) error - - // IsFromPoster returns whether the provided post was sent by this poster - IsFromPoster(post *model.Post) bool -} - -type Telemetry interface { - NotifyAdmins(userID string, action string) - StartTrial(userID string, action string) -} - -// New creates a new bot poster. -func New(serviceAdapter playbooks.ServicesAPI, botUserID string, configService config.Service, telemetry Telemetry) *Bot { - return &Bot{ - serviceAdapter: serviceAdapter, - botUserID: botUserID, - configService: configService, - telemetry: telemetry, - } -} diff --git a/server/playbooks/server/bot/mocks/mock_poster.go b/server/playbooks/server/bot/mocks/mock_poster.go deleted file mode 100644 index 3be0df21fce..00000000000 --- a/server/playbooks/server/bot/mocks/mock_poster.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/bot (interfaces: Poster) - -// Package mock_bot is a generated GoMock package. -package mock_bot - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/public/model" -) - -// MockPoster is a mock of Poster interface. -type MockPoster struct { - ctrl *gomock.Controller - recorder *MockPosterMockRecorder -} - -// MockPosterMockRecorder is the mock recorder for MockPoster. -type MockPosterMockRecorder struct { - mock *MockPoster -} - -// NewMockPoster creates a new mock instance. -func NewMockPoster(ctrl *gomock.Controller) *MockPoster { - mock := &MockPoster{ctrl: ctrl} - mock.recorder = &MockPosterMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPoster) EXPECT() *MockPosterMockRecorder { - return m.recorder -} - -// DM mocks base method. -func (m *MockPoster) DM(arg0 string, arg1 *model.Post) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DM", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DM indicates an expected call of DM. -func (mr *MockPosterMockRecorder) DM(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DM", reflect.TypeOf((*MockPoster)(nil).DM), arg0, arg1) -} - -// EphemeralPost mocks base method. -func (m *MockPoster) EphemeralPost(arg0, arg1 string, arg2 *model.Post) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "EphemeralPost", arg0, arg1, arg2) -} - -// EphemeralPost indicates an expected call of EphemeralPost. -func (mr *MockPosterMockRecorder) EphemeralPost(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EphemeralPost", reflect.TypeOf((*MockPoster)(nil).EphemeralPost), arg0, arg1, arg2) -} - -// EphemeralPostWithAttachments mocks base method. -func (m *MockPoster) EphemeralPostWithAttachments(arg0, arg1, arg2 string, arg3 []*model.SlackAttachment, arg4 string, arg5 ...interface{}) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1, arg2, arg3, arg4} - for _, a := range arg5 { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "EphemeralPostWithAttachments", varargs...) -} - -// EphemeralPostWithAttachments indicates an expected call of EphemeralPostWithAttachments. -func (mr *MockPosterMockRecorder) EphemeralPostWithAttachments(arg0, arg1, arg2, arg3, arg4 interface{}, arg5 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1, arg2, arg3, arg4}, arg5...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EphemeralPostWithAttachments", reflect.TypeOf((*MockPoster)(nil).EphemeralPostWithAttachments), varargs...) -} - -// IsFromPoster mocks base method. -func (m *MockPoster) IsFromPoster(arg0 *model.Post) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsFromPoster", arg0) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsFromPoster indicates an expected call of IsFromPoster. -func (mr *MockPosterMockRecorder) IsFromPoster(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFromPoster", reflect.TypeOf((*MockPoster)(nil).IsFromPoster), arg0) -} - -// NotifyAdmins mocks base method. -func (m *MockPoster) NotifyAdmins(arg0, arg1 string, arg2 bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NotifyAdmins", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// NotifyAdmins indicates an expected call of NotifyAdmins. -func (mr *MockPosterMockRecorder) NotifyAdmins(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotifyAdmins", reflect.TypeOf((*MockPoster)(nil).NotifyAdmins), arg0, arg1, arg2) -} - -// Post mocks base method. -func (m *MockPoster) Post(arg0 *model.Post) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Post", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// Post indicates an expected call of Post. -func (mr *MockPosterMockRecorder) Post(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockPoster)(nil).Post), arg0) -} - -// PostCustomMessageWithAttachments mocks base method. -func (m *MockPoster) PostCustomMessageWithAttachments(arg0, arg1 string, arg2 []*model.SlackAttachment, arg3 string, arg4 ...interface{}) (*model.Post, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1, arg2, arg3} - for _, a := range arg4 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "PostCustomMessageWithAttachments", varargs...) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// PostCustomMessageWithAttachments indicates an expected call of PostCustomMessageWithAttachments. -func (mr *MockPosterMockRecorder) PostCustomMessageWithAttachments(arg0, arg1, arg2, arg3 interface{}, arg4 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1, arg2, arg3}, arg4...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostCustomMessageWithAttachments", reflect.TypeOf((*MockPoster)(nil).PostCustomMessageWithAttachments), varargs...) -} - -// PostMessage mocks base method. -func (m *MockPoster) PostMessage(arg0, arg1 string, arg2 ...interface{}) (*model.Post, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} - for _, a := range arg2 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "PostMessage", varargs...) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// PostMessage indicates an expected call of PostMessage. -func (mr *MockPosterMockRecorder) PostMessage(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMessage", reflect.TypeOf((*MockPoster)(nil).PostMessage), varargs...) -} - -// PostMessageToThread mocks base method. -func (m *MockPoster) PostMessageToThread(arg0 string, arg1 *model.Post) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PostMessageToThread", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// PostMessageToThread indicates an expected call of PostMessageToThread. -func (mr *MockPosterMockRecorder) PostMessageToThread(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMessageToThread", reflect.TypeOf((*MockPoster)(nil).PostMessageToThread), arg0, arg1) -} - -// PostMessageWithAttachments mocks base method. -func (m *MockPoster) PostMessageWithAttachments(arg0 string, arg1 []*model.SlackAttachment, arg2 string, arg3 ...interface{}) (*model.Post, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1, arg2} - for _, a := range arg3 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "PostMessageWithAttachments", varargs...) - ret0, _ := ret[0].(*model.Post) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// PostMessageWithAttachments indicates an expected call of PostMessageWithAttachments. -func (mr *MockPosterMockRecorder) PostMessageWithAttachments(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMessageWithAttachments", reflect.TypeOf((*MockPoster)(nil).PostMessageWithAttachments), varargs...) -} - -// PublishWebsocketEventToChannel mocks base method. -func (m *MockPoster) PublishWebsocketEventToChannel(arg0 string, arg1 interface{}, arg2 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "PublishWebsocketEventToChannel", arg0, arg1, arg2) -} - -// PublishWebsocketEventToChannel indicates an expected call of PublishWebsocketEventToChannel. -func (mr *MockPosterMockRecorder) PublishWebsocketEventToChannel(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebsocketEventToChannel", reflect.TypeOf((*MockPoster)(nil).PublishWebsocketEventToChannel), arg0, arg1, arg2) -} - -// PublishWebsocketEventToTeam mocks base method. -func (m *MockPoster) PublishWebsocketEventToTeam(arg0 string, arg1 interface{}, arg2 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "PublishWebsocketEventToTeam", arg0, arg1, arg2) -} - -// PublishWebsocketEventToTeam indicates an expected call of PublishWebsocketEventToTeam. -func (mr *MockPosterMockRecorder) PublishWebsocketEventToTeam(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebsocketEventToTeam", reflect.TypeOf((*MockPoster)(nil).PublishWebsocketEventToTeam), arg0, arg1, arg2) -} - -// PublishWebsocketEventToUser mocks base method. -func (m *MockPoster) PublishWebsocketEventToUser(arg0 string, arg1 interface{}, arg2 string) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "PublishWebsocketEventToUser", arg0, arg1, arg2) -} - -// PublishWebsocketEventToUser indicates an expected call of PublishWebsocketEventToUser. -func (mr *MockPosterMockRecorder) PublishWebsocketEventToUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishWebsocketEventToUser", reflect.TypeOf((*MockPoster)(nil).PublishWebsocketEventToUser), arg0, arg1, arg2) -} - -// SystemEphemeralPost mocks base method. -func (m *MockPoster) SystemEphemeralPost(arg0, arg1 string, arg2 *model.Post) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SystemEphemeralPost", arg0, arg1, arg2) -} - -// SystemEphemeralPost indicates an expected call of SystemEphemeralPost. -func (mr *MockPosterMockRecorder) SystemEphemeralPost(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SystemEphemeralPost", reflect.TypeOf((*MockPoster)(nil).SystemEphemeralPost), arg0, arg1, arg2) -} diff --git a/server/playbooks/server/bot/poster.go b/server/playbooks/server/bot/poster.go deleted file mode 100644 index 256211672c2..00000000000 --- a/server/playbooks/server/bot/poster.go +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package bot - -import ( - "encoding/json" - "fmt" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const maxAdminsToQueryForNotification = 1000 - -// PostMessage posts a message to a specified channel. -func (b *Bot) PostMessage(channelID, format string, args ...interface{}) (*model.Post, error) { - post := &model.Post{ - Message: fmt.Sprintf(format, args...), - UserId: b.botUserID, - ChannelId: channelID, - } - if _, err := b.serviceAdapter.CreatePost(post); err != nil { - return nil, err - } - return post, nil -} - -// Post posts a custom post. The Message and ChannelId fields should be provided in the specified -// post -func (b *Bot) Post(post *model.Post) error { - if post.Message == "" { - return fmt.Errorf("the post does not contain a message") - } - - if !model.IsValidId(post.ChannelId) { - return fmt.Errorf("the post does not contain a valid ChannelId") - } - - post.UserId = b.botUserID - _, err := b.serviceAdapter.CreatePost(post) - return err -} - -// PostMessageToThread posts a message to a specified thread identified by rootPostID. -// If the rootPostID is blank, or the rootPost is deleted, it will create a standalone post. The -// overwritten post's RootID will be the correct rootID (save that if you want to continue the thread). -func (b *Bot) PostMessageToThread(rootPostID string, post *model.Post) error { - rootID := "" - if rootPostID != "" { - root, err := b.serviceAdapter.GetPostsByIds([]string{rootPostID}) - if err == nil && len(root) > 0 && root[0].DeleteAt == 0 { - rootID = root[0].Id - } - } - - post.UserId = b.botUserID - post.RootId = rootID - - _, err := b.serviceAdapter.CreatePost(post) - return err -} - -// PostMessageWithAttachments posts a message with slack attachments to channelID. Returns the post id if -// posting was successful. Often used to include post actions. -func (b *Bot) PostMessageWithAttachments(channelID string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error) { - post := &model.Post{ - Message: fmt.Sprintf(format, args...), - UserId: b.botUserID, - ChannelId: channelID, - } - model.ParseSlackAttachment(post, attachments) - if _, err := b.serviceAdapter.CreatePost(post); err != nil { - return nil, err - } - return post, nil -} - -func (b *Bot) PostCustomMessageWithAttachments(channelID, customType string, attachments []*model.SlackAttachment, format string, args ...interface{}) (*model.Post, error) { - post := &model.Post{ - Message: fmt.Sprintf(format, args...), - UserId: b.botUserID, - ChannelId: channelID, - Type: customType, - } - model.ParseSlackAttachment(post, attachments) - if _, err := b.serviceAdapter.CreatePost(post); err != nil { - return nil, err - } - return post, nil -} - -// DM sends a DM from the plugin bot to the specified user -func (b *Bot) DM(userID string, post *model.Post) error { - channel, err := b.serviceAdapter.GetDirectChannelOrCreate(userID, b.botUserID) - if err != nil { - return errors.Wrapf(err, "failed to get bot DM channel with user_id %s", userID) - } - post.ChannelId = channel.Id - post.UserId = b.botUserID - - _, err = b.serviceAdapter.CreatePost(post) - return err -} - -// EphemeralPost sends an ephemeral message to a user -func (b *Bot) EphemeralPost(userID, channelID string, post *model.Post) { - post.UserId = b.botUserID - post.ChannelId = channelID - - b.serviceAdapter.SendEphemeralPost(userID, post) -} - -// SystemEphemeralPost sends an ephemeral message to a user authored by the System -func (b *Bot) SystemEphemeralPost(userID, channelID string, post *model.Post) { - post.ChannelId = channelID - - b.serviceAdapter.SendEphemeralPost(userID, post) -} - -// EphemeralPostWithAttachments sends an ephemeral message to a user with Slack attachments. -func (b *Bot) EphemeralPostWithAttachments(userID, channelID, postID string, attachments []*model.SlackAttachment, format string, args ...interface{}) { - post := &model.Post{ - Message: fmt.Sprintf(format, args...), - UserId: b.botUserID, - ChannelId: channelID, - RootId: postID, - } - - model.ParseSlackAttachment(post, attachments) - b.serviceAdapter.SendEphemeralPost(userID, post) -} - -// PublishWebsocketEventToTeam sends a websocket event with payload to teamID -func (b *Bot) PublishWebsocketEventToTeam(event string, payload interface{}, teamID string) { - payloadMap := b.makePayloadMap(payload) - b.serviceAdapter.PublishWebSocketEvent(event, payloadMap, &model.WebsocketBroadcast{ - TeamId: teamID, - }) -} - -// PublishWebsocketEventToChannel sends a websocket event with payload to channelID -func (b *Bot) PublishWebsocketEventToChannel(event string, payload interface{}, channelID string) { - payloadMap := b.makePayloadMap(payload) - b.serviceAdapter.PublishWebSocketEvent(event, payloadMap, &model.WebsocketBroadcast{ - ChannelId: channelID, - }) -} - -// PublishWebsocketEventToUser sends a websocket event with payload to userID -func (b *Bot) PublishWebsocketEventToUser(event string, payload interface{}, userID string) { - payloadMap := b.makePayloadMap(payload) - b.serviceAdapter.PublishWebSocketEvent(event, payloadMap, &model.WebsocketBroadcast{ - UserId: userID, - }) -} - -func (b *Bot) NotifyAdmins(messageType, authorUserID string, isTeamEdition bool) error { - author, err := b.serviceAdapter.GetUserByID(authorUserID) - if err != nil { - return errors.Wrap(err, "unable to find author user") - } - - admins, err := b.serviceAdapter.GetUsersFromProfiles(&model.UserGetOptions{ - Role: string(model.SystemAdminRoleId), - Page: 0, - PerPage: maxAdminsToQueryForNotification, - }) - - if err != nil { - return errors.Wrap(err, "unable to find all admin users") - } - - if len(admins) == 0 { - return fmt.Errorf("no admins found") - } - - var postType, footer string - - isCloud := b.configService.IsCloud() - - separator := "\n\n---\n\n" - if isCloud { - postType = "custom_cloud_upgrade" - footer = separator + "[Upgrade now](https://customers.mattermost.com)." - } else { - footer = "[Learn more](https://mattermost.com/pricing).\n\nWhen you select **Start 30-day trial**, you agree to the [Mattermost Software Evaluation Agreement](https://mattermost.com/software-evaluation-agreement/), [Privacy Policy](https://mattermost.com/privacy-policy/), and receiving product emails." - - if isTeamEdition { - footer = "[Learn more](https://mattermost.com/pricing).\n\n[Convert to Mattermost Starter](https://docs.mattermost.com/install/ee-install.html#converting-team-edition-to-enterprise-edition) to unlock this feature. Then, start a trial or upgrade to Mattermost Professional or Enterprise." - } - } - - var message, title, text string - - switch messageType { - case "start_trial_to_add_message_to_timeline", "start_trial_to_view_timeline": - message = fmt.Sprintf("@%s requested access to the playbook run timeline.", author.Username) - title = "Keep a complete record of the playbook run timeline" - text = "The playbook run timeline automatically tracks key events and messages in chronological order so that they can be traced and reviewed afterwards. Teams use timeline to perform retrospectives and extract lessons for the next time that they run the playbook." - case "start_trial_to_access_retrospective": - message = fmt.Sprintf("@%s requested access to the retrospective.", author.Username) - title = "Publish retrospective report and access the timeline" - text = "Celebrate success and learn from mistakes with retrospective reports. Filter timeline events for process review, stakeholder engagement, and auditing purposes." - case "start_trial_to_restrict_playbook_access": - message = fmt.Sprintf("@%s requested permission to configure who can access specific playbooks.", author.Username) - title = "Control who can access your team's playbooks" - text = "Playbooks are workflows that your teams and tools should follow, including everything from checklists, actions, templates, and retrospectives. When you upgrade, you can set playbook permissions for specific users or set a global permission to control which team members can create playbooks.\n" + footer - case "start_trial_to_restrict_playbook_creation": - message = fmt.Sprintf("@%s requested permission to configure who can create playbooks.", author.Username) - title = "Control who can create playbooks" - text = "Playbooks are workflows that your teams and tools should follow, including everything from checklists, actions, templates, and retrospectives. When you upgrade, you can set playbook permissions for specific users or set a global permission to control which team members can create playbooks.\n" + footer - case "start_trial_to_export_channel": - message = fmt.Sprintf("@%s requested access to export the playbook run channel.", author.Username) - title = "Save the message history of your playbook runs" - text = "Export the channel of your playbook run and save it for later analysis. When you upgrade, you can automatically generate and download a CSV file containing all the timestamped messages sent to the channel.\n" + footer - case "start_trial_to_access_playbook_dashboard": - message = fmt.Sprintf("@%s requested access to view playbook statistics", author.Username) - title = "All the statistics you need" - text = "View trends for total runs, active runs, and participants involved in runs of this playbook." - case "start_trial_to_access_metrics": - message = fmt.Sprintf("@%s requested access to playbook key metrics feature", author.Username) - title = "Track key metrics and measure value" - text = "Use metrics to understand patterns and progress across runs, and track performance." - case "start_trial_to_request_update": - message = fmt.Sprintf("@%s requested access to ask for status updates in playbook runs", author.Username) - title = "Try request update with a free trial" - text = "Request updates for playbook runs in a single click and get notified directly when an update is posted. Start a free, 30-day trial to try it out.\n" + footer - } - - actions := []*model.PostAction{ - { - - Id: "message", - Name: "Start 30-day trial", - Style: "primary", - Type: "button", - Integration: &model.PostActionIntegration{ - URL: fmt.Sprintf("/plugins/%s/api/v0/bot/notify-admins/button-start-trial", - "playbooks"), - Context: map[string]interface{}{ - "users": 100, - "termsAccepted": true, - "receiveEmailsAccepted": true, - }, - }, - }, - } - - if isTeamEdition || isCloud { - actions = []*model.PostAction{} - } - - attachments := []*model.SlackAttachment{ - { - Title: title, - Text: separator + text, - Actions: actions, - }, - } - - for _, admin := range admins { - go func(adminID string) { - channel, err := b.serviceAdapter.GetDirectChannelOrCreate(adminID, b.botUserID) - if err != nil { - logrus.WithError(err).WithFields(logrus.Fields{ - "user_id": adminID, - "bot_id": b.botUserID, - }).Warn("failed to get Direct Message channel between user and bot") - return - } - - if _, err := b.PostCustomMessageWithAttachments(channel.Id, postType, attachments, message); err != nil { - logrus.WithError(err).WithField("user_id", adminID).Error("failed to send a DM to user") - } - }(admin.Id) - } - - b.telemetry.NotifyAdmins(authorUserID, messageType) - - return nil -} - -func (b *Bot) IsFromPoster(post *model.Post) bool { - return post.UserId == b.botUserID -} - -func (b *Bot) makePayloadMap(payload interface{}) map[string]interface{} { - payloadJSON, err := json.Marshal(payload) - if err != nil { - logrus.WithError(err).Error("could not marshall payload") - payloadJSON = []byte("null") - } - return map[string]interface{}{"payload": string(payloadJSON)} -} diff --git a/server/playbooks/server/command/command.go b/server/playbooks/server/command/command.go deleted file mode 100644 index cac7baded9f..00000000000 --- a/server/playbooks/server/command/command.go +++ /dev/null @@ -1,2044 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package command - -import ( - "fmt" - "math/rand" - "strconv" - "strings" - "time" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/plugin" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/mattermost/mattermost/server/v8/playbooks/server/bot" - "github.com/mattermost/mattermost/server/v8/playbooks/server/config" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - "github.com/mattermost/mattermost/server/v8/playbooks/server/timeutils" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const helpText = "###### Mattermost Playbooks Plugin - Slash Command Help\n" + - "* `/playbook run` - Run a playbook. \n" + - "* `/playbook finish` - Finish the playbook run in this channel. \n" + - "* `/playbook update` - Provide a status update. \n" + - "* `/playbook check [checklist #] [item #]` - check/uncheck the checklist item. \n" + - "* `/playbook checkadd [checklist #] [item text]` - add a checklist item. \n" + - "* `/playbook checkremove [checklist #] [item #]` - remove a checklist item. \n" + - "* `/playbook owner [@username]` - Show or change the current owner. \n" + - "* `/playbook info` - Show a summary of the current playbook run. \n" + - "* `/playbook timeline` - Show the timeline for the current playbook run. \n" + - "* `/playbook todo` - Get a list of your assigned tasks. \n" + - "* `/playbook settings digest [on/off]` - turn daily digest on/off. \n" + - "* `/playbook settings weekly-digest [on/off]` - turn weekly digest on/off. \n" + - "\n" + - "Learn more [in our documentation](https://mattermost.com/pl/default-incident-response-app-documentation). \n" + - "" - -const confirmPrompt = "CONFIRM" - -// Register is a function that allows the runner to register commands with the mattermost server. -type Register func(*model.Command) error - -// RegisterCommands should be called by the plugin to register all necessary commands -func RegisterCommands(registerFunc Register, addTestCommands bool) error { - return registerFunc(getCommand(addTestCommands)) -} - -func getCommand(addTestCommands bool) *model.Command { - return &model.Command{ - Trigger: "playbook", - DisplayName: "Playbook", - Description: "Playbooks", - AutoComplete: true, - AutoCompleteDesc: "Available commands: run, finish, update, check, list, owner, info, todo, settings", - AutoCompleteHint: "[command]", - AutocompleteData: getAutocompleteData(addTestCommands), - } -} - -func getAutocompleteData(addTestCommands bool) *model.AutocompleteData { - command := model.NewAutocompleteData("playbook", "[command]", - "Available commands: run, finish, update, check, checkadd, checkremove, list, owner, info, timeline, todo, settings") - - run := model.NewAutocompleteData("run", "", "Starts a new playbook run") - command.AddCommand(run) - - finish := model.NewAutocompleteData("finish", "", - "Finishes a playbook run associated with the current channel") - finish.AddDynamicListArgument( - "List of channel runs is loading", - "api/v0/runs/runs-autocomplete", true) - command.AddCommand(finish) - - update := model.NewAutocompleteData("update", "", - "Provide a status update.") - update.AddDynamicListArgument( - "List of channel runs is loading", - "api/v0/runs/runs-autocomplete", true) - command.AddCommand(update) - - checklist := model.NewAutocompleteData("check", "[checklist item]", - "Checks or unchecks a checklist item.") - checklist.AddDynamicListArgument( - "List of checklist items is loading", - "api/v0/runs/checklist-autocomplete-item", true) - command.AddCommand(checklist) - - itemAdd := model.NewAutocompleteData("checkadd", "[checklist]", - "Add a checklist item") - itemAdd.AddDynamicListArgument( - "List of checklist items is loading", - "api/v0/runs/checklist-autocomplete", true) - - itemRemove := model.NewAutocompleteData("checkremove", "[checklist item]", - "Remove a checklist item") - itemRemove.AddDynamicListArgument( - "List of checklist items is loading", - "api/v0/runs/checklist-autocomplete-item", true) - - command.AddCommand(itemAdd) - command.AddCommand(itemRemove) - - owner := model.NewAutocompleteData("owner", "[@username]", - "Show or change the current owner") - owner.AddDynamicListArgument( - "List of channel runs is loading", - "api/v0/runs/runs-autocomplete", true) - owner.AddTextArgument("The desired new owner.", "[@username]", "") - command.AddCommand(owner) - - info := model.NewAutocompleteData("info", "", "Shows a summary of the current playbook run") - info.AddDynamicListArgument( - "List of channel runs is loading", - "api/v0/runs/runs-autocomplete", true) - command.AddCommand(info) - - timeline := model.NewAutocompleteData("timeline", "", "Shows the timeline for the current playbook run") - timeline.AddDynamicListArgument( - "List of channel runs is loading", - "api/v0/runs/runs-autocomplete", true) - command.AddCommand(timeline) - - todo := model.NewAutocompleteData("todo", "", "Get a list of your assigned tasks") - command.AddCommand(todo) - - settings := model.NewAutocompleteData("settings", "", "Change personal playbook settings") - display := model.NewAutocompleteData(" ", "Display current settings", "") - settings.AddCommand(display) - - weeklyDigest := model.NewAutocompleteData("weekly-digest", "[on/off]", "Turn weekly digest on/off") - weeklyDigestValues := []model.AutocompleteListItem{{ - HelpText: "Turn weekly digest on", - Item: "on", - }, { - HelpText: "Turn weekly digest off", - Item: "off", - }} - weeklyDigest.AddStaticListArgument("", true, weeklyDigestValues) - settings.AddCommand((weeklyDigest)) - - digest := model.NewAutocompleteData("digest", "[on/off]", "Turn digest on/off") - digestValue := []model.AutocompleteListItem{{ - HelpText: "Turn daily digest on", - Item: "on", - }, { - HelpText: "Turn daily digest off", - Item: "off", - }} - digest.AddStaticListArgument("", true, digestValue) - settings.AddCommand(digest) - command.AddCommand(settings) - - if addTestCommands { - test := model.NewAutocompleteData("test", "", "Commands for testing and debugging.") - - testGeneratePlaybooks := model.NewAutocompleteData("create-playbooks", "[total playbooks]", "Create one or more playbooks based on number of playbooks defined") - testGeneratePlaybooks.AddTextArgument("An integer indicating how many playbooks will be generated (at most 5).", "Number of playbooks", "") - test.AddCommand(testGeneratePlaybooks) - - testCreate := model.NewAutocompleteData("create-playbook-run", "[playbook ID] [timestamp] [name]", "Run a playbook with a specific creation date") - testCreate.AddDynamicListArgument("List of playbooks is loading", "api/v0/playbooks/autocomplete", true) - testCreate.AddTextArgument("Date in format 2020-01-31", "Creation timestamp", `/[0-9]{4}-[0-9]{2}-[0-9]{2}/`) - testCreate.AddTextArgument("Name of the playbook run", "Name", "") - test.AddCommand(testCreate) - - testData := model.NewAutocompleteData("bulk-data", "[ongoing] [ended] [days] [seed]", "Generate random test data in bulk") - testData.AddTextArgument("An integer indicating how many ongoing playbook runs will be generated.", "Number of ongoing playbook runs", "") - testData.AddTextArgument("An integer indicating how many ended playbook runs will be generated.", "Number of ended playbook runs", "") - testData.AddTextArgument("An integer n. The playbook runs generated will have a start date between n days ago and today.", "Range of days for the start date", "") - testData.AddTextArgument("An integer in case you need random, but reproducible, results", "Random seed (optional)", "") - test.AddCommand(testData) - - testSelf := model.NewAutocompleteData("self", "", "DESTRUCTIVE ACTION - Perform a series of self tests to ensure everything works as expected.") - test.AddCommand(testSelf) - - command.AddCommand(test) - } - - return command -} - -// Runner handles commands. -type Runner struct { - context *plugin.Context - args *model.CommandArgs - api playbooks.ServicesAPI - poster bot.Poster - playbookRunService app.PlaybookRunService - playbookService app.PlaybookService - configService config.Service - userInfoStore app.UserInfoStore - userInfoTelemetry app.UserInfoTelemetry - permissions *app.PermissionsService -} - -// NewCommandRunner creates a command runner. -func NewCommandRunner(ctx *plugin.Context, - args *model.CommandArgs, - api playbooks.ServicesAPI, - poster bot.Poster, - playbookRunService app.PlaybookRunService, - playbookService app.PlaybookService, - configService config.Service, - userInfoStore app.UserInfoStore, - userInfoTelemetry app.UserInfoTelemetry, - permissions *app.PermissionsService, -) *Runner { - return &Runner{ - context: ctx, - args: args, - api: api, - poster: poster, - playbookRunService: playbookRunService, - playbookService: playbookService, - configService: configService, - userInfoStore: userInfoStore, - userInfoTelemetry: userInfoTelemetry, - permissions: permissions, - } -} - -func (r *Runner) isValid() error { - if r.context == nil || r.args == nil || r.api == nil { - return errors.New("invalid arguments to command.Runner") - } - return nil -} - -func (r *Runner) postCommandResponse(text string) { - post := &model.Post{ - Message: text, - } - r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, post) -} - -func (r *Runner) warnUserAndLogErrorf(format string, args ...interface{}) { - logrus.Errorf(format, args...) - r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, &model.Post{ - Message: "Your request could not be completed. Check the system logs for more information.", - }) -} - -func (r *Runner) actionRun(args []string) { - clientID := "" - if len(args) > 0 { - clientID = args[0] - } - - postID := "" - if len(args) == 2 { - postID = args[1] - } - - requesterInfo := app.RequesterInfo{ - UserID: r.args.UserId, - TeamID: r.args.TeamId, - IsAdmin: app.IsSystemAdmin(r.args.UserId, r.api), - } - - playbooksResults, err := r.playbookService.GetPlaybooksForTeam(requesterInfo, r.args.TeamId, - app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Direction: app.DirectionAsc, - Page: 0, - PerPage: app.PerPageDefault, - }) - if err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } - - if err := r.playbookRunService.OpenCreatePlaybookRunDialog(r.args.TeamId, r.args.UserId, r.args.TriggerId, postID, clientID, playbooksResults.Items); err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } -} - -// actionRunPlaybook is intended for scripting use, not use by the end user (they would have -// to type in the correct playbookID). -func (r *Runner) actionRunPlaybook(args []string) { - if len(args) != 2 { - r.postCommandResponse("Usage: `/playbook run-playbook `") - return - } - - playbookID := args[0] - clientID := args[1] - - requesterInfo := app.RequesterInfo{ - UserID: r.args.UserId, - TeamID: r.args.TeamId, - IsAdmin: app.IsSystemAdmin(r.args.UserId, r.api), - } - - // Using the GetPlaybooksForTeam so that requesterInfo and the expected security restrictions - // are respected. - playbooksResults, err := r.playbookService.GetPlaybooksForTeam(requesterInfo, r.args.TeamId, - app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Direction: app.DirectionAsc, - Page: 0, - PerPage: app.PerPageDefault, - }) - if err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } - - var playbook []app.Playbook - for _, pb := range playbooksResults.Items { - if pb.ID == playbookID { - playbook = append(playbook, pb) - break - } - } - if len(playbook) == 0 { - r.postCommandResponse("Playbook not found for id: " + playbookID) - return - } - - if err := r.playbookRunService.OpenCreatePlaybookRunDialog(r.args.TeamId, r.args.UserId, r.args.TriggerId, "", clientID, playbook); err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } -} - -func (r *Runner) actionCheck(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if !multipleRuns && len(args) != 2 { - r.postCommandResponse("Command expects two arguments: the checklist number and the item number.") - return - } - - if multipleRuns && len(args) != 3 { - r.postCommandResponse("Command expects three arguments: the run number, the checklist number and the item number.") - return - } - - run := 0 - index := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[index]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - index++ - } - - checklist, err := strconv.Atoi(args[index]) - index++ - if err != nil { - r.postCommandResponse("Error parsing the argument. Must be a number.") - return - } - - item, err := strconv.Atoi(args[index]) - if err != nil { - r.postCommandResponse("Error parsing the argument. Must be a number.") - return - } - - if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil { - r.postCommandResponse("Become a participant to interact with this run.") - return - } - - err = r.playbookRunService.ToggleCheckedState(playbookRuns[run].ID, r.args.UserId, checklist, item) - if err != nil { - r.warnUserAndLogErrorf("Error checking/unchecking item: %v", err) - } -} - -func (r *Runner) actionAddChecklistItem(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if !multipleRuns && len(args) < 1 { - r.postCommandResponse("Command expects one argument: the checklist number.") - return - } - - if multipleRuns && len(args) < 2 { - r.postCommandResponse("Command expects two arguments: the run number and the checklist number.") - return - } - - run := 0 - index := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[index]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - index++ - } - - checklist, err := strconv.Atoi(args[index]) - index++ - if err != nil { - r.postCommandResponse("Error parsing the argument. Must be a number.") - return - } - - if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil { - r.postCommandResponse("Become a participant to interact with this run.") - return - } - - // If we didn't get the item's text, then use the interactive dialog - if len(args) == index { - if err := r.playbookRunService.OpenAddChecklistItemDialog(r.args.TriggerId, r.args.UserId, playbookRuns[run].ID, checklist); err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } - return - } - - combineargs := strings.Join(args[index:], " ") - if err := r.playbookRunService.AddChecklistItem(playbookRuns[run].ID, r.args.UserId, checklist, app.ChecklistItem{ - Title: combineargs, - }); err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } -} - -func (r *Runner) actionRemoveChecklistItem(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if !multipleRuns && len(args) != 2 { - r.postCommandResponse("Command expects two arguments: the checklist number and the item number.") - return - } - - if multipleRuns && len(args) != 3 { - r.postCommandResponse("Command expects three arguments: the run number, the checklist number and the item number.") - return - } - - run := 0 - index := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[index]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - index++ - } - - checklist, err := strconv.Atoi(args[index]) - index++ - if err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - - item, err := strconv.Atoi(args[index]) - if err != nil { - r.postCommandResponse("Error parsing the second argument. Must be a number.") - return - } - - if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil { - r.postCommandResponse("Become a participant to interact with this run.") - return - } - - err = r.playbookRunService.RemoveChecklistItem(playbookRuns[run].ID, r.args.UserId, checklist, item) - if err != nil { - r.warnUserAndLogErrorf("Error removing item: %v", err) - } -} - -func (r *Runner) actionOwner(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - extraArg := 0 - // if channel has multiple runs, we require additional argument: run number - if multipleRuns { - extraArg = 1 - } - - switch len(args) - extraArg { - case 0: - r.actionShowOwner(args, playbookRuns) - case 1: - r.actionChangeOwner(args, playbookRuns) - default: - r.postCommandResponse("/playbook owner expects at most one argument.") - } -} - -func (r *Runner) actionShowOwner(args []string, playbookRuns []app.PlaybookRun) { - multipleRuns := len(playbookRuns) > 1 - run := 0 - if multipleRuns { - var err error - if run, err = strconv.Atoi(args[0]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - } - - currentPlaybookRun := playbookRuns[run] - ownerUser, err := r.api.GetUserByID(currentPlaybookRun.OwnerUserID) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving owner user: %v", err) - return - } - - r.postCommandResponse(fmt.Sprintf("**@%s** is the current owner for this playbook run.", ownerUser.Username)) -} - -func (r *Runner) actionChangeOwner(args []string, playbookRuns []app.PlaybookRun) { - multipleRuns := len(playbookRuns) > 1 - run := 0 - index := 0 - if multipleRuns { - var err error - if run, err = strconv.Atoi(args[index]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - index++ - } - - targetOwnerUsername := strings.TrimLeft(args[index], "@") - - if err := r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil { - r.postCommandResponse("Become a participant to interact with this run.") - return - } - - currentPlaybookRun := playbookRuns[run] - - targetOwnerUser, err := r.api.GetUserByUsername(targetOwnerUsername) - if errors.Is(err, app.ErrNotFound) { - r.postCommandResponse(fmt.Sprintf("Unable to find user @%s", targetOwnerUsername)) - return - } else if err != nil { - r.warnUserAndLogErrorf("Error finding user @%s: %v", targetOwnerUsername, err) - return - } - - if currentPlaybookRun.OwnerUserID == targetOwnerUser.Id { - r.postCommandResponse(fmt.Sprintf("User @%s is already owner of this playbook run.", targetOwnerUsername)) - return - } - - err = r.playbookRunService.ChangeOwner(currentPlaybookRun.ID, r.args.UserId, targetOwnerUser.Id) - if err != nil { - r.warnUserAndLogErrorf("Failed to change owner to @%s: %v", targetOwnerUsername, err) - return - } -} - -func (r *Runner) actionInfo(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - session, err := r.api.GetSession(r.context.SessionId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving session: %v", err) - return - } - - if !session.IsMobileApp() { - // The RHS was opened by the webapp, so inform the user - r.postCommandResponse("Your playbook run details are already open in the right hand side of the channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if multipleRuns && len(args) == 0 { - r.postCommandResponse("Command expects one argument: the run number.") - return - } - - run := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[0]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - } - - playbookRun := playbookRuns[run] - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err) - return - } - - owner, err := r.api.GetUserByID(playbookRun.OwnerUserID) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving owner user: %v", err) - return - } - - tasks := "" - for _, checklist := range playbookRun.Checklists { - for _, item := range checklist.Items { - icon := ":white_large_square: " - timestamp := "" - if item.State == app.ChecklistItemStateClosed { - icon = ":white_check_mark: " - timestamp = " (" + timeutils.GetTimeForMillis(item.StateModified).Format("15:04 PM") + ")" - } - - tasks += icon + item.Title + timestamp + "\n" - } - } - attachment := &model.SlackAttachment{ - Fields: []*model.SlackAttachmentField{ - {Title: "Name:", Value: fmt.Sprintf("**%s**", strings.Trim(playbookRun.Name, " "))}, - {Title: "Duration:", Value: timeutils.DurationString(timeutils.GetTimeForMillis(playbookRun.CreateAt), time.Now())}, - {Title: "Owner:", Value: fmt.Sprintf("@%s", owner.Username)}, - {Title: "Tasks:", Value: tasks}, - }, - } - - post := &model.Post{ - Props: map[string]interface{}{ - "attachments": []*model.SlackAttachment{attachment}, - }, - } - r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, post) -} - -func (r *Runner) actionFinish(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if multipleRuns && len(args) == 0 { - r.postCommandResponse("Command expects one argument: the run number.") - return - } - - run := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[0]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - } - - r.actionFinishByID([]string{playbookRuns[run].ID}) -} - -func (r *Runner) actionFinishByID(args []string) { - if len(args) == 0 { - r.postCommandResponse("Command expects one argument: the run ID.") - return - } - - if err := r.permissions.RunManageProperties(r.args.UserId, args[0]); err != nil { - if errors.Is(err, app.ErrNoPermissions) { - r.postCommandResponse(fmt.Sprintf("userID `%s` is not an admin or channel member", r.args.UserId)) - return - } - r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err) - return - } - - err := r.playbookRunService.OpenFinishPlaybookRunDialog(args[0], r.args.UserId, r.args.TriggerId) - if err != nil { - r.warnUserAndLogErrorf("Error finishing the playbook run: %v", err) - return - } -} - -func (r *Runner) actionUpdate(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if multipleRuns && len(args) == 0 { - r.postCommandResponse("Command expects one argument: the run number.") - return - } - - run := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[0]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - } - - if err = r.permissions.RunManageProperties(r.args.UserId, playbookRuns[run].ID); err != nil { - if errors.Is(err, app.ErrNoPermissions) { - r.postCommandResponse(fmt.Sprintf("userID `%s` is not an admin or channel member", r.args.UserId)) - return - } - r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err) - return - } - - err = r.playbookRunService.OpenUpdateStatusDialog(playbookRuns[run].ID, r.args.UserId, r.args.TriggerId) - switch { - case errors.Is(err, app.ErrPlaybookRunNotActive): - r.postCommandResponse("This playbook run has already been closed.") - return - case err != nil: - r.warnUserAndLogErrorf("Error: %v", err) - return - } -} - -func (r *Runner) actionAdd(args []string) { - if len(args) != 1 { - r.postCommandResponse("Need to provide a postId") - return - } - - postID := args[0] - if postID == "" { - r.postCommandResponse("Need to provide a postId") - return - } - - requesterInfo, err := app.GetRequesterInfo(r.args.UserId, r.api) - if err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } - - if err := r.playbookRunService.OpenAddToTimelineDialog(requesterInfo, postID, r.args.TeamId, r.args.TriggerId); err != nil { - r.warnUserAndLogErrorf("Error: %v", err) - return - } -} - -func (r *Runner) actionTimeline(args []string) { - playbookRuns, err := r.playbookRunService.GetPlaybookRunsForChannelByUser(r.args.ChannelId, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook runs: %v", err) - return - } - if len(playbookRuns) == 0 { - r.postCommandResponse("This command only works when run from a playbook run channel.") - return - } - - multipleRuns := len(playbookRuns) > 1 - - if multipleRuns && len(args) == 0 { - r.postCommandResponse("Command expects one argument: the run number.") - return - } - - run := 0 - if multipleRuns { - if run, err = strconv.Atoi(args[0]); err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - if run < 0 || run >= len(playbookRuns) { - r.postCommandResponse("Invalid run number") - return - } - } - - playbookRun := playbookRuns[run] - if err != nil { - r.warnUserAndLogErrorf("Error retrieving playbook run: %v", err) - return - } - - if len(playbookRun.TimelineEvents) == 0 { - r.postCommandResponse("There are no timeline events to display.") - return - } - - team, err := r.api.GetTeam(r.args.TeamId) - if err != nil { - r.warnUserAndLogErrorf("Error retrieving team: %v", err) - return - } - postURL := fmt.Sprintf("/%s/pl/", team.Name) - - message := "Timeline for **" + playbookRun.Name + "**:\n\n" + - "|Event Time | Since Reported | Event |\n" + - "|:----------|:---------------|:------|\n" - - var reported time.Time - for _, e := range playbookRun.TimelineEvents { - if e.EventType == app.PlaybookRunCreated { - reported = timeutils.GetTimeForMillis(e.EventAt) - break - } - } - for _, e := range playbookRun.TimelineEvents { - if e.EventType == app.AssigneeChanged || - e.EventType == app.TaskStateModified || - e.EventType == app.RanSlashCommand { - continue - } - - timeLink := timeutils.GetTimeForMillis(e.EventAt).Format("Jan 2 15:04") - if e.PostID != "" { - timeLink = " [" + timeLink + "](" + postURL + e.PostID + ") " - } - message += "|" + timeLink + "|" + r.timeSince(e, reported) + "|" + r.summaryMessage(e) + "|\n" - } - - r.poster.EphemeralPost(r.args.UserId, r.args.ChannelId, &model.Post{Message: message}) -} - -func (r *Runner) summaryMessage(event app.TimelineEvent) string { - var username string - user, err := r.api.GetUserByID(event.SubjectUserID) - if err == nil { - username = user.Username - } - - switch event.EventType { - case app.PlaybookRunCreated: - return "Run started by @" + username - case app.StatusUpdated: - if event.Summary == "" { - return "@" + username + " posted a status update" - } - return "@" + username + " changed status from " + event.Summary - case app.OwnerChanged: - return "Owner changes from " + event.Summary - case app.TaskStateModified: - return "@" + username + " " + event.Summary - case app.AssigneeChanged: - return "@" + username + " " + event.Summary - case app.RanSlashCommand: - return "@" + username + " " + event.Summary - case app.PublishedRetrospective: - return "@" + username + " published retrospective" - case app.CanceledRetrospective: - return "@" + username + " canceled retrospective" - default: - return event.Summary - } -} - -func (r *Runner) timeSince(event app.TimelineEvent, reported time.Time) string { - if event.EventType == app.PlaybookRunCreated { - return "" - } - eventAt := timeutils.GetTimeForMillis(event.EventAt) - if reported.Before(eventAt) { - return timeutils.DurationString(reported, eventAt) - } - return "-" + timeutils.DurationString(eventAt, reported) -} - -func (r *Runner) actionTodo() { - if err := r.playbookRunService.EphemeralPostTodoDigestToUser(r.args.UserId, r.args.ChannelId, true, true); err != nil { - r.warnUserAndLogErrorf("Error getting tasks and runs digest: %v", err) - } -} - -func (r *Runner) actionSettings(args []string) { - settingsHelpText := "###### Playbooks Personal Settings - Slash Command Help\n" + - "* `/playbook settings` - display current settings. \n" + - "* `/playbook settings digest on` - turn daily digest on. \n" + - "* `/playbook settings digest off` - turn daily digest off. \n" + - "* `/playbook settings weekly-digest on` - turn weekly digest on. \n" + - "* `/playbook settings weekly-digest off` - turn weekly digest off. \n" - - if len(args) == 0 { - r.displayCurrentSettings() - return - } - - isDigest := args[0] == "digest" || args[0] == "weekly-digest" - - if len(args) != 2 || !isDigest || (args[1] != "on" && args[1] != "off") { - r.postCommandResponse(settingsHelpText) - return - } - - info, err := r.userInfoStore.Get(r.args.UserId) - if errors.Is(err, app.ErrNotFound) { - info = app.UserInfo{ - ID: r.args.UserId, - } - } else if err != nil { - r.warnUserAndLogErrorf("Error getting userInfo: %v", err) - return - } - - oldInfo := info - - if args[0] == "weekly-digest" && args[1] == "off" { - info.DisableWeeklyDigest = true - } else if args[0] == "weekly-digest" { - info.DisableWeeklyDigest = false - } else if args[0] == "digest" && args[1] == "off" { - info.DisableDailyDigest = true - } else { - info.DisableDailyDigest = false - } - - if err = r.userInfoStore.Upsert(info); err != nil { - r.warnUserAndLogErrorf("Error updating userInfo: %v", err) - return - } - - r.userInfoTelemetry.ChangeDigestSettings(r.args.UserId, oldInfo.DigestNotificationSettings, info.DigestNotificationSettings) - - r.displayCurrentSettings() -} - -func (r *Runner) displayCurrentSettings() { - info, err := r.userInfoStore.Get(r.args.UserId) - if err != nil { - if !errors.Is(err, app.ErrNotFound) { - r.warnUserAndLogErrorf("Error getting userInfo: %v", err) - return - } - } - - dailyDigestSetting := "Daily digest: on" - if info.DisableDailyDigest { - dailyDigestSetting = "Daily digest: off" - } - weeklyDigestSetting := "Weekly digest: on" - if info.DisableWeeklyDigest { - weeklyDigestSetting = "Weekly digest: off" - } - r.postCommandResponse(fmt.Sprintf("###### Playbooks Personal Settings\n- %s, %s", dailyDigestSetting, weeklyDigestSetting)) -} - -func (r *Runner) actionTestSelf(args []string) { - if r.api.GetConfig().ServiceSettings.EnableTesting == nil || - !*r.api.GetConfig().ServiceSettings.EnableTesting { - r.postCommandResponse(helpText) - return - } - - if !r.api.HasPermissionTo(r.args.UserId, model.PermissionManageSystem) { - r.postCommandResponse("Running the self-test is restricted to system administrators.") - return - } - - if len(args) != 3 || args[0] != confirmPrompt || args[1] != "TEST" || args[2] != "SELF" { - r.postCommandResponse("Are you sure you want to self-test (which will nuke the database and delete all data -- instances, configuration)? " + - "All data will be lost. To self-test, type `/playbook test self CONFIRM TEST SELF`") - return - } - - if err := r.playbookRunService.NukeDB(); err != nil { - r.postCommandResponse("There was an error while nuking db. Err: " + err.Error()) - return - } - - shortDescription := "A short description." - longDescription := `A very long description describing the item in a very descriptive way. Now with Markdown syntax! We have *italics* and **bold**. We have [external](http://example.com) and [internal links](/ad-1/playbooks/playbooks). We have even links to channels: ~town-square. And links to users: @sysadmin, @user-1. We do have the usual headings and lists, of course: -## Unordered List -- One -- Two -- Three - -### Ordered List -1. One -2. Two -3. Three - -We also have images: - -![Mattermost logo](/static/icon_152x152.png) - -And... yes, of course, we have emojis - -:muscle: :sunglasses: :tada: :confetti_ball: :balloon: :cowboy_hat_face: :nail_care:` - - testPlaybook := app.Playbook{ - Title: "testing playbook", - TeamID: r.args.TeamId, - Checklists: []app.Checklist{ - { - Title: "Identification", - Items: []app.ChecklistItem{ - { - Title: "Create Jira ticket", - Description: longDescription, - }, - { - Title: "Add on-call team members", - State: app.ChecklistItemStateClosed, - }, - { - Title: "Identify blast radius", - Description: shortDescription, - }, - { - Title: "Identify impacted services", - }, - { - Title: "Collect server data logs", - }, - { - Title: "Identify blast Analyze data logs", - }, - }, - }, - { - Title: "Resolution", - Items: []app.ChecklistItem{ - { - Title: "Align on plan of attack", - }, - { - Title: "Confirm resolution", - }, - }, - }, - { - Title: "Analysis", - Items: []app.ChecklistItem{ - { - Title: "Writeup root-cause analysis", - }, - { - Title: "Review post-mortem", - }, - }, - }, - }, - } - playbookID, err := r.playbookService.Create(testPlaybook, r.args.UserId) - if err != nil { - r.postCommandResponse("There was an error while creating playbook. Err: " + err.Error()) - return - } - - gotplaybook, err := r.playbookService.Get(playbookID) - if err != nil { - r.postCommandResponse(fmt.Sprintf("There was an error while retrieving playbook. ID: %v Err: %v", playbookID, err.Error())) - return - } - - if gotplaybook.Title != testPlaybook.Title { - r.postCommandResponse(fmt.Sprintf("Retrieved playbook is wrong, ID: %v Playbook: %+v", playbookID, gotplaybook)) - return - } - - if gotplaybook.ID == "" { - r.postCommandResponse("Retrieved playbook has a blank ID") - return - } - - gotPlaybooks, err := r.playbookService.GetPlaybooks() - if err != nil { - r.postCommandResponse("There was an error while retrieving all playbooks. Err: " + err.Error()) - return - } - - if len(gotPlaybooks) != 1 || gotPlaybooks[0].Title != testPlaybook.Title { - r.postCommandResponse(fmt.Sprintf("Retrieved playbooks are wrong: %+v", gotPlaybooks)) - return - } - - gotplaybook.Title = "This is an updated title" - if err = r.playbookService.Update(gotplaybook, r.args.UserId); err != nil { - r.postCommandResponse("Unable to update playbook Err:" + err.Error()) - return - } - - gotupdated, err := r.playbookService.Get(playbookID) - if err != nil { - r.postCommandResponse(fmt.Sprintf("There was an error while retrieving playbook. ID: %v Err: %v", playbookID, err.Error())) - return - } - - if gotupdated.Title != gotplaybook.Title { - r.postCommandResponse("Update was ineffective") - return - } - - todeleteid, err := r.playbookService.Create(testPlaybook, r.args.UserId) - if err != nil { - r.postCommandResponse("There was an error while creating playbook. Err: " + err.Error()) - return - } - testPlaybook.ID = todeleteid - if err = r.playbookService.Archive(testPlaybook, r.args.UserId); err != nil { - r.postCommandResponse("There was an error while deleting playbook. Err: " + err.Error()) - return - } - - if deletedPlaybook, _ := r.playbookService.Get(todeleteid); deletedPlaybook.Title != "" { - r.postCommandResponse("Playbook should have been vaporized! Where's the kaboom? There was supposed to be an earth-shattering Kaboom!") - return - } - - playbookRun, err := r.playbookRunService.CreatePlaybookRun(&app.PlaybookRun{ - Name: "Cloud Incident 4739", - TeamID: r.args.TeamId, - OwnerUserID: r.args.UserId, - PlaybookID: gotplaybook.ID, - Checklists: gotplaybook.Checklists, - BroadcastChannelIDs: gotplaybook.BroadcastChannelIDs, - Type: app.RunTypePlaybook, - }, &gotplaybook, r.args.UserId, true) - if err != nil { - r.postCommandResponse("Unable to create test playbook run: " + err.Error()) - return - } - - if err := r.playbookRunService.AddChecklistItem(playbookRun.ID, r.args.UserId, 0, app.ChecklistItem{ - Title: "I should be checked and second", - }); err != nil { - r.postCommandResponse("Unable to add checklist item: " + err.Error()) - return - } - - if err := r.playbookRunService.AddChecklistItem(playbookRun.ID, r.args.UserId, 0, app.ChecklistItem{ - Title: "I should be deleted", - }); err != nil { - r.postCommandResponse("Unable to add checklist item: " + err.Error()) - return - } - - if err := r.playbookRunService.AddChecklistItem(playbookRun.ID, r.args.UserId, 0, app.ChecklistItem{ - Title: "I should not say this.", - State: app.ChecklistItemStateClosed, - }); err != nil { - r.postCommandResponse("Unable to add checklist item: " + err.Error()) - return - } - - if err := r.playbookRunService.ModifyCheckedState(playbookRun.ID, r.args.UserId, app.ChecklistItemStateClosed, 0, 0); err != nil { - r.postCommandResponse("Unable to modify checked state: " + err.Error()) - return - } - - if err := r.playbookRunService.ModifyCheckedState(playbookRun.ID, r.args.UserId, app.ChecklistItemStateOpen, 0, 2); err != nil { - r.postCommandResponse("Unable to modify checked state: " + err.Error()) - return - } - - if err := r.playbookRunService.RemoveChecklistItem(playbookRun.ID, r.args.UserId, 0, 1); err != nil { - r.postCommandResponse("Unable to remove checklist item: " + err.Error()) - return - } - - if err := r.playbookRunService.EditChecklistItem(playbookRun.ID, r.args.UserId, 0, 1, - "I should say this! and be unchecked and first!", "", ""); err != nil { - r.postCommandResponse("Unable to remove checklist item: " + err.Error()) - return - } - - if err := r.playbookRunService.MoveChecklistItem(playbookRun.ID, r.args.UserId, 0, 0, 0, 1); err != nil { - r.postCommandResponse("Unable to remove checklist item: " + err.Error()) - return - } - - r.postCommandResponse("Self test success.") -} - -func (r *Runner) actionTest(args []string) { - if r.api.GetConfig().ServiceSettings.EnableTesting == nil || - !*r.api.GetConfig().ServiceSettings.EnableTesting { - r.postCommandResponse("Setting `EnableTesting` must be set to `true` to run the test command.") - return - } - - if !r.api.HasPermissionTo(r.args.UserId, model.PermissionManageSystem) { - r.postCommandResponse("Running the test command is restricted to system administrators.") - return - } - - if len(args) < 1 { - r.postCommandResponse("The `/playbook test` command needs at least one command.") - return - } - - command := strings.ToLower(args[0]) - var params = []string{} - if len(args) > 1 { - params = args[1:] - } - - switch command { - case "create-playbooks": - r.actionTestGeneratePlaybooks(params) - case "create-playbook-run": - r.actionTestCreate(params) - return - case "bulk-data": - r.actionTestData(params) - case "self": - r.actionTestSelf(params) - default: - r.postCommandResponse(fmt.Sprintf("Command '%s' unknown.", args[0])) - return - } -} - -func (r *Runner) actionTestGeneratePlaybooks(params []string) { - if len(params) < 1 { - r.postCommandResponse("The command expects one parameter: ") - return - } - - numPlaybooks, err := strconv.Atoi(params[0]) - if err != nil { - r.postCommandResponse("Error parsing the first argument. Must be a number.") - return - } - - if numPlaybooks > 5 { - r.postCommandResponse("Maximum number of playbooks is 5") - return - } - - rand.Shuffle(len(dummyListPlaybooks), func(i, j int) { - dummyListPlaybooks[i], dummyListPlaybooks[j] = dummyListPlaybooks[j], dummyListPlaybooks[i] - }) - - playbookIds := make([]string, 0, numPlaybooks) - for i := 0; i < numPlaybooks; i++ { - dummyPlaybook := dummyListPlaybooks[i] - dummyPlaybook.TeamID = r.args.TeamId - dummyPlaybook.Members = []app.PlaybookMember{ - { - UserID: r.args.UserId, - Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin}, - }, - } - newPlaybookID, errCreatePlaybook := r.playbookService.Create(dummyPlaybook, r.args.UserId) - if errCreatePlaybook != nil { - r.warnUserAndLogErrorf("unable to create playbook: %v", err) - return - } - - playbookIds = append(playbookIds, newPlaybookID) - } - - msg := "Playbooks successfully created" - for i, playbookID := range playbookIds { - url := fmt.Sprintf("/playbooks/playbooks/%s", playbookID) - msg += fmt.Sprintf("\n- [%s](%s)", dummyListPlaybooks[i].Title, url) - } - - r.postCommandResponse(msg) -} - -func (r *Runner) actionTestCreate(params []string) { - if len(params) < 3 { - r.postCommandResponse("The command expects three parameters: ") - return - } - - playbookID := params[0] - if !model.IsValidId(playbookID) { - r.postCommandResponse("The first parameter, , must be a valid ID.") - return - } - playbook, err := r.playbookService.Get(playbookID) - if err != nil { - r.postCommandResponse(fmt.Sprintf("The playbook with ID '%s' does not exist.", playbookID)) - return - } - - creationTimestamp, err := time.ParseInLocation("2006-01-02", params[1], time.Now().Location()) - if err != nil { - r.postCommandResponse(fmt.Sprintf("Timestamp '%s' could not be parsed as a date. If you want the playbook run to start on January 2, 2006, the timestamp should be '2006-01-02'.", params[1])) - return - } - - playbookRunName := strings.Join(params[2:], " ") - - playbookRun, err := r.playbookRunService.CreatePlaybookRun( - &app.PlaybookRun{ - Name: playbookRunName, - OwnerUserID: r.args.UserId, - TeamID: r.args.TeamId, - PlaybookID: playbookID, - Checklists: playbook.Checklists, - Type: app.RunTypePlaybook, - }, - &playbook, - r.args.UserId, - true, - ) - - if err != nil { - r.warnUserAndLogErrorf("unable to create playbook run: %v", err) - return - } - - if err = r.playbookRunService.ChangeCreationDate(playbookRun.ID, creationTimestamp); err != nil { - r.warnUserAndLogErrorf("unable to change date of recently created playbook run: %v", err) - return - } - - channel, err := r.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - r.warnUserAndLogErrorf("unable to retrieve information of playbook run's channel: %v", err) - return - } - - r.postCommandResponse(fmt.Sprintf("PlaybookRun successfully created: ~%s.", channel.Name)) -} - -func (r *Runner) actionTestData(params []string) { - if len(params) < 3 { - r.postCommandResponse("`/playbook test bulk-data` expects at least 3 arguments: [ongoing] [ended] [days]. Optionally, a fourth argument can be added: [seed].") - return - } - - ongoing, err := strconv.Atoi(params[0]) - if err != nil { - r.postCommandResponse(fmt.Sprintf("The provided value for ongoing playbook runs, '%s', is not an integer.", params[0])) - return - } - - ended, err := strconv.Atoi(params[1]) - if err != nil { - r.postCommandResponse(fmt.Sprintf("The provided value for ended playbook runs, '%s', is not an integer.", params[1])) - return - } - - days, err := strconv.Atoi((params[2])) - if err != nil { - r.postCommandResponse(fmt.Sprintf("The provided value for days, '%s', is not an integer.", params[2])) - return - } - - if days < 1 { - r.postCommandResponse(fmt.Sprintf("The provided value for days, '%d', is not greater than 0.", days)) - return - } - - begin := time.Now().AddDate(0, 0, -days) - end := time.Now() - - seed := time.Now().Unix() - if len(params) > 3 { - parsedSeed, err := strconv.ParseInt(params[3], 10, 0) - if err != nil { - r.postCommandResponse(fmt.Sprintf("The provided value for the random seed, '%s', is not an integer.", params[3])) - return - } - - seed = parsedSeed - } - - r.generateTestData(ongoing, ended, begin, end, seed) -} - -var fakeCompanyNames = []string{ - "Dach Inc", - "Schuster LLC", - "Kirlin Group", - "Kohler Group", - "Ruelas S.L.", - "Armenta S.L.", - "Vega S.A.", - "Delarosa S.A.", - "Sarabia S.A.", - "Torp - Reilly", - "Heathcote Inc", - "Swift - Bruen", - "Stracke - Lemke", - "Shields LLC", - "Bruen Group", - "Senger - Stehr", - "Krogh - Eide", - "Andresen BA", - "Hagen - Holm", - "Martinsen BA", - "Holm BA", - "Berg BA", - "Fossum RFH", - "Nordskaug - Torp", - "Gran - Lunde", - "Nordby BA", - "Ryan Gruppen", - "Karlsson AB", - "Nilsson HB", - "Karlsson Group", - "Miller - Harber", - "Yost Group", - "Leuschke Group", - "Mertz Group", - "Welch LLC", - "Baumbach Group", - "Ward - Schmitt", - "Romaguera Group", - "Hickle - Kemmer", - "Stewart Corp", -} - -var playbookRunNames = []string{ - "Cluster servers are down", - "API performance degradation", - "Customers unable to login", - "Deployment failed", - "Build failed", - "Build timeout failure", - "Server is unresponsive", - "Server is crashing on start-up", - "MM crashes on start-up", - "Provider is down", - "Database is unresponsive", - "Database servers are down", - "Database replica lag", - "LDAP fails to sync", - "LDAP account unable to login", - "Broken MFA process", - "MFA fails to login users", - "UI is unresponsive", - "Security threat", - "Security breach", - "Customers data breach", - "SLA broken", - "MySQL max connections error", - "Postgres max connections error", - "Elastic Search unresponsive", - "Posts deleted", - "Mentions deleted", - "Replies deleted", - "Cloud server is down", - "Cloud deployment failed", - "Cloud provisioner is down", - "Cloud running out of memory", - "Unable to create new users", - "Installations in crashloop", - "Compliance report timeout", - "RN crash", - "RN out of memory", - "RN performance issues", - "MM fails to start", - "MM HA sync errors", -} - -var dummyListPlaybooks = []app.Playbook{ - { - Title: "Blank Playbook", - Description: "This is an example of an empty playbook", - }, - { - Title: "Test playbook", - RetrospectiveEnabled: true, - StatusUpdateEnabled: true, - Checklists: []app.Checklist{ - { - Title: "Identification", - Items: []app.ChecklistItem{ - { - Title: "Create Jira ticket", - }, - { - Title: "Add on-call team members", - State: app.ChecklistItemStateClosed, - }, - { - Title: "Identify blast radius", - }, - { - Title: "Identify impacted services", - }, - { - Title: "Collect server data logs", - }, - { - Title: "Identify blast Analyze data logs", - }, - }, - }, - { - Title: "Resolution", - Items: []app.ChecklistItem{ - { - Title: "Align on plan of attack", - }, - { - Title: "Confirm resolution", - }, - }, - }, - { - Title: "Analysis", - Items: []app.ChecklistItem{ - { - Title: "Writeup root-cause analysis", - }, - { - Title: "Review post-mortem", - }, - }, - }, - }, - }, - { - Title: "Release 2.4", - RetrospectiveEnabled: true, - StatusUpdateEnabled: true, - Checklists: []app.Checklist{ - { - Title: "Preparation", - Items: []app.ChecklistItem{ - { - Title: "Invite Feature Team to Channel", - Command: "/echo ''", - }, - { - Title: "Acknowledge Alert", - }, - { - Title: "Get Alert Info", - Command: "/announce ~release-checklist", - }, - { - Title: "Invite Escalators", - Command: "/github mvp-2.4", - }, - { - Title: "Determine Priority", - }, - { - Title: "Update Alert Priority", - }, - }, - }, - { - Title: "Meeting", - Items: []app.ChecklistItem{ - { - Title: "Final Testing by QA", - }, - { - Title: "Prepare Deployment Documentation", - }, - { - Title: "Create New Alert for User", - }, - }, - }, - { - Title: "Deployment", - Items: []app.ChecklistItem{ - { - Title: "Database Backup", - }, - { - Title: "Migrate New migration File", - }, - { - Title: "Deploy Backend API", - }, - { - Title: "Deploy Front-end", - }, - { - Title: "Create new tag in gitlab", - }, - }, - }, - }, - }, - { - Title: "Incident #4281", - Description: "There is an error when accessing message from deleted channel", - RetrospectiveEnabled: true, - StatusUpdateEnabled: true, - Checklists: []app.Checklist{ - { - Title: "Prepare the Jira card for this task", - Items: []app.ChecklistItem{ - { - Title: "Create new Jira Card and fill the description", - }, - { - Title: "Set someone to be asignee for this task", - }, - { - Title: "Set story point for this card", - }, - }, - }, - { - Title: "Resolve the issue", - Items: []app.ChecklistItem{ - { - Title: "Check the root cause of the issue", - }, - { - Title: "Fix the bug", - }, - { - Title: "Testing the issue manually by programmer", - }, - }, - }, - { - Title: "QA", - Items: []app.ChecklistItem{ - { - Title: "Create several scenario testing", - }, - { - Title: "Implement it using cypress", - }, - { - Title: "Run the testing and check the result", - }, - }, - }, - { - Title: "Deployment", - Items: []app.ChecklistItem{ - { - Title: "Merge the result to branch 'master'", - }, - { - Title: "Create new Merge Request", - }, - { - Title: "Run deployment pipeline", - }, - { - Title: "Test the result in production", - }, - }, - }, - }, - }, - { - Title: "Playbooks Playbook", - Description: "Sample playbook", - RetrospectiveEnabled: true, - StatusUpdateEnabled: true, - Checklists: []app.Checklist{ - { - Title: "Triage", - Items: []app.ChecklistItem{ - { - Title: "Announce incident type and resources", - }, - { - Title: "Acknowledge alert", - }, - { - Title: "Get alert info", - }, - { - Title: "Invite escalators", - }, - { - Title: "Determine priority", - }, - { - Title: "Update alert priority", - }, - { - Title: "Update alert priority", - }, - { - Title: "Create a JIRA ticket", - Command: "/jira create", - }, - { - Title: "Find out who’s on call", - Command: "/genie whoisoncall", - }, - { - Title: "Announce incident", - }, - { - Title: "Invite on-call lead", - }, - }, - }, { - Title: "Investigation", - Items: []app.ChecklistItem{ - { - Title: "Perform initial investigation", - }, - { - Title: "Escalate to other on-call members (optional)", - }, - { - Title: "Escalate to other engineering teams (optional)", - }, - }, - }, { - Title: "Resolution", - Items: []app.ChecklistItem{ - { - Title: "Close alert", - }, - { - Title: "End the incident", - Command: "/playbook end", - }, - { - Title: "Schedule a post-mortem", - }, - { - Title: "Record post-mortem action items", - }, - { - Title: "Update playbook with learnings", - }, - { - Title: "Export channel message history", - Command: "/export", - }, - { - Title: "Archive this channel", - }, - }, - }, - }, - }, -} - -// generateTestData generates `numActivePlaybookRuns` ongoing playbook runs and -// `numEndedPlaybookRuns` ended playbook runs, whose creation timestamp lies randomly -// between the `begin` and `end` timestamps. -// All playbook runs are created with a playbook randomly picked from the ones the -// user is a member of, and the randomness is controlled by the `seed` parameter -// to create reproducible results if needed. -func (r *Runner) generateTestData(numActivePlaybookRuns, numEndedPlaybookRuns int, begin, end time.Time, seed int64) { - rand.Seed(seed) - - beginMillis := begin.Unix() * 1000 - endMillis := end.Unix() * 1000 - - numPlaybookRuns := numActivePlaybookRuns + numEndedPlaybookRuns - - if numPlaybookRuns == 0 { - r.postCommandResponse("Zero playbook runs created.") - return - } - - timestamps := make([]int64, 0, numPlaybookRuns) - for i := 0; i < numPlaybookRuns; i++ { - timestamp := rand.Int63n(endMillis-beginMillis) + beginMillis - timestamps = append(timestamps, timestamp) - } - - requesterInfo := app.RequesterInfo{ - UserID: r.args.UserId, - TeamID: r.args.TeamId, - IsAdmin: app.IsSystemAdmin(r.args.UserId, r.api), - } - - playbooksResult, err := r.playbookService.GetPlaybooksForTeam(requesterInfo, r.args.TeamId, app.PlaybookFilterOptions{ - Page: 0, - PerPage: app.PerPageDefault, - }) - if err != nil { - r.warnUserAndLogErrorf("Error getting playbooks: %v", err) - return - } - - var playbooks []app.Playbook - if len(playbooksResult.Items) == 0 { - for _, dummyPlaybook := range dummyListPlaybooks { - dummyPlaybook.TeamID = r.args.TeamId - dummyPlaybook.Members = []app.PlaybookMember{ - { - UserID: r.args.UserId, - Roles: []string{app.PlaybookRoleMember, app.PlaybookRoleAdmin}, - }, - } - newPlaybookID, err := r.playbookService.Create(dummyPlaybook, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("unable to create playbook: %v", err) - return - } - - newPlaybook, err := r.playbookService.Get(newPlaybookID) - if err != nil { - r.warnUserAndLogErrorf("Error getting playbook: %v", err) - return - } - - playbooks = append(playbooks, newPlaybook) - } - } else { - playbooks = make([]app.Playbook, 0, len(playbooksResult.Items)) - for _, thePlaybook := range playbooksResult.Items { - wholePlaybook, err := r.playbookService.Get(thePlaybook.ID) - if err != nil { - r.warnUserAndLogErrorf("Error getting playbook: %v", err) - return - } - - playbooks = append(playbooks, wholePlaybook) - } - } - - tableMsg := "| Run name | Created at | Status |\n|- |- |- |\n" - playbookRuns := make([]*app.PlaybookRun, 0, numPlaybookRuns) - for i := 0; i < numPlaybookRuns; i++ { - playbook := playbooks[rand.Intn(len(playbooks))] - - playbookRunName := playbookRunNames[rand.Intn(len(playbookRunNames))] - // Give a company name to 1/3 of the playbook runs created - if rand.Intn(3) == 0 { - companyName := fakeCompanyNames[rand.Intn(len(fakeCompanyNames))] - playbookRunName = fmt.Sprintf("[%s] %s", companyName, playbookRunName) - } - - playbookRun, err := r.playbookRunService.CreatePlaybookRun( - &app.PlaybookRun{ - Name: playbookRunName, - OwnerUserID: r.args.UserId, - TeamID: r.args.TeamId, - PlaybookID: playbook.ID, - Checklists: playbook.Checklists, - RetrospectiveEnabled: playbook.RetrospectiveEnabled, - StatusUpdateEnabled: playbook.StatusUpdateEnabled, - Type: app.RunTypePlaybook, - }, - &playbook, - r.args.UserId, - true, - ) - - if err != nil { - r.warnUserAndLogErrorf("Error creating playbook run: %v", err) - return - } - - createAt := timeutils.GetTimeForMillis(timestamps[i]) - err = r.playbookRunService.ChangeCreationDate(playbookRun.ID, createAt) - if err != nil { - r.warnUserAndLogErrorf("Error changing creation date: %v", err) - return - } - - channel, err := r.api.GetChannelByID(playbookRun.ChannelID) - if err != nil { - r.warnUserAndLogErrorf("Error retrieveing playbook run's channel: %v", err) - return - } - - status := "Ended" - if i >= numEndedPlaybookRuns { - status = "Ongoing" - } - tableMsg += fmt.Sprintf("|~%s|%s|%s|\n", channel.Name, createAt.Format("2006-01-02"), status) - - playbookRuns = append(playbookRuns, playbookRun) - } - - for i := 0; i < numEndedPlaybookRuns; i++ { - err := r.playbookRunService.FinishPlaybookRun(playbookRuns[i].ID, r.args.UserId) - if err != nil { - r.warnUserAndLogErrorf("Error ending the playbook run: %v", err) - return - } - } - - r.postCommandResponse(fmt.Sprintf("The test data was successfully generated:\n\n%s\n", tableMsg)) -} - -func (r *Runner) actionNukeDB(args []string) { - if r.api.GetConfig().ServiceSettings.EnableTesting == nil || - !*r.api.GetConfig().ServiceSettings.EnableTesting { - r.postCommandResponse(helpText) - return - } - - if !r.api.HasPermissionTo(r.args.UserId, model.PermissionManageSystem) { - r.postCommandResponse("Nuking the database is restricted to system administrators.") - return - } - - if len(args) != 2 || args[0] != "CONFIRM" || args[1] != "NUKE" { - r.postCommandResponse("Are you sure you want to nuke the database (delete all data -- instances, configuration)?" + - "All data will be lost. To nuke database, type `/playbook nuke-db CONFIRM NUKE`") - return - } - - if err := r.playbookRunService.NukeDB(); err != nil { - r.warnUserAndLogErrorf("There was an error while nuking db: %v", err) - return - } - r.postCommandResponse("DB has been reset.") -} - -// Execute should be called by the plugin when a command invocation is received from the Mattermost server. -func (r *Runner) Execute() error { - if err := r.isValid(); err != nil { - return err - } - - split := strings.Fields(r.args.Command) - command := split[0] - parameters := []string{} - cmd := "" - if len(split) > 1 { - cmd = split[1] - } - if len(split) > 2 { - parameters = split[2:] - } - - if command != "/playbook" { - return nil - } - - switch cmd { - case "run": - r.actionRun(parameters) - case "run-playbook": - r.actionRunPlaybook(parameters) - case "finish": - r.actionFinish(parameters) - case "finish-by-id": - r.actionFinishByID(parameters) - case "update": - r.actionUpdate(parameters) - case "check": - r.actionCheck(parameters) - case "checkadd": - r.actionAddChecklistItem(parameters) - case "checkremove": - r.actionRemoveChecklistItem(parameters) - case "owner": - r.actionOwner(parameters) - case "info": - r.actionInfo(parameters) - case "add": - r.actionAdd(parameters) - case "timeline": - r.actionTimeline(parameters) - case "todo": - r.actionTodo() - case "settings": - r.actionSettings(parameters) - case "nuke-db": - r.actionNukeDB(parameters) - case "test": - r.actionTest(parameters) - default: - r.postCommandResponse(helpText) - } - - return nil -} diff --git a/server/playbooks/server/config/config.go b/server/playbooks/server/config/config.go deleted file mode 100644 index 0e2de4d2cdc..00000000000 --- a/server/playbooks/server/config/config.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package config - -// Service is the config.Service interface. -// NOTE: for now we are defining this here for simplicity. It will be mocked by multiple consumers, -// so keep the definition in one place -- here. In the future we may move to a -// consumer-defines-the-interface style (and mocks it themselves), but since this is used -// internally, at this point the trade-off is not worth it. -type Service interface { - // GetConfiguration retrieves the active configuration under lock, making it safe to use - // concurrently. The active configuration may change underneath the client of this method, but - // the struct returned by this API call is considered immutable. - GetConfiguration() *Configuration - - // UpdateConfiguration updates the config. Any parts of the config that are persisted in the plugin's - // section in the server's config will be saved to the server. - UpdateConfiguration(f func(*Configuration)) error - - // RegisterConfigChangeListener registers a function that will called when the config might have - // been changed. Returns an id which can be used to unregister the listener. - RegisterConfigChangeListener(listener func()) string - - // UnregisterConfigChangeListener unregisters the listener function identified by id. - UnregisterConfigChangeListener(id string) - - // IsConfiguredForDevelopmentAndTesting returns true when the server has `EnableDeveloper` and - // `EnableTesting` configuration settings enabled. - IsConfiguredForDevelopmentAndTesting() bool - - // IsCloud returns true when the server has a Cloud license. - IsCloud() bool - - // SupportsGivingFeedback returns nil when the nps plugin is installed and enabled, thus enabling giving feedback. - SupportsGivingFeedback() error -} diff --git a/server/playbooks/server/config/configuration.go b/server/playbooks/server/config/configuration.go deleted file mode 100644 index 9531da513f0..00000000000 --- a/server/playbooks/server/config/configuration.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package config - -// Configuration captures the plugin's external configuration as exposed in the Mattermost server -// configuration, as well as values computed from the configuration. Any public fields will be -// deserialized from the Mattermost server configuration in OnConfigurationChange. -// -// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin -// configuration can change at any time, access to the configuration must be synchronized. The -// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire -// struct whenever it changes. You may replace this with whatever strategy you choose. -// -// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep -// copy appropriate for your types. -type Configuration struct { - // PlaybookCreatorsUserIds is a list of users that can edit playbooks - PlaybookCreatorsUserIds []string - - // EnableExperimentalFeatures determines if experimental features are enabled. - EnableExperimentalFeatures bool - - // ** The following are NOT stored on the server - // AdminUserIDs contains a list of user IDs that are allowed - // to administer plugin functions, even if not Mattermost sysadmins. - AllowedUserIDs []string - - // BotUserID used to post messages. - BotUserID string - - // AdminLogLevel is "debug", "info", "warn", or "error". - AdminLogLevel string - - // AdminLogVerbose: set to include full context with admin log messages. - AdminLogVerbose bool -} - -// Clone shallow copies the configuration. Your implementation may require a deep copy if -// your configuration has reference types. -func (c *Configuration) Clone() *Configuration { - var clone = *c - return &clone -} - -func (c *Configuration) serialize() map[string]interface{} { - ret := make(map[string]interface{}) - ret["BotUserID"] = c.BotUserID - return ret -} diff --git a/server/playbooks/server/config/service.go b/server/playbooks/server/config/service.go deleted file mode 100644 index f1254babc55..00000000000 --- a/server/playbooks/server/config/service.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package config - -import ( - "reflect" - "sync" - - "github.com/pkg/errors" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -// const npsPluginID = "com.mattermost.nps" - -// ServiceImpl holds access to the plugin's Configuration. -type ServiceImpl struct { - api playbooks.ServicesAPI - - // configurationLock synchronizes access to the configuration. - configurationLock sync.RWMutex - - // configuration is the active plugin configuration. Consult getConfiguration and - // setConfiguration for usage. - configuration *Configuration - - // configChangeListeners will be notified when the OnConfigurationChange event has been called. - configChangeListeners map[string]func() -} - -// NewConfigService Creates a new ServiceImpl struct. -func NewConfigService(api playbooks.ServicesAPI) *ServiceImpl { - c := &ServiceImpl{} - c.api = api - c.configuration = new(Configuration) - c.configChangeListeners = make(map[string]func()) - - // LoadPluginConfiguration never returns an error, so ignore it. - _ = api.LoadPluginConfiguration(c.configuration) - - return c -} - -// GetConfiguration retrieves the active configuration under lock, making it safe to use -// concurrently. The active configuration may change underneath the client of this method, but -// the struct returned by this API call is considered immutable. -func (c *ServiceImpl) GetConfiguration() *Configuration { - c.configurationLock.RLock() - defer c.configurationLock.RUnlock() - - if c.configuration == nil { - return &Configuration{} - } - - return c.configuration -} - -// UpdateConfiguration updates the config. Any parts of the config that are persisted in the plugin's -// section in the server's config will be saved to the server. -func (c *ServiceImpl) UpdateConfiguration(f func(*Configuration)) error { - c.configurationLock.Lock() - - if c.configuration == nil { - c.configuration = &Configuration{} - } - - oldStorableConfig := c.configuration.serialize() - f(c.configuration) - newStorableConfig := c.configuration.serialize() - // Don't hold the lock longer than necessary, especially since we're calling the api and then listeners. - c.configurationLock.Unlock() - - if !reflect.DeepEqual(oldStorableConfig, newStorableConfig) { - if appErr := c.api.SavePluginConfig(newStorableConfig); appErr != nil { - return errors.New(appErr.Error()) - } - } - - for _, f := range c.configChangeListeners { - f() - } - - return nil -} - -// RegisterConfigChangeListener registers a function that will called when the config might have -// been changed. Returns an id which can be used to unregister the listener. -func (c *ServiceImpl) RegisterConfigChangeListener(listener func()) string { - if c.configChangeListeners == nil { - c.configChangeListeners = make(map[string]func()) - } - - id := model.NewId() - c.configChangeListeners[id] = listener - return id -} - -// UnregisterConfigChangeListener unregisters the listener function identified by id. -func (c *ServiceImpl) UnregisterConfigChangeListener(id string) { - delete(c.configChangeListeners, id) -} - -// OnConfigurationChange is invoked when configuration changes may have been made. -// This method satisfies the interface expected by the server. Embed config.Config in the plugin. -func (c *ServiceImpl) OnConfigurationChange() error { - // Have we been setup by OnActivate? - if c.api == nil { - return nil - } - - var configuration = new(Configuration) - - // Load the public configuration fields from the Mattermost server configuration. - if err := c.api.LoadPluginConfiguration(configuration); err != nil { - return errors.Wrapf(err, "failed to load plugin configuration") - } - - configuration.BotUserID = c.configuration.BotUserID - - c.setConfiguration(configuration) - - for _, f := range c.configChangeListeners { - f() - } - - return nil -} - -// setConfiguration replaces the active configuration under lock. -// -// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not -// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a -// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur. -// -// This method panics if setConfiguration is called with the existing configuration. This almost -// certainly means that the configuration was modified without being cloned and may result in -// an unsafe access. -func (c *ServiceImpl) setConfiguration(configuration *Configuration) { - c.configurationLock.Lock() - defer c.configurationLock.Unlock() - - if configuration != nil && c.configuration == configuration { - // Ignore assignment if the configuration struct is empty. Go will optimize the - // allocation for same to point at the same memory address, breaking the check - // above. - if reflect.ValueOf(*configuration).NumField() == 0 { - return - } - - panic("setConfiguration called with the existing configuration") - } - - c.configuration = configuration -} - -// IsConfiguredForDevelopmentAndTesting returns true when the server has `EnableDeveloper` and -// `EnableTesting` configuration settings enabled. -func (c *ServiceImpl) IsConfiguredForDevelopmentAndTesting() bool { - config := c.api.GetConfig() - - return config != nil && - config.ServiceSettings.EnableTesting != nil && - *config.ServiceSettings.EnableTesting && - config.ServiceSettings.EnableDeveloper != nil && - *config.ServiceSettings.EnableDeveloper -} - -// IsCloud returns true when the server is on cloud, and false otherwise -func (c *ServiceImpl) IsCloud() bool { - license := c.api.GetLicense() - if license == nil || license.Features == nil || license.Features.Cloud == nil { - return false - } - - return *license.Features.Cloud -} - -// SupportsGivingFeedback returns nil when the nps plugin is installed and enabled, thus enabling giving feedback. -func (c *ServiceImpl) SupportsGivingFeedback() error { - //TODO: Do we need this functions? - // pluginState := c.pluginAPIAdapter.GetConfig().PluginSettings.PluginStates[npsPluginID] - - // if pluginState == nil || !pluginState.Enable { - // return errors.New("nps plugin not enabled") - // } - - // pluginStatus, err := c.api.Plugin.GetPluginStatus(npsPluginID) - // if err != nil { - // return fmt.Errorf("failed to query nps plugin status: %w", err) - // } - - // if pluginStatus == nil { - // return errors.New("nps plugin not running") - // } - - return errors.New("can't get nps plugin status") -} diff --git a/server/playbooks/server/e2etest.config.json.sample b/server/playbooks/server/e2etest.config.json.sample deleted file mode 100644 index 14cdc9c4287..00000000000 --- a/server/playbooks/server/e2etest.config.json.sample +++ /dev/null @@ -1,14 +0,0 @@ -// Rename this file (and remove comments) to e2etest.config.json to force some server settings to e2e engine -// -// Note that e2etest.config.json will be ignored in git. -// -// Example: you can use the following json to enable logging (disabled by default due -// to excesive noise) -{ - "LogSettings": { - "ConsoleLevel": "INFO", - "EnableConsole": true, - "ConsoleJson": true, - "EnableFile": false - } -} diff --git a/server/playbooks/server/enterprise/LICENSE b/server/playbooks/server/enterprise/LICENSE deleted file mode 100644 index c698c02052e..00000000000 --- a/server/playbooks/server/enterprise/LICENSE +++ /dev/null @@ -1,39 +0,0 @@ -The Mattermost Source Available License license (the “Source Available License”) -Copyright (c) 2015-present Mattermost - -With regard to the Mattermost Software: - -This software and associated documentation files (the "Software") may only be -used in production, if you (and any entity that you represent) have agreed to, -and are in compliance with, the Mattermost Terms of Service, available at -https://mattermost.com/enterprise-edition-terms/ (the “EE Terms”), or other -agreement governing the use of the Software, as agreed by you and Mattermost, -and otherwise have a valid Mattermost Enterprise E20 subscription for the -correct number of user seats. Subject to the foregoing sentence, you are free -to modify this Software and publish patches to the Software. You agree that -Mattermost and/or its licensors (as applicable) retain all right, title and -interest in and to all such modifications and/or patches, and all such -modifications and/or patches may only be used, copied, modified, displayed, -distributed, or otherwise exploited with a valid Mattermost Enterprise E20 -Edition subscription for the correct number of user seats. Notwithstanding -the foregoing, you may copy and modify the Software for development and testing -purposes, without requiring a subscription. You agree that Mattermost and/or -its licensors (as applicable) retain all right, title and interest in and to -all such modifications. You are not granted any other rights beyond what is -expressly stated herein. Subject to the foregoing, it is forbidden to copy, -merge, publish, distribute, sublicense, and/or sell the Software. - -The full text of this EE License shall be included in all copies or substantial -portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -For all third party components incorporated into the Mattermost Software, those -components are licensed under the original license provided by the owner of the -applicable component. diff --git a/server/playbooks/server/enterprise/license.go b/server/playbooks/server/enterprise/license.go deleted file mode 100644 index 9d264284ad4..00000000000 --- a/server/playbooks/server/enterprise/license.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package enterprise - -import ( - "github.com/mattermost/mattermost/server/v8/playbooks/product/pluginapi" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -type LicenseChecker struct { - api playbooks.ServicesAPI -} - -func NewLicenseChecker(api playbooks.ServicesAPI) *LicenseChecker { - return &LicenseChecker{ - api, - } -} - -// isAtLeastE20Licensed returns true when the server either has an E20 license or is configured for development. -func (e *LicenseChecker) isAtLeastE20Licensed() bool { - config := e.api.GetConfig() - license := e.api.GetLicense() - - return pluginapi.IsE20LicensedOrDevelopment(config, license) -} - -// isAtLeastE10Licensed returns true when the server either has at least an E10 license or is configured for development. -func (e *LicenseChecker) isAtLeastE10Licensed() bool { - config := e.api.GetConfig() - license := e.api.GetLicense() - - return pluginapi.IsE10LicensedOrDevelopment(config, license) -} - -// PlaybookAllowed returns true if the specified playbook is valid with the current license. -func (e *LicenseChecker) PlaybookAllowed(isPlaybookPublic bool) bool { - // Private playbooks are E20-only - return e.isAtLeastE20Licensed() || isPlaybookPublic -} - -// RetrospectiveAllowed returns true if the retrospective feature is allowed with the current license. -func (e *LicenseChecker) RetrospectiveAllowed() bool { - return e.isAtLeastE10Licensed() -} - -// TimelineAllowed returns true if the timeline feature is allowed with the current license. -func (e *LicenseChecker) TimelineAllowed() bool { - return e.isAtLeastE10Licensed() -} - -// StatsAllowed returns true if the stats feature is allowed with the current license. -func (e *LicenseChecker) StatsAllowed() bool { - return e.isAtLeastE20Licensed() -} - -// ChecklistItemDueDateAllowed returns true if setting/editing checklist item due date is allowed. -func (e *LicenseChecker) ChecklistItemDueDateAllowed() bool { - return e.isAtLeastE10Licensed() -} diff --git a/server/playbooks/server/httptools/client.go b/server/playbooks/server/httptools/client.go deleted file mode 100644 index d3b34ab1b4d..00000000000 --- a/server/playbooks/server/httptools/client.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package httptools - -import ( - "net" - "net/http" - "strings" - "time" - "unicode" - - "github.com/mattermost/mattermost/server/v8/platform/services/httpservice" - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" -) - -func MakeClient(api playbooks.ServicesAPI) *http.Client { - return &http.Client{ - Transport: MakeTransport(api), - Timeout: 30 * time.Second, - } -} - -func splitFields(c rune) bool { - return unicode.IsSpace(c) || c == ',' -} - -// Copy paste with adaptations from sercvices/httpservice/httpservice.go in the future that package will be adapted -// to be used by the suite and this should be replaced. -func MakeTransport(api playbooks.ServicesAPI) *httpservice.MattermostTransport { - insecure := api.GetConfig().ServiceSettings.EnableInsecureOutgoingConnections != nil && *api.GetConfig().ServiceSettings.EnableInsecureOutgoingConnections - - allowHost := func(host string) bool { - if api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections == nil { - return false - } - for _, allowed := range strings.FieldsFunc(*api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) { - if host == allowed { - return true - } - } - return false - } - - allowIP := func(ip net.IP) bool { - reservedIP := httpservice.IsReservedIP(ip) - ownIP, err := httpservice.IsOwnIP(ip) - - // If there is an error getting the self-assigned IPs, default to the secure option - if err != nil { - return false - } - - // If it's not a reserved IP and it's not self-assigned IP, accept the IP - if !reservedIP && !ownIP { - return true - } - - if api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections == nil { - return false - } - - // In the case it's the self-assigned IP, enforce that it needs to be explicitly added to the AllowedUntrustedInternalConnections - for _, allowed := range strings.FieldsFunc(*api.GetConfig().ServiceSettings.AllowedUntrustedInternalConnections, splitFields) { - if _, ipRange, err := net.ParseCIDR(allowed); err == nil && ipRange.Contains(ip) { - return true - } - } - return false - } - - return httpservice.NewTransport(insecure, allowHost, allowIP) -} diff --git a/server/playbooks/server/main_test.go b/server/playbooks/server/main_test.go deleted file mode 100644 index 18249ac0f8f..00000000000 --- a/server/playbooks/server/main_test.go +++ /dev/null @@ -1,507 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package server - -import ( - "context" - "encoding/json" - "fmt" - "os" - "testing" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/channels/api4" - sapp "github.com/mattermost/mattermost/server/v8/channels/app" - "github.com/mattermost/mattermost/server/v8/channels/app/request" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/channels/utils" - "github.com/mattermost/mattermost/server/v8/config" - "github.com/mattermost/mattermost/server/v8/playbooks/client" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "gopkg.in/guregu/null.v4" - - _ "github.com/mattermost/mattermost/server/v8/playbooks/product" -) - -func TestMain(m *testing.M) { - // This actually runs the tests - status := m.Run() - - os.Exit(status) -} - -type PermissionsHelper interface { - SaveDefaultRolePermissions() map[string][]string - RestoreDefaultRolePermissions(data map[string][]string) - RemovePermissionFromRole(permission string, roleName string) - AddPermissionToRole(permission string, roleName string) - SetupChannelScheme() *model.Scheme -} - -type serverPermissionsWrapper struct { - api4.TestHelper -} - -type TestEnvironment struct { - T testing.TB - Srv *sapp.Server - A *sapp.App - - Permissions PermissionsHelper - logger mlog.LoggerIFace - - ServerAdminClient *model.Client4 - PlaybooksAdminClient *client.Client - ServerClient *model.Client4 - PlaybooksClient *client.Client - PlaybooksClient2 *client.Client - PlaybooksClientNotInTeam *client.Client - PlaybooksClientGuest *client.Client - - UnauthenticatedPlaybooksClient *client.Client - - BasicTeam *model.Team - BasicTeam2 *model.Team - BasicPublicChannel *model.Channel - BasicPublicChannelPost *model.Post - BasicPrivateChannel *model.Channel - BasicPrivateChannelPost *model.Post - BasicPlaybook *client.Playbook - BasicPrivatePlaybook *client.Playbook - PrivatePlaybookNoMembers *client.Playbook - ArchivedPlaybook *client.Playbook - BasicRun *client.PlaybookRun - AdminUser *model.User - RegularUser *model.User - RegularUser2 *model.User - RegularUserNotInTeam *model.User - GuestUser *model.User -} - -func getEnvWithDefault(name, defaultValue string) string { - if value := os.Getenv(name); value != "" { - return value - } - return defaultValue -} - -func Setup(t *testing.T) *TestEnvironment { - // Ignore any locally defined SiteURL as we intend to host our own. - os.Unsetenv("MM_SERVICESETTINGS_SITEURL") - os.Unsetenv("MM_SERVICESETTINGS_LISTENADDRESS") - - // Ignore any globally defined datasource if a test dsn defined - if os.Getenv("TEST_DATABASE_MYSQL_DSN") != "" || os.Getenv("TEST_DATABASE_POSTGRESQL_DSN") != "" { - t.Log("Unsetting MM_SQLSETTINGS_DATASOURCE since test dsn detected") - os.Unsetenv("MM_SQLSETTINGS_DATASOURCE") - } - - // Environment Settings - driverName := getEnvWithDefault("MM_SQLSETTINGS_DRIVERNAME", "postgres") - sqlSettings := storetest.MakeSqlSettings(driverName, false) - - // Create a test memory store and modify configuration appropriately - configStore := config.NewTestMemoryStore() - config := configStore.Get() - // Force plugins to be disabled since we are in product mode - config.PluginSettings.Enable = model.NewBool(false) - config.ServiceSettings.ListenAddress = model.NewString("localhost:0") - config.TeamSettings.MaxUsersPerTeam = model.NewInt(10000) - config.LocalizationSettings.SetDefaults() - config.SqlSettings = *sqlSettings - config.ServiceSettings.SiteURL = model.NewString("http://testsiteurlplaybooks.mattermost.com/") - config.LogSettings.EnableConsole = model.NewBool(true) - config.LogSettings.EnableFile = model.NewBool(false) - config.LogSettings.ConsoleLevel = model.NewString("INFO") - - // override config with e2etest.config.json if it exists - textConfig, err := os.ReadFile("./e2etest.config.json") - if err == nil { - err = json.Unmarshal(textConfig, config) - if err != nil { - require.NoError(t, err) - } - } - - _, _, err = configStore.Set(config) - require.NoError(t, err) - - // Create a logger to override - testLogger, err := mlog.NewLogger() - require.NoError(t, err) - testLogger.LockConfiguration() - - // Create a server with our specified options - err = utils.TranslationsPreInit() - require.NoError(t, err) - - options := []sapp.Option{ - sapp.ConfigStore(configStore), - } - server, err := sapp.NewServer(options...) - require.NoError(t, err) - _, err = api4.Init(server) - require.NoError(t, err) - err = server.Start() - require.NoError(t, err) - - // Cleanup to run after test is complete - t.Cleanup(func() { - server.Shutdown() - }) - - ap := sapp.New(sapp.ServerConnector(server.Channels())) - - return &TestEnvironment{ - T: t, - Srv: server, - A: ap, - Permissions: &serverPermissionsWrapper{ - TestHelper: api4.TestHelper{ - Server: server, - App: ap, - }, - }, - logger: testLogger, - } -} - -func (e *TestEnvironment) CreateClients() { - e.T.Helper() - - userPassword := "Password123!" - admin, appErr := e.A.CreateUserAsAdmin(request.EmptyContext(e.logger), &model.User{ - Email: "playbooksadmin@example.com", - Username: "playbooksadmin", - Password: userPassword, - }, "") - require.Nil(e.T, appErr) - e.AdminUser = admin - - user, appErr := e.A.CreateUser(request.EmptyContext(e.logger), &model.User{ - Email: "playbooksuser@example.com", - Username: "playbooksuser", - Password: userPassword, - FirstName: "First 1", - LastName: "Last 1", - }) - require.Nil(e.T, appErr) - e.RegularUser = user - - user2, appErr := e.A.CreateUser(request.EmptyContext(e.logger), &model.User{ - Email: "playbooksuser2@example.com", - Username: "playbooksuser2", - Password: userPassword, - FirstName: "First 2", - LastName: "Last 2", - }) - require.Nil(e.T, appErr) - e.RegularUser2 = user2 - - notInTeam, appErr := e.A.CreateUser(request.EmptyContext(e.logger), &model.User{ - Email: "playbooksusernotinteam@example.com", - Username: "playbooksusenotinteam", - Password: userPassword, - }) - require.Nil(e.T, appErr) - e.RegularUserNotInTeam = notInTeam - - siteURL := fmt.Sprintf("http://localhost:%v", e.A.Srv().ListenAddr.Port) - - serverAdminClient := model.NewAPIv4Client(siteURL) - _, _, err := serverAdminClient.Login(context.Background(), admin.Email, userPassword) - require.NoError(e.T, err) - - playbooksAdminClient, err := client.New(serverAdminClient) - require.NoError(e.T, err) - - e.ServerAdminClient = serverAdminClient - e.PlaybooksAdminClient = playbooksAdminClient - - serverClient := model.NewAPIv4Client(siteURL) - _, _, err = serverClient.Login(context.Background(), user.Email, userPassword) - require.NoError(e.T, err) - - playbooksClient, err := client.New(serverClient) - require.NoError(e.T, err) - - unauthServerClient := model.NewAPIv4Client(siteURL) - unauthClient, err := client.New(unauthServerClient) - require.NoError(e.T, err) - - serverClient2 := model.NewAPIv4Client(siteURL) - _, _, err = serverClient2.Login(context.Background(), user2.Email, userPassword) - require.NoError(e.T, err) - - playbooksClient2, err := client.New(serverClient2) - require.NoError(e.T, err) - - serverClientNotInTeam := model.NewAPIv4Client(siteURL) - _, _, err = serverClientNotInTeam.Login(context.Background(), notInTeam.Email, userPassword) - require.NoError(e.T, err) - - playbooksClientNotInTeam, err := client.New(serverClientNotInTeam) - require.NoError(e.T, err) - - e.ServerClient = serverClient - e.PlaybooksClient = playbooksClient - e.PlaybooksClient2 = playbooksClient2 - e.UnauthenticatedPlaybooksClient = unauthClient - e.PlaybooksClientNotInTeam = playbooksClientNotInTeam -} - -func (e *TestEnvironment) CreateBasicServer() { - e.T.Helper() - - team, _, err := e.ServerAdminClient.CreateTeam(context.Background(), &model.Team{ - DisplayName: "basic", - Name: "basic", - Email: "success+playbooks@simulator.amazonses.com", - Type: model.TeamOpen, - }) - require.NoError(e.T, err) - - _, _, err = e.ServerAdminClient.AddTeamMember(context.Background(), team.Id, e.RegularUser.Id) - require.NoError(e.T, err) - _, _, err = e.ServerAdminClient.AddTeamMember(context.Background(), team.Id, e.RegularUser2.Id) - require.NoError(e.T, err) - - pubChannel, _, err := e.ServerAdminClient.CreateChannel(context.Background(), &model.Channel{ - DisplayName: "testpublic1", - Name: "testpublic1", - Type: model.ChannelTypeOpen, - TeamId: team.Id, - }) - require.NoError(e.T, err) - - pubPost, _, err := e.ServerAdminClient.CreatePost(context.Background(), &model.Post{ - UserId: e.AdminUser.Id, - ChannelId: pubChannel.Id, - Message: "this is a public channel post by a system admin", - }) - require.NoError(e.T, err) - - _, _, err = e.ServerAdminClient.AddChannelMember(context.Background(), pubChannel.Id, e.RegularUser.Id) - require.NoError(e.T, err) - - privateChannel, _, err := e.ServerAdminClient.CreateChannel(context.Background(), &model.Channel{ - DisplayName: "testprivate1", - Name: "testprivate1", - Type: model.ChannelTypePrivate, - TeamId: team.Id, - }) - require.NoError(e.T, err) - - privatePost, _, err := e.ServerAdminClient.CreatePost(context.Background(), &model.Post{ - UserId: e.AdminUser.Id, - ChannelId: privateChannel.Id, - Message: "this is a private channel post by a system admin", - }) - require.NoError(e.T, err) - - e.BasicTeam = team - e.BasicPublicChannel = pubChannel - e.BasicPublicChannelPost = pubPost - e.BasicPrivateChannel = privateChannel - e.BasicPrivateChannelPost = privatePost - - // Add a second team to test cross-team features - team2, _, err := e.ServerAdminClient.CreateTeam(context.Background(), &model.Team{ - DisplayName: "second team", - Name: "second-team", - Email: "success+playbooks@simulator.amazonses.com", - Type: model.TeamOpen, - }) - require.NoError(e.T, err) - - _, _, err = e.ServerAdminClient.AddTeamMember(context.Background(), team2.Id, e.RegularUser.Id) - require.NoError(e.T, err) - - e.BasicTeam2 = team2 -} - -func (e *TestEnvironment) CreateBasicPlaybook() { - e.T.Helper() - - e.CreateBasicPublicPlaybook() - e.CreateBasicPrivatePlaybook() -} - -func (e *TestEnvironment) CreateBasicPrivatePlaybook() { - e.T.Helper() - - privateID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybook", - TeamID: e.BasicTeam.Id, - Public: false, - Members: []client.PlaybookMember{ - {UserID: e.RegularUser.Id, Roles: []string{app.PlaybookRoleMember}}, - {UserID: e.AdminUser.Id, Roles: []string{app.PlaybookRoleAdmin, app.PlaybookRoleMember}}, - }, - CreateChannelMemberOnNewParticipant: true, - RemoveChannelMemberOnRemovedParticipant: true, - }) - require.NoError(e.T, err) - - privatePlaybook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), privateID) - require.NoError(e.T, err) - - e.BasicPrivatePlaybook = privatePlaybook -} - -func (e *TestEnvironment) CreateBasicPublicPlaybook() { - - e.T.Helper() - id, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPlaybook", - TeamID: e.BasicTeam.Id, - Public: true, - Members: []client.PlaybookMember{ - {UserID: e.RegularUser.Id, Roles: []string{app.PlaybookRoleMember}}, - {UserID: e.AdminUser.Id, Roles: []string{app.PlaybookRoleAdmin, app.PlaybookRoleMember}}, - }, - Metrics: []client.PlaybookMetricConfig{ - {Title: "testmetric", Type: app.MetricTypeDuration, Target: null.IntFrom(0)}, - }, - CreateChannelMemberOnNewParticipant: true, - RemoveChannelMemberOnRemovedParticipant: true, - }) - require.NoError(e.T, err) - - playbook, err := e.PlaybooksClient.Playbooks.Get(context.Background(), id) - require.NoError(e.T, err) - - e.BasicPlaybook = playbook -} - -func (e *TestEnvironment) CreateBasicRun() { - e.T.Helper() - - run, err := e.PlaybooksClient.PlaybookRuns.Create(context.Background(), client.PlaybookRunCreateOptions{ - Name: "Basic create", - OwnerUserID: e.RegularUser.Id, - TeamID: e.BasicTeam.Id, - PlaybookID: e.BasicPlaybook.ID, - }) - require.NoError(e.T, err) - require.NotNil(e.T, run) - - run, err = e.PlaybooksClient.PlaybookRuns.Get(context.Background(), run.ID) - require.NoError(e.T, err) - require.NotNil(e.T, run) - - e.BasicRun = run -} - -func (e *TestEnvironment) CreateAdditionalPlaybooks() { - e.T.Helper() - - privateID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestPrivatePlaybookNoMembers", - TeamID: e.BasicTeam.Id, - Public: false, - }) - require.NoError(e.T, err) - - privatePlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), privateID) - require.NoError(e.T, err) - - e.PrivatePlaybookNoMembers = privatePlaybook - - archivedID, err := e.PlaybooksAdminClient.Playbooks.Create(context.Background(), client.PlaybookCreateOptions{ - Title: "TestArchivedPlaybook", - TeamID: e.BasicTeam.Id, - Public: true, - Members: []client.PlaybookMember{ - {UserID: e.RegularUser.Id, Roles: []string{app.PlaybookRoleMember}}, - {UserID: e.AdminUser.Id, Roles: []string{app.PlaybookRoleAdmin, app.PlaybookRoleMember}}, - }, - }) - require.NoError(e.T, err) - - err = e.PlaybooksAdminClient.Playbooks.Archive(context.Background(), archivedID) - require.NoError(e.T, err) - - archivedPlaybook, err := e.PlaybooksAdminClient.Playbooks.Get(context.Background(), archivedID) - require.NoError(e.T, err) - - e.ArchivedPlaybook = archivedPlaybook -} - -func (e *TestEnvironment) CreateGuest() { - cfg := e.Srv.Config() - cfg.GuestAccountsSettings.Enable = model.NewBool(true) - _, _, err := e.ServerAdminClient.UpdateConfig(context.Background(), cfg) - require.NoError(e.T, err) - - userPassword := "password123!" - guest, appErr := e.A.CreateGuest(request.EmptyContext(e.logger), &model.User{ - Email: "playbookguest@example.com", - Username: "playbookguest", - Password: userPassword, - }) - require.Nil(e.T, appErr) - e.GuestUser = guest - - _, _, err = e.ServerAdminClient.AddTeamMember(context.Background(), e.BasicPublicChannel.TeamId, e.GuestUser.Id) - require.NoError(e.T, err) - - _, _, err = e.ServerAdminClient.AddChannelMember(context.Background(), e.BasicPublicChannel.Id, e.GuestUser.Id) - require.NoError(e.T, err) - - siteURL := fmt.Sprintf("http://localhost:%v", e.A.Srv().ListenAddr.Port) - serverClientGuest := model.NewAPIv4Client(siteURL) - _, _, err = serverClientGuest.Login(context.Background(), e.GuestUser.Email, userPassword) - require.NoError(e.T, err) - - playbooksClientGuest, err := client.New(serverClientGuest) - require.NoError(e.T, err) - e.PlaybooksClientGuest = playbooksClientGuest -} - -func (e *TestEnvironment) RemoveLicence() { - e.Srv.SetLicense(nil) -} - -func (e *TestEnvironment) SetE10Licence() { - license := model.NewTestLicense() - license.SkuShortName = model.LicenseShortSkuE10 - e.Srv.SetLicense(license) -} - -func (e *TestEnvironment) SetE20Licence() { - license := model.NewTestLicense() - license.SkuShortName = model.LicenseShortSkuE20 - e.Srv.SetLicense(license) -} - -func (e *TestEnvironment) CreateBasic() { - e.T.Helper() - - e.CreateClients() - e.CreateBasicServer() - e.SetE20Licence() - e.CreateBasicPlaybook() - e.CreateBasicRun() - e.CreateAdditionalPlaybooks() -} - -// TestTestFramework If this is failing you know the break is not exclusively in your test. -func TestTestFramework(t *testing.T) { - e := Setup(t) - e.CreateBasic() -} - -func requireErrorWithStatusCode(t *testing.T, err error, statusCode int) { - t.Helper() - - require.Error(t, err) - - var errResponse *client.ErrorResponse - require.Truef(t, errors.As(err, &errResponse), "err is not an instance of errResponse: %s", err.Error()) - require.Equal(t, statusCode, errResponse.StatusCode) -} diff --git a/server/playbooks/server/metrics/metrics.go b/server/playbooks/server/metrics/metrics.go deleted file mode 100644 index f983afcd6a0..00000000000 --- a/server/playbooks/server/metrics/metrics.go +++ /dev/null @@ -1,254 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package metrics - -import ( - "os" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" -) - -const ( - MetricsNamespace = "playbooks_plugin" - MetricsSubsystemPlaybooks = "playbooks" - MetricsSubsystemRuns = "runs" - MetricsSubsystemSystem = "system" - - MetricsCloudInstallationLabel = "installationId" -) - -type InstanceInfo struct { - Version string - InstallationID string -} - -// Metrics used to instrumentate metrics in prometheus. -type Metrics struct { - registry *prometheus.Registry - - instance *prometheus.GaugeVec - - playbooksCreatedCount prometheus.Counter - playbooksArchivedCount prometheus.Counter - playbooksRestoredCount prometheus.Counter - runsCreatedCount prometheus.Counter - runsFinishedCount prometheus.Counter - errorsCount prometheus.Counter - - playbooksActiveTotal prometheus.Gauge - runsActiveTotal prometheus.Gauge - remindersOutstandingTotal prometheus.Gauge - retrosOutstandingTotal prometheus.Gauge - followersActiveTotal prometheus.Gauge - participantsActiveTotal prometheus.Gauge -} - -// NewMetrics Factory method to create a new metrics collector. -func NewMetrics(info InstanceInfo) *Metrics { - m := &Metrics{} - - m.registry = prometheus.NewRegistry() - options := collectors.ProcessCollectorOpts{ - Namespace: MetricsNamespace, - } - m.registry.MustRegister(collectors.NewProcessCollector(options)) - m.registry.MustRegister(collectors.NewGoCollector()) - - additionalLabels := map[string]string{} - if info.InstallationID != "" { - additionalLabels[MetricsCloudInstallationLabel] = os.Getenv("MM_CLOUD_INSTALLATION_ID") - } - - m.instance = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "playbook_instance_info", - Help: "Instance information for Playbook.", - ConstLabels: additionalLabels, - }, []string{"Version"}) - m.registry.MustRegister(m.instance) - m.instance.WithLabelValues(info.Version).Set(1) - - m.playbooksCreatedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemPlaybooks, - Name: "playbook_created_count", - Help: "Number of playbooks created since the last launch.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.playbooksCreatedCount) - - m.playbooksArchivedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemPlaybooks, - Name: "playbook_archived_count", - Help: "Number of playbooks archived since the last launch.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.playbooksArchivedCount) - - m.playbooksRestoredCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemPlaybooks, - Name: "playbook_restored_count", - Help: "Number of playbooks restored since the last launch.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.playbooksRestoredCount) - - m.runsCreatedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "runs_created_count", - Help: "Number of runs created since the last launch.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.runsCreatedCount) - - m.runsFinishedCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "runs_finished_count", - Help: "Number of runs finished since the last launch.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.runsFinishedCount) - - m.errorsCount = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemSystem, - Name: "errors_count", - Help: "Number of errors since the last launch.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.errorsCount) - - m.playbooksActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemPlaybooks, - Name: "playbooks_active_total", - Help: "Total number of active playbooks.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.playbooksActiveTotal) - - m.runsActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "runs_active_total", - Help: "Total number of active runs.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.runsActiveTotal) - - m.remindersOutstandingTotal = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "reminders_outstanding_total", - Help: "Total number of outstanding reminders.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.remindersOutstandingTotal) - - m.retrosOutstandingTotal = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "retros_outstanding_total", - Help: "Total number of outstanding retrospective reminders.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.retrosOutstandingTotal) - - m.followersActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "followers_active_total", - Help: "Total number of active followers, including duplicates.", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.followersActiveTotal) - - m.participantsActiveTotal = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: MetricsNamespace, - Subsystem: MetricsSubsystemRuns, - Name: "participants_active_total", - Help: "Total number of active participants (i.e. members of the playbook run channel when the run is active), including duplicates", - ConstLabels: additionalLabels, - }) - m.registry.MustRegister(m.participantsActiveTotal) - return m -} - -func (m *Metrics) IncrementPlaybookCreatedCount(num int) { - if m != nil { - m.playbooksCreatedCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementPlaybookArchivedCount(num int) { - if m != nil { - m.playbooksArchivedCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementPlaybookRestoredCount(num int) { - if m != nil { - m.playbooksRestoredCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementRunsCreatedCount(num int) { - if m != nil { - m.runsCreatedCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementRunsFinishedCount(num int) { - if m != nil { - m.runsFinishedCount.Add(float64(num)) - } -} - -func (m *Metrics) IncrementErrorsCount(num int) { - if m != nil { - m.errorsCount.Add(float64(num)) - } -} - -func (m *Metrics) ObservePlaybooksActiveTotal(count int64) { - if m != nil { - m.playbooksActiveTotal.Set(float64(count)) - } -} - -func (m *Metrics) ObserveRunsActiveTotal(count int64) { - if m != nil { - m.runsActiveTotal.Set(float64(count)) - } -} - -func (m *Metrics) ObserveRemindersOutstandingTotal(count int64) { - if m != nil { - m.remindersOutstandingTotal.Set(float64(count)) - } -} - -func (m *Metrics) ObserveRetrosOutstandingTotal(count int64) { - if m != nil { - m.retrosOutstandingTotal.Set(float64(count)) - } -} - -func (m *Metrics) ObserveFollowersActiveTotal(count int64) { - if m != nil { - m.followersActiveTotal.Set(float64(count)) - } -} - -func (m *Metrics) ObserveParticipantsActiveTotal(count int64) { - if m != nil { - m.participantsActiveTotal.Set(float64(count)) - } -} diff --git a/server/playbooks/server/metrics/service.go b/server/playbooks/server/metrics/service.go deleted file mode 100644 index 31b3ea3abf9..00000000000 --- a/server/playbooks/server/metrics/service.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package metrics - -import ( - "net/http" - "time" - - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sirupsen/logrus" -) - -// Service prometheus to run the server. -type Service struct { - *http.Server -} - -type ErrorLoggerWrapper struct { -} - -func (el *ErrorLoggerWrapper) Println(v ...interface{}) { - logrus.Warn("metric server error", v) -} - -// NewMetricsServer factory method to create a new prometheus server. -func NewMetricsServer(address string, metricsService *Metrics) *Service { - return &Service{ - &http.Server{ - ReadTimeout: 30 * time.Second, - Addr: address, - Handler: promhttp.HandlerFor(metricsService.registry, promhttp.HandlerOpts{ - ErrorLog: &ErrorLoggerWrapper{}, - }), - }, - } -} - -// Run will start the prometheus server. -func (h *Service) Run() error { - return errors.Wrap(h.Server.ListenAndServe(), "prometheus ListenAndServe") -} - -// Shutdown will shutdown the prometheus server. -func (h *Service) Shutdown() error { - return errors.Wrap(h.Server.Close(), "prometheus Close") -} diff --git a/server/playbooks/server/playbooks/service_api.go b/server/playbooks/server/playbooks/service_api.go deleted file mode 100644 index 10d17359e83..00000000000 --- a/server/playbooks/server/playbooks/service_api.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -//go:generate mockgen --build_flags= -destination=mocks/mockservicesapi.go -package mocks . ServicesAPI - -package playbooks - -import ( - "database/sql" - - "github.com/gorilla/mux" - - mm_model "github.com/mattermost/mattermost/server/public/model" -) - -const ( - botUsername = "playbooks" - botDisplayname = "Playbooks" - botDescription = "Playbooks bot." - ownerID = "playbooks" -) - -var PlaybooksBot = &mm_model.Bot{ - Username: botUsername, - DisplayName: botDisplayname, - Description: botDescription, - OwnerId: ownerID, -} - -type ServicesAPI interface { - // Channels service - GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error) - GetChannelByID(channelID string) (*mm_model.Channel, error) - GetChannelMember(channelID string, userID string) (*mm_model.ChannelMember, error) - GetChannelsForTeamForUser(teamID string, userID string, includeDeleted bool) (mm_model.ChannelList, error) - GetChannelSidebarCategories(userID, teamID string) (*mm_model.OrderedSidebarCategories, error) - GetChannelMembers(channelID string, page, perPage int) (mm_model.ChannelMembers, error) - CreateChannelSidebarCategory(userID, teamID string, newCategory *mm_model.SidebarCategoryWithChannels) (*mm_model.SidebarCategoryWithChannels, error) - UpdateChannelSidebarCategories(userID, teamID string, categories []*mm_model.SidebarCategoryWithChannels) ([]*mm_model.SidebarCategoryWithChannels, error) - CreateChannel(channel *mm_model.Channel) error - AddMemberToChannel(channelID, userID string) (*mm_model.ChannelMember, error) - AddUserToChannel(channelID, userID, asUserID string) (*mm_model.ChannelMember, error) - UpdateChannelMemberRoles(channelID, userID, newRoles string) (*mm_model.ChannelMember, error) - DeleteChannelMember(channelID, userID string) error - AddChannelMember(channelID, userID string) (*mm_model.ChannelMember, error) - GetDirectChannelOrCreate(userID1, userID2 string) (*mm_model.Channel, error) - - // Post service - CreatePost(post *mm_model.Post) (*mm_model.Post, error) - GetPostsByIds(postIDs []string) ([]*mm_model.Post, error) - SendEphemeralPost(userID string, post *mm_model.Post) - GetPost(postID string) (*mm_model.Post, error) - DeletePost(postID string) (*mm_model.Post, error) - UpdatePost(post *mm_model.Post) (*mm_model.Post, error) - - // User service - GetUserByID(userID string) (*mm_model.User, error) - GetUserByUsername(name string) (*mm_model.User, error) - GetUserByEmail(email string) (*mm_model.User, error) - UpdateUser(user *mm_model.User) (*mm_model.User, error) - GetUsersFromProfiles(options *mm_model.UserGetOptions) ([]*mm_model.User, error) - - // Team service - GetTeamMember(teamID string, userID string) (*mm_model.TeamMember, error) - CreateMember(teamID string, userID string) (*mm_model.TeamMember, error) - GetGroup(groupID string) (*mm_model.Group, error) - GetTeam(teamID string) (*mm_model.Team, error) - GetGroupMemberUsers(groupID string, page, perPage int) ([]*mm_model.User, error) - - // Permissions service - HasPermissionTo(userID string, permission *mm_model.Permission) bool - HasPermissionToTeam(userID, teamID string, permission *mm_model.Permission) bool - HasPermissionToChannel(askingUserID string, channelID string, permission *mm_model.Permission) bool - RolesGrantPermission(roleNames []string, permissionID string) bool - - // Bot service - EnsureBot(bot *mm_model.Bot) (string, error) - - // License service - GetLicense() *mm_model.License - RequestTrialLicense(requesterID string, users int, termsAccepted bool, receiveEmailsAccepted bool) error - - // FileInfoStore service - GetFileInfo(fileID string) (*mm_model.FileInfo, error) - - // Cluster service - PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *mm_model.WebsocketBroadcast) - PublishPluginClusterEvent(ev mm_model.PluginClusterEvent, opts mm_model.PluginClusterEventSendOptions) error - - // Cloud service - GetCloudLimits() (*mm_model.ProductLimits, error) - - // Config service - GetConfig() *mm_model.Config - LoadPluginConfiguration(dest any) error - SavePluginConfig(pluginConfig map[string]any) error - - // KVStore service - KVSetWithOptions(key string, value []byte, options mm_model.PluginKVSetOptions) (bool, error) - Get(key string, o interface{}) error - KVGet(key string) ([]byte, error) - KVDelete(key string) error - KVList(page, count int) ([]string, error) - - // Store service - GetMasterDB() (*sql.DB, error) - DriverName() string - - // System service - GetDiagnosticID() string - GetServerVersion() string - - // Router service - RegisterRouter(sub *mux.Router) - - // Preferences services - GetPreferencesForUser(userID string) (mm_model.Preferences, error) - UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error - DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error - - // Session service - GetSession(sessionID string) (*mm_model.Session, error) - - // Frontend service - OpenInteractiveDialog(dialog mm_model.OpenDialogRequest) error - - // Command service - Execute(command *mm_model.CommandArgs) (*mm_model.CommandResponse, error) - RegisterCommand(command *mm_model.Command) error - - // Threads service - RegisterCollectionAndTopic(collectionType, topicType string) error - - IsEnterpriseReady() bool -} diff --git a/server/playbooks/server/scheduler/scheduler.go b/server/playbooks/server/scheduler/scheduler.go deleted file mode 100644 index 99a2ac1dac4..00000000000 --- a/server/playbooks/server/scheduler/scheduler.go +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package scheduler - -import ( - "fmt" - "time" -) - -type TaskFunc func() - -type ScheduledTask struct { - Name string `json:"name"` - Interval time.Duration `json:"interval"` - Recurring bool `json:"recurring"` - function func() - cancel chan struct{} - cancelled chan struct{} -} - -// WARNING: Tasks will run on every cluster node, so use this carefully. -func CreateTask(name string, function TaskFunc, timeToExecution time.Duration) *ScheduledTask { - return createTask(name, function, timeToExecution, false) -} - -func CreateRecurringTask(name string, function TaskFunc, interval time.Duration) *ScheduledTask { - return createTask(name, function, interval, true) -} - -func createTask(name string, function TaskFunc, interval time.Duration, recurring bool) *ScheduledTask { - task := &ScheduledTask{ - Name: name, - Interval: interval, - Recurring: recurring, - function: function, - cancel: make(chan struct{}), - cancelled: make(chan struct{}), - } - - go func() { - defer close(task.cancelled) - - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - function() - case <-task.cancel: - return - } - - if !task.Recurring { - break - } - } - }() - - return task -} - -func (task *ScheduledTask) Cancel() { - close(task.cancel) - <-task.cancelled -} - -func (task *ScheduledTask) String() string { - return fmt.Sprintf( - "%s\nInterval: %s\nRecurring: %t\n", - task.Name, - task.Interval.String(), - task.Recurring, - ) -} diff --git a/server/playbooks/server/sqlstore/actions.go b/server/playbooks/server/sqlstore/actions.go deleted file mode 100644 index 018ec731842..00000000000 --- a/server/playbooks/server/sqlstore/actions.go +++ /dev/null @@ -1,321 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - "fmt" - - sq "github.com/Masterminds/squirrel" - "github.com/go-sql-driver/mysql" - "github.com/lib/pq" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -// playbookStore is a sql store for playbooks. Use NewPlaybookStore to create it. -type channelActionStore struct { - pluginAPI PluginAPIClient - store *SQLStore - queryBuilder sq.StatementBuilderType - channelActionSelect sq.SelectBuilder -} - -// NewPlaybookStore creates a new store for playbook service. -func NewChannelActionStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.ChannelActionStore { - channelActionSelect := sqlStore.builder. - Select( - "c.ID", - "c.ChannelID", - "c.Enabled", - "c.DeleteAt", - "c.ActionType", - "c.TriggerType", - "c.Payload", - ). - From("IR_ChannelAction c") - - return &channelActionStore{ - pluginAPI: pluginAPI, - store: sqlStore, - queryBuilder: sqlStore.builder, - channelActionSelect: channelActionSelect, - } -} - -// Create creates a new playbook -func (c *channelActionStore) Create(action app.GenericChannelAction) (string, error) { - if action.ID != "" { - return "", errors.New("ID should be empty") - } - action.ID = model.NewId() - - payloadJSON, err := json.Marshal(action.Payload) - if err != nil { - return "", errors.Wrapf(err, "failed to marshal payload json for action id: %q", action.ID) - } - - if len(payloadJSON) > maxJSONLength { - return "", errors.Wrapf(errors.New("invalid data"), "payload json for action id '%s' is too long (max %d)", action.ID, maxJSONLength) - } - - _, err = c.store.execBuilder(c.store.db, sq. - Insert("IR_ChannelAction"). - SetMap(map[string]interface{}{ - "ID": action.ID, - "ChannelID": action.ChannelID, - "Enabled": action.Enabled, - "DeleteAt": action.DeleteAt, - "ActionType": action.ActionType, - "TriggerType": action.TriggerType, - "Payload": payloadJSON, - })) - if err != nil { - return "", errors.Wrap(err, "failed to store new action") - } - - return action.ID, nil -} - -func (c *channelActionStore) Get(id string) (app.GenericChannelAction, error) { - if !model.IsValidId(id) { - return app.GenericChannelAction{}, errors.New("ID is not valid") - } - - var action app.GenericChannelAction - err := c.store.getBuilder(c.store.db, &action, c.channelActionSelect.Where(sq.Eq{"c.ID": id})) - if err == sql.ErrNoRows { - return app.GenericChannelAction{}, errors.Wrapf(app.ErrNotFound, "action does not exist for id %q", id) - } else if err != nil { - return app.GenericChannelAction{}, errors.Wrapf(err, "failed to get action by id %q", id) - } - - return action, nil -} - -type sqlGenericChannelAction struct { - app.GenericChannelActionWithoutPayload - Payload json.RawMessage -} - -func (c *channelActionStore) GetChannelActions(channelID string, options app.GetChannelActionOptions) ([]app.GenericChannelAction, error) { - if !model.IsValidId(channelID) { - return nil, errors.New("ID is not valid") - } - - query := c.channelActionSelect.Where(sq.Eq{"c.ChannelID": channelID}) - - if options.TriggerType != "" { - query = query.Where(sq.Eq{"c.TriggerType": options.TriggerType}) - } - - if options.ActionType != "" { - query = query.Where(sq.Eq{"c.ActionType": options.ActionType}) - } - - sqlActions := []sqlGenericChannelAction{} - err := c.store.selectBuilder(c.store.db, &sqlActions, query) - if err == sql.ErrNoRows { - return nil, errors.Wrapf(app.ErrNotFound, "no actions for channel id %q", channelID) - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get actions for channel id %q", channelID) - } - - actions := make([]app.GenericChannelAction, 0, len(sqlActions)) - for _, sqlAction := range sqlActions { - switch sqlAction.ActionType { - case app.ActionTypeWelcomeMessage: - var welcomePayload app.WelcomeMessagePayload - if err := json.Unmarshal(sqlAction.Payload, &welcomePayload); err != nil { - return nil, errors.Wrapf(err, fmt.Sprintf("unable to unmarshal payload for action with ID %q and type %q", sqlAction.ID, sqlAction.ActionType), channelID) - } - - action := app.GenericChannelAction{ - GenericChannelActionWithoutPayload: sqlAction.GenericChannelActionWithoutPayload, - Payload: welcomePayload, - } - - actions = append(actions, action) - case app.ActionTypePromptRunPlaybook: - var promptRunPlaybookPayload app.PromptRunPlaybookFromKeywordsPayload - if err := json.Unmarshal(sqlAction.Payload, &promptRunPlaybookPayload); err != nil { - return nil, errors.Wrapf(err, fmt.Sprintf("unable to unmarshal payload for action with ID %q and type %q", sqlAction.ID, sqlAction.ActionType), channelID) - } - - action := app.GenericChannelAction{ - GenericChannelActionWithoutPayload: sqlAction.GenericChannelActionWithoutPayload, - Payload: promptRunPlaybookPayload, - } - - actions = append(actions, action) - case app.ActionTypeCategorizeChannel: - var categorizeChannelPayload app.CategorizeChannelPayload - if err := json.Unmarshal(sqlAction.Payload, &categorizeChannelPayload); err != nil { - return nil, errors.Wrapf(err, fmt.Sprintf("unable to unmarshal payload for action with ID %q and type %q", sqlAction.ID, sqlAction.ActionType), channelID) - } - - action := app.GenericChannelAction{ - GenericChannelActionWithoutPayload: sqlAction.GenericChannelActionWithoutPayload, - Payload: categorizeChannelPayload, - } - - actions = append(actions, action) - } - } - - return actions, nil -} - -func (c *channelActionStore) Update(action app.GenericChannelAction) error { - if action.ID == "" { - return errors.New("id should not be empty") - } - - payloadJSON, err := json.Marshal(action.Payload) - if err != nil { - return errors.Wrapf(err, "failed to marshal payload json for action id: %q", action.ID) - } - - _, err = c.store.execBuilder(c.store.db, sq. - Update("IR_ChannelAction"). - SetMap(map[string]interface{}{ - "ID": action.ID, - "ChannelID": action.ChannelID, - "Enabled": action.Enabled, - "DeleteAt": action.DeleteAt, - "ActionType": action.ActionType, - "TriggerType": action.TriggerType, - "Payload": payloadJSON, - }). - Where(sq.Eq{"ID": action.ID})) - - if err != nil { - return errors.Wrapf(err, "failed to update action with id '%s'", action.ID) - } - - return nil -} - -// HasViewed returns true if userID has viewed channelID -func (c *channelActionStore) HasViewedChannel(userID, channelID string) bool { - query := sq.Expr( - `SELECT EXISTS(SELECT * - FROM IR_ViewedChannel as vc - WHERE vc.ChannelID = ? - AND vc.UserID = ?) - `, channelID, userID) - - var exists bool - err := c.store.getBuilder(c.store.db, &exists, query) - if err != nil { - return false - } - - return exists -} - -// SetViewed records that userID has viewed channelID. -func (c *channelActionStore) SetViewedChannel(userID, channelID string) error { - if c.HasViewedChannel(userID, channelID) { - return nil - } - - _, err := c.store.execBuilder(c.store.db, sq. - Insert("IR_ViewedChannel"). - SetMap(map[string]interface{}{ - "ChannelID": channelID, - "UserID": userID, - })) - - if err != nil { - if c.store.db.DriverName() == model.DatabaseDriverMysql { - me, ok := err.(*mysql.MySQLError) - if ok && me.Number == 1062 { - return errors.Wrap(app.ErrDuplicateEntry, err.Error()) - } - } else { - pe, ok := err.(*pq.Error) - if ok && pe.Code == "23505" { - return errors.Wrap(app.ErrDuplicateEntry, err.Error()) - } - } - - return errors.Wrapf(err, "failed to store userID and channelID") - } - - return nil -} - -func (c *channelActionStore) SetMultipleViewedChannel(userIDs []string, channelID string) error { - tx, err := c.store.db.Beginx() - if err != nil { - return errors.Wrap(err, "could not begin transaction") - } - defer c.store.finalizeTransaction(tx) - - // Retrieve the users that have already viewed the channel - var usersToSkip []string - err = c.store.selectBuilder(tx, &usersToSkip, sq. - Select("UserID"). - From("IR_ViewedChannel"). - Where(sq.Eq{ - "UserID": userIDs, - "ChannelID": channelID, - })) - if err != nil && err != sql.ErrNoRows { - return errors.Wrap(err, "unable to retrieve users that have already viewed the channel") - } - - // Build a map out of the previous users for fast lookup - usersToSkipMap := make(map[string]bool) - for _, user := range usersToSkip { - usersToSkipMap[user] = true - } - - // Filter out the users in the map from the original array - usersToSet := []string{} - for _, user := range userIDs { - if !usersToSkipMap[user] { - usersToSet = append(usersToSet, user) - } - } - - if len(usersToSet) == 0 { - return nil - } - - // Set the channelID as viewed for every user in usersToSet - query := sq. - Insert("IR_ViewedChannel"). - Columns("UserID", "ChannelID") - for _, user := range usersToSet { - query = query.Values(user, channelID) - } - - _, err = c.store.execBuilder(c.store.db, query) - if err != nil { - // If there's an error, return a specific one if possible - if c.store.db.DriverName() == model.DatabaseDriverMysql { - me, ok := err.(*mysql.MySQLError) - if ok && me.Number == 1062 { - return errors.Wrap(app.ErrDuplicateEntry, err.Error()) - } - } else { - pe, ok := err.(*pq.Error) - if ok && pe.Code == "23505" { - return errors.Wrap(app.ErrDuplicateEntry, err.Error()) - } - } - - return errors.Wrapf(err, "failed to store userIDs and channelID") - } - - if err = tx.Commit(); err != nil { - return errors.Wrap(err, "could not commit transaction") - } - - return nil -} diff --git a/server/playbooks/server/sqlstore/actions_test.go b/server/playbooks/server/sqlstore/actions_test.go deleted file mode 100644 index ce834aab62f..00000000000 --- a/server/playbooks/server/sqlstore/actions_test.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - mock_sqlstore "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore/mocks" -) - -func setupChannelActionStore(t *testing.T, db *sqlx.DB) app.ChannelActionStore { - mockCtrl := gomock.NewController(t) - - kvAPI := mock_sqlstore.NewMockKVAPI(mockCtrl) - configAPI := mock_sqlstore.NewMockConfigurationAPI(mockCtrl) - pluginAPIClient := PluginAPIClient{ - KV: kvAPI, - Configuration: configAPI, - } - - sqlStore := setupSQLStore(t, db) - - return NewChannelActionStore(pluginAPIClient, sqlStore) -} - -func TestViewedChannel(t *testing.T) { - db := setupTestDB(t) - _ = setupSQLStore(t, db) - channelActionStore := setupChannelActionStore(t, db) - - t.Run("two new users get welcome messages, one old user doesn't", func(t *testing.T) { - channelID := model.NewId() - - oldID := model.NewId() - newID1 := model.NewId() - newID2 := model.NewId() - - err := channelActionStore.SetViewedChannel(oldID, channelID) - require.NoError(t, err) - - // Setting multiple times is okay - err = channelActionStore.SetViewedChannel(oldID, channelID) - require.NoError(t, err) - err = channelActionStore.SetViewedChannel(oldID, channelID) - require.NoError(t, err) - - // new users get welcome messages - hasViewed := channelActionStore.HasViewedChannel(newID1, channelID) - require.False(t, hasViewed) - err = channelActionStore.SetViewedChannel(newID1, channelID) - require.NoError(t, err) - - hasViewed = channelActionStore.HasViewedChannel(newID2, channelID) - require.False(t, hasViewed) - err = channelActionStore.SetViewedChannel(newID2, channelID) - require.NoError(t, err) - - // old user does not - hasViewed = channelActionStore.HasViewedChannel(oldID, channelID) - require.True(t, hasViewed) - - // new users do not, now: - hasViewed = channelActionStore.HasViewedChannel(newID1, channelID) - require.True(t, hasViewed) - hasViewed = channelActionStore.HasViewedChannel(newID2, channelID) - require.True(t, hasViewed) - - var rows int64 - err = db.Get(&rows, "SELECT COUNT(*) FROM IR_ViewedChannel") - require.NoError(t, err) - require.Equal(t, 3, int(rows)) - - // cannot add a duplicate row - if db.DriverName() == model.DatabaseDriverPostgres { - _, err = db.Exec("INSERT INTO IR_ViewedChannel (UserID, ChannelID) VALUES ($1, $2)", oldID, channelID) - require.Error(t, err) - require.Contains(t, err.Error(), "duplicate key value") - } else { - _, err = db.Exec("INSERT INTO IR_ViewedChannel (UserID, ChannelID) VALUES (?, ?)", oldID, channelID) - require.Error(t, err) - require.Contains(t, err.Error(), "Duplicate entry") - } - - err = db.Get(&rows, "SELECT COUNT(*) FROM IR_ViewedChannel") - require.NoError(t, err) - require.Equal(t, 3, int(rows)) - }) -} diff --git a/server/playbooks/server/sqlstore/category.go b/server/playbooks/server/sqlstore/category.go deleted file mode 100644 index 45b0dcb3a1b..00000000000 --- a/server/playbooks/server/sqlstore/category.go +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -// playbookStore is a sql store for playbooks. Use NewPlaybookStore to create it. -type categoryStore struct { - pluginAPI PluginAPIClient - store *SQLStore - queryBuilder sq.StatementBuilderType - categorySelect sq.SelectBuilder - categoryItemSelect sq.SelectBuilder -} - -// Ensure playbookStore implements the playbook.Store interface. -var _ app.CategoryStore = (*categoryStore)(nil) - -func NewCategoryStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.CategoryStore { - categorySelect := sqlStore.builder. - Select( - "c.ID", - "c.Name", - "c.TeamID", - "c.UserID", - "c.Collapsed", - "c.CreateAt", - "c.UpdateAt", - "c.DeleteAt", - ). - From("IR_Category c") - - categoryItemSelect := sqlStore.builder. - Select( - "ci.ItemID", - "ci.Type", - ). - From("IR_Category_Item ci") - - return &categoryStore{ - pluginAPI: pluginAPI, - store: sqlStore, - queryBuilder: sqlStore.builder, - categorySelect: categorySelect, - categoryItemSelect: categoryItemSelect, - } -} - -// Get retrieves a Category. Returns ErrNotFound if not found. -func (c *categoryStore) Get(id string) (app.Category, error) { - if !model.IsValidId(id) { - return app.Category{}, errors.New("ID is not valid") - } - - var category app.Category - err := c.store.getBuilder(c.store.db, &category, c.categorySelect.Where(sq.Eq{"c.ID": id})) - if err == sql.ErrNoRows { - return app.Category{}, errors.Wrapf(app.ErrNotFound, "category does not exist for id %q", id) - } else if err != nil { - return app.Category{}, errors.Wrapf(err, "failed to get category by id %q", id) - } - - items, err := c.getItems(id) - if err != nil { - return app.Category{}, errors.Wrapf(err, "failed to get category items by id %q", id) - } - category.Items = items - return category, nil -} - -func (c *categoryStore) getItems(id string) ([]app.CategoryItem, error) { - var items []app.CategoryItem - var playbookItems []app.CategoryItem - queryPlaybooks := c.queryBuilder. - Select( - "ci.ItemID", - "ci.Type", - "COALESCE(p.title, '') AS Name", - "COALESCE(p.public, false) AS Public", - ). - From("IR_Category_Item ci"). - LeftJoin("IR_Playbook as p on ci.ItemID=p.id"). - Where(sq.And{sq.Eq{"ci.CategoryID": id}, sq.Eq{"ci.Type": "p"}}) - err := c.store.selectBuilder(c.store.db, &playbookItems, queryPlaybooks) - if err == sql.ErrNoRows { - items = []app.CategoryItem{} - } else if err != nil { - return []app.CategoryItem{}, err - } else { - items = playbookItems - } - - var runItems []app.CategoryItem - queryRuns := c.queryBuilder. - Select( - "ci.ItemID", - "ci.Type", - "COALESCE(r.name, '') AS Name", - ). - From("IR_Category_Item ci"). - LeftJoin("IR_Incident as r on ci.ItemID=r.id"). - Where(sq.And{sq.Eq{"ci.CategoryID": id}, sq.Eq{"ci.Type": "r"}}) - err = c.store.selectBuilder(c.store.db, &runItems, queryRuns) - if err == sql.ErrNoRows { - return items, nil - } else if err != nil { - return []app.CategoryItem{}, err - } - items = append(items, runItems...) - return items, nil -} - -// Create creates a new Category -func (c *categoryStore) Create(category app.Category) error { - if _, err := c.store.execBuilder(c.store.db, sq. - Insert("IR_Category"). - SetMap(map[string]interface{}{ - "ID": category.ID, - "Name": category.Name, - "TeamID": category.TeamID, - "UserID": category.UserID, - "Collapsed": category.Collapsed, - "CreateAt": category.CreateAt, - "UpdateAt": category.UpdateAt, - })); err != nil { - return errors.Wrap(err, "failed to store new category") - } - - return nil -} - -// GetCategories retrieves all categories for user for team -func (c *categoryStore) GetCategories(teamID, userID string) ([]app.Category, error) { - query := c.categorySelect.Where(sq.And{sq.Eq{"c.TeamID": teamID}, sq.Eq{"c.UserID": userID}}) - - categories := []app.Category{} - err := c.store.selectBuilder(c.store.db, &categories, query) - if err == sql.ErrNoRows { - return nil, errors.Wrapf(app.ErrNotFound, "no category for team id %q and user id %q", teamID, userID) - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get categories for team id %q and user id %q", teamID, userID) - } - for i, category := range categories { - items, err := c.getItems(category.ID) - if err != nil { - return nil, errors.Wrapf(err, "failed to get category items for category id %q", category.ID) - } - categories[i].Items = items - } - return categories, nil -} - -// Update updates a category -func (c *categoryStore) Update(category app.Category) error { - if _, err := c.store.execBuilder(c.store.db, sq. - Update("IR_Category"). - Set("Name", category.Name). - Set("UpdateAt", category.UpdateAt). - Set("Collapsed", category.Collapsed). - Where(sq.Eq{"ID": category.ID})); err != nil { - return errors.Wrapf(err, "failed to update category with id '%s'", category.ID) - } - return nil -} - -// Delete deletes a category -func (c *categoryStore) Delete(categoryID string) error { - if _, err := c.store.execBuilder(c.store.db, sq. - Update("IR_Category"). - Set("DeleteAt", model.GetMillis()). - Where(sq.Eq{"ID": categoryID})); err != nil { - return errors.Wrapf(err, "failed to delete category with id '%s'", categoryID) - } - return nil -} - -// GetFavoriteCategory returns favorite category -func (c *categoryStore) GetFavoriteCategory(teamID, userID string) (app.Category, error) { - var category app.Category - err := c.store.getBuilder(c.store.db, &category, c.categorySelect.Where(sq.Eq{ - "c.Name": "Favorite", - "c.TeamID": teamID, - "c.UserID": userID, - })) - if err == sql.ErrNoRows { - return app.Category{}, err - } - category.Items, err = c.getItems(category.ID) - if err != nil { - return app.Category{}, errors.Wrap(err, "failed to get Items for category") - } - return category, nil -} - -// createFavoriteCategory creates and returns favorite category -func (c *categoryStore) createFavoriteCategory(teamID, userID string) (app.Category, error) { - now := model.GetMillis() - favCat := app.Category{ - ID: model.NewId(), - Name: "Favorite", - TeamID: teamID, - UserID: userID, - Collapsed: false, - CreateAt: now, - UpdateAt: now, - Items: []app.CategoryItem{}, - } - if err := c.Create(favCat); err != nil { - return app.Category{}, errors.Wrap(err, "can't create favorite category") - } - return favCat, nil -} - -// AddItemToFavoriteCategory adds an item to favorite category, -// if favorite category does not exist it creates one -func (c *categoryStore) AddItemToFavoriteCategory(item app.CategoryItem, teamID, userID string) error { - favoriteCategory, err := c.GetFavoriteCategory(teamID, userID) - if err == sql.ErrNoRows { - // No favorite category, we should create one - if favoriteCategory, err = c.createFavoriteCategory(teamID, userID); err != nil { - return err - } - } else if err != nil { - return errors.Wrap(err, "can't get favorite category") - } - for _, favItem := range favoriteCategory.Items { - if favItem.ItemID == item.ItemID && favItem.Type == item.Type { - return errors.New("Item already is favorite") - } - } - if err := c.AddItemToCategory(item, favoriteCategory.ID); err != nil { - return errors.Wrap(err, "can't add item to favorite category") - } - return nil -} - -// AddItemToCategory adds an item to category -func (c *categoryStore) AddItemToCategory(item app.CategoryItem, categoryID string) error { - if _, err := c.store.execBuilder(c.store.db, sq. - Insert("IR_Category_Item"). - SetMap(map[string]interface{}{ - "CategoryID": categoryID, - "ItemID": item.ItemID, - "Type": item.Type, - })); err != nil { - return errors.Wrap(err, "failed to store item in category") - } - return nil -} - -// DeleteItemFromCategory deletes an item from category -func (c *categoryStore) DeleteItemFromCategory(item app.CategoryItem, categoryID string) error { - if _, err := c.store.execBuilder(c.store.db, sq. - Delete("IR_Category_Item"). - Where(sq.Eq{ - "CategoryID": categoryID, - "ItemID": item.ItemID, - "Type": item.Type, - })); err != nil { - return errors.Wrapf(err, "failed to delete category with item id '%s'", item.ItemID) - } - return nil -} diff --git a/server/playbooks/server/sqlstore/category_test.go b/server/playbooks/server/sqlstore/category_test.go deleted file mode 100644 index 3af9f82e0fc..00000000000 --- a/server/playbooks/server/sqlstore/category_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - mock_sqlstore "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore/mocks" -) - -func setupCategoryStore(t *testing.T, db *sqlx.DB) app.CategoryStore { - mockCtrl := gomock.NewController(t) - - kvAPI := mock_sqlstore.NewMockKVAPI(mockCtrl) - configAPI := mock_sqlstore.NewMockConfigurationAPI(mockCtrl) - pluginAPIClient := PluginAPIClient{ - KV: kvAPI, - Configuration: configAPI, - } - - sqlStore := setupSQLStore(t, db) - - return NewCategoryStore(pluginAPIClient, sqlStore) -} - -func TestCategories(t *testing.T) { - db := setupTestDB(t) - _ = setupSQLStore(t, db) - categoryStore := setupCategoryStore(t, db) - - t.Run("create category, add items, get category", func(t *testing.T) { - userID1 := model.NewId() - teamID1 := model.NewId() - categoryID1 := model.NewId() - - itemID1 := model.NewId() - itemID2 := model.NewId() - - err := categoryStore.Create(app.Category{ - ID: categoryID1, - Name: "cat1", - TeamID: teamID1, - UserID: userID1, - Collapsed: false, - CreateAt: 100, - UpdateAt: 100, - }) - require.NoError(t, err) - - err = categoryStore.AddItemToCategory(app.CategoryItem{ItemID: itemID1, Type: "p"}, categoryID1) - require.NoError(t, err) - - cat, err := categoryStore.Get(categoryID1) - require.NoError(t, err) - - require.Len(t, cat.Items, 1) - - err = categoryStore.AddItemToCategory(app.CategoryItem{ItemID: itemID2, Type: "r"}, categoryID1) - require.NoError(t, err) - - cat, err = categoryStore.Get(categoryID1) - require.NoError(t, err) - - require.Len(t, cat.Items, 2) - }) - - t.Run("create category, delete category, get category", func(t *testing.T) { - userID1 := model.NewId() - teamID1 := model.NewId() - categoryID1 := model.NewId() - - err := categoryStore.Create(app.Category{ - ID: categoryID1, - Name: "cat1", - TeamID: teamID1, - UserID: userID1, - Collapsed: false, - CreateAt: 100, - UpdateAt: 100, - }) - require.NoError(t, err) - - err = categoryStore.Delete(categoryID1) - require.NoError(t, err) - - cat, err := categoryStore.Get(categoryID1) - require.NoError(t, err) - require.NotEqual(t, cat.DeleteAt, 0) - }) - - t.Run("create category, update category, get category", func(t *testing.T) { - userID1 := model.NewId() - teamID1 := model.NewId() - categoryID1 := model.NewId() - - myCategory := app.Category{ - ID: categoryID1, - Name: "cat1", - TeamID: teamID1, - UserID: userID1, - Collapsed: false, - CreateAt: 100, - UpdateAt: 100, - } - err := categoryStore.Create(myCategory) - require.NoError(t, err) - - myCategory.Name = "cat2" - err = categoryStore.Update(myCategory) - require.NoError(t, err) - - cat, err := categoryStore.Get(categoryID1) - require.NoError(t, err) - require.Equal(t, cat.Name, "cat2") - }) -} diff --git a/server/playbooks/server/sqlstore/migrate.go b/server/playbooks/server/sqlstore/migrate.go deleted file mode 100644 index 9ba369051a4..00000000000 --- a/server/playbooks/server/sqlstore/migrate.go +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "context" - "embed" - "fmt" - "path/filepath" - - "github.com/blang/semver" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/morph" - "github.com/mattermost/morph/drivers" - "github.com/mattermost/morph/sources" - "github.com/mattermost/morph/sources/embedded" - "github.com/pkg/errors" - - "github.com/mattermost/morph/drivers/mysql" - "github.com/mattermost/morph/drivers/postgres" -) - -//go:embed migrations -var assets embed.FS - -// RunMigrations will run the migrations (if any). The caller should hold a cluster mutex if there -// is a danger of this being run on multiple servers at once. -func (sqlStore *SQLStore) RunMigrations() error { - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - if err != nil { - return errors.Wrapf(err, "failed to get the current schema version") - } - - // WARNING: Disable morph migrations until proper testing - // if err := sqlStore.runMigrationsWithMorph(); err != nil { - // return fmt.Errorf("failed to complete migrations (with morph): %w", err) - // } - - if currentSchemaVersion.LT(LatestVersion()) { - if err := sqlStore.runMigrationsLegacy(currentSchemaVersion); err != nil { - return errors.Wrapf(err, "failed to complete migrations") - } - } - - return nil -} - -func (sqlStore *SQLStore) runMigrationsLegacy(originalSchemaVersion semver.Version) error { - currentSchemaVersion := originalSchemaVersion - for _, migration := range migrations { - if !currentSchemaVersion.EQ(migration.fromVersion) { - continue - } - - if err := sqlStore.migrate(migration); err != nil { - return err - } - - currentSchemaVersion = migration.toVersion - } - - return nil -} - -func (sqlStore *SQLStore) migrate(migration Migration) (err error) { - tx, err := sqlStore.db.Beginx() - if err != nil { - return errors.Wrap(err, "could not begin transaction") - } - defer sqlStore.finalizeTransaction(tx) - - if err := migration.migrationFunc(tx, sqlStore); err != nil { - return errors.Wrapf(err, "error executing migration from version %s to version %s", migration.fromVersion.String(), migration.toVersion.String()) - } - - if err := sqlStore.SetCurrentVersion(tx, migration.toVersion); err != nil { - return errors.Wrapf(err, "failed to set the current version to %s", migration.toVersion.String()) - } - - if err := tx.Commit(); err != nil { - return errors.Wrap(err, "could not commit transaction") - } - return nil -} - -func (sqlStore *SQLStore) createDriver() (drivers.Driver, error) { - driverName := sqlStore.db.DriverName() - - var driver drivers.Driver - var err error - switch driverName { - case model.DatabaseDriverMysql: - driver, err = mysql.WithInstance(sqlStore.db.DB) - case model.DatabaseDriverPostgres: - driver, err = postgres.WithInstance(sqlStore.db.DB) - default: - err = fmt.Errorf("unsupported database type %s for migration", driverName) - } - return driver, err -} - -func (sqlStore *SQLStore) createSource() (sources.Source, error) { - driverName := sqlStore.db.DriverName() - assetsList, err := assets.ReadDir(filepath.Join("migrations", driverName)) - if err != nil { - return nil, err - } - - assetNamesForDriver := make([]string, len(assetsList)) - for i, entry := range assetsList { - assetNamesForDriver[i] = entry.Name() - } - - src, err := embedded.WithInstance(&embedded.AssetSource{ - Names: assetNamesForDriver, - AssetFunc: func(name string) ([]byte, error) { - return assets.ReadFile(filepath.Join("migrations", driverName, name)) - }, - }) - - return src, err -} - -func (sqlStore *SQLStore) createMorphEngine() (*morph.Morph, error) { - src, err := sqlStore.createSource() - if err != nil { - return nil, err - } - - driver, err := sqlStore.createDriver() - if err != nil { - return nil, err - } - - opts := []morph.EngineOption{ - morph.WithLock("mm-playbooks-lock-key"), - morph.SetMigrationTableName("IR_db_migrations"), - morph.SetStatementTimeoutInSeconds(100000), - } - engine, err := morph.New(context.Background(), driver, src, opts...) - - return engine, err -} - -// WARNING: We don't use morph migration until proper testing -// func (sqlStore *SQLStore) runMigrationsWithMorph() error { -// engine, err := sqlStore.createMorphEngine() -// if err != nil { -// return err -// } -// defer engine.Close() - -// if err := engine.ApplyAll(); err != nil { -// return fmt.Errorf("could not apply migrations: %w", err) -// } - -// return nil -// } diff --git a/server/playbooks/server/sqlstore/migrations.go b/server/playbooks/server/sqlstore/migrations.go deleted file mode 100644 index 9e5ff4a6279..00000000000 --- a/server/playbooks/server/sqlstore/migrations.go +++ /dev/null @@ -1,2477 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "encoding/json" - "fmt" - "strings" - "time" - - sq "github.com/Masterminds/squirrel" - "github.com/blang/semver" - "github.com/go-sql-driver/mysql" - "github.com/jmoiron/sqlx" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -type Migration struct { - fromVersion semver.Version - toVersion semver.Version - migrationFunc func(sqlx.Ext, *SQLStore) error -} - -const MySQLCharset = "DEFAULT CHARACTER SET utf8mb4" - -var migrations = []Migration{ - { - fromVersion: semver.MustParse("0.0.0"), - toVersion: semver.MustParse("0.1.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_System ( - SKey VARCHAR(64) PRIMARY KEY, - SValue VARCHAR(1024) NULL - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_System") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Incident ( - ID VARCHAR(26) PRIMARY KEY, - Name VARCHAR(1024) NOT NULL, - Description VARCHAR(4096) NOT NULL, - IsActive BOOLEAN NOT NULL, - CommanderUserID VARCHAR(26) NOT NULL, - TeamID VARCHAR(26) NOT NULL, - ChannelID VARCHAR(26) NOT NULL UNIQUE, - CreateAt BIGINT NOT NULL, - EndAt BIGINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ActiveStage BIGINT NOT NULL, - PostID VARCHAR(26) NOT NULL DEFAULT '', - PlaybookID VARCHAR(26) NOT NULL DEFAULT '', - ChecklistsJSON TEXT NOT NULL, - INDEX IR_Incident_TeamID (TeamID), - INDEX IR_Incident_TeamID_CommanderUserID (TeamID, CommanderUserID), - INDEX IR_Incident_ChannelID (ChannelID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_Incident") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Playbook ( - ID VARCHAR(26) PRIMARY KEY, - Title VARCHAR(1024) NOT NULL, - Description VARCHAR(4096) NOT NULL, - TeamID VARCHAR(26) NOT NULL, - CreatePublicIncident BOOLEAN NOT NULL, - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ChecklistsJSON TEXT NOT NULL, - NumStages BIGINT NOT NULL DEFAULT 0, - NumSteps BIGINT NOT NULL DEFAULT 0, - INDEX IR_Playbook_TeamID (TeamID), - INDEX IR_PlaybookMember_PlaybookID (ID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_Playbook") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_PlaybookMember ( - PlaybookID VARCHAR(26) NOT NULL REFERENCES IR_Playbook(ID), - MemberID VARCHAR(26) NOT NULL, - INDEX IR_PlaybookMember_PlaybookID (PlaybookID), - INDEX IR_PlaybookMember_MemberID (MemberID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_PlaybookMember") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_System ( - SKey VARCHAR(64) PRIMARY KEY, - SValue VARCHAR(1024) NULL - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_System") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Incident ( - ID TEXT PRIMARY KEY, - Name TEXT NOT NULL, - Description TEXT NOT NULL, - IsActive BOOLEAN NOT NULL, - CommanderUserID TEXT NOT NULL, - TeamID TEXT NOT NULL, - ChannelID TEXT NOT NULL UNIQUE, - CreateAt BIGINT NOT NULL, - EndAt BIGINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ActiveStage BIGINT NOT NULL, - PostID TEXT NOT NULL DEFAULT '', - PlaybookID TEXT NOT NULL DEFAULT '', - ChecklistsJSON JSON NOT NULL - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_Incident") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Playbook ( - ID TEXT PRIMARY KEY, - Title TEXT NOT NULL, - Description TEXT NOT NULL, - TeamID TEXT NOT NULL, - CreatePublicIncident BOOLEAN NOT NULL, - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ChecklistsJSON JSON NOT NULL, - NumStages BIGINT NOT NULL DEFAULT 0, - NumSteps BIGINT NOT NULL DEFAULT 0 - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_Playbook") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_PlaybookMember ( - PlaybookID TEXT NOT NULL REFERENCES IR_Playbook(ID), - MemberID TEXT NOT NULL, - UNIQUE (PlaybookID, MemberID) - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_PlaybookMember") - } - - if _, err := e.Exec(createPGIndex("IR_Incident_TeamID", "IR_Incident", "TeamID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Incident_TeamID") - } - - if _, err := e.Exec(createPGIndex("IR_Incident_TeamID_CommanderUserID", "IR_Incident", "TeamID, CommanderUserID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Incident_TeamID_CommanderUserID") - } - - if _, err := e.Exec(createPGIndex("IR_Incident_ChannelID", "IR_Incident", "ChannelID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Incident_ChannelID") - } - - if _, err := e.Exec(createPGIndex("IR_Playbook_TeamID", "IR_Playbook", "TeamID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Playbook_TeamID") - } - - if _, err := e.Exec(createPGIndex("IR_PlaybookMember_PlaybookID", "IR_PlaybookMember", "PlaybookID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_PlaybookMember_PlaybookID") - } - - if _, err := e.Exec(createPGIndex("IR_PlaybookMember_MemberID", "IR_PlaybookMember", "MemberID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_PlaybookMember_MemberID ") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.1.0"), - toVersion: semver.MustParse("0.2.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // prior to v1.0.0 of the plugin, this migration was used to trigger the data migration from the kvstore - return nil - }, - }, - { - fromVersion: semver.MustParse("0.2.0"), - toVersion: semver.MustParse("0.3.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ActiveStageTitle", "VARCHAR(1024) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ActiveStageTitle to table IR_Incident") - } - - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ActiveStageTitle", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ActiveStageTitle to table IR_Incident") - } - } - - getPlaybookRunsQuery := sqlStore.builder. - Select("ID", "ActiveStage", "ChecklistsJSON"). - From("IR_Incident") - - var playbookRuns []struct { - ID string - ActiveStage int - ChecklistsJSON json.RawMessage - } - if err := sqlStore.selectBuilder(e, &playbookRuns, getPlaybookRunsQuery); err != nil { - return errors.Wrapf(err, "failed getting playbook runs to update their ActiveStageTitle") - } - - for _, playbookRun := range playbookRuns { - var checklists []app.Checklist - if err := json.Unmarshal(playbookRun.ChecklistsJSON, &checklists); err != nil { - return errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: '%s'", playbookRun.ID) - } - - numChecklists := len(checklists) - if numChecklists == 0 { - continue - } - - if playbookRun.ActiveStage < 0 || playbookRun.ActiveStage >= numChecklists { - logrus.WithFields(logrus.Fields{ - "active_stage": playbookRun.ActiveStage, - "playbook_run_id": playbookRun.ID, - "num_checklists": numChecklists, - }).Warn("index out of bounds: setting ActiveStageTitle to the empty string", playbookRun.ActiveStage, playbookRun.ID, numChecklists) - continue - } - - playbookRunUpdate := sqlStore.builder. - Update("IR_Incident"). - Set("ActiveStageTitle", checklists[playbookRun.ActiveStage].Title). - Where(sq.Eq{"ID": playbookRun.ID}) - - if _, err := sqlStore.execBuilder(e, playbookRunUpdate); err != nil { - return errors.Errorf("failed updating the ActiveStageTitle field of playbook run '%s'", playbookRun.ID) - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.3.0"), - toVersion: semver.MustParse("0.4.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_StatusPosts ( - IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID), - PostID VARCHAR(26) NOT NULL, - CONSTRAINT posts_unique UNIQUE (IncidentID, PostID), - INDEX IR_StatusPosts_IncidentID (IncidentID), - INDEX IR_StatusPosts_PostID (PostID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_StatusPosts") - } - - if err := addColumnToMySQLTable(e, "IR_Incident", "ReminderPostID", "VARCHAR(26)"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderPostID to table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Incident", "BroadcastChannelID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "BroadcastChannelID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Playbook") - } - - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_StatusPosts ( - IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID), - PostID TEXT NOT NULL, - UNIQUE (IncidentID, PostID) - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_StatusPosts") - } - - if _, err := e.Exec(createPGIndex("IR_StatusPosts_IncidentID", "IR_StatusPosts", "IncidentID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_StatusPosts_IncidentID") - } - - if _, err := e.Exec(createPGIndex("IR_StatusPosts_PostID", "IR_StatusPosts", "PostID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_StatusPosts_PostID ") - } - - if err := addColumnToPGTable(e, "IR_Incident", "ReminderPostID", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderPostID to table IR_Incident") - } - - if err := addColumnToPGTable(e, "IR_Incident", "BroadcastChannelID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Incident") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "BroadcastChannelID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column BroadcastChannelID to table IR_Playbook") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.4.0"), - toVersion: semver.MustParse("0.5.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "PreviousReminder", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column PreviousReminder to table IR_Incident") - } - if err := addColumnToMySQLTable(e, "IR_Playbook", "ReminderMessageTemplate", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET ReminderMessageTemplate = '' WHERE ReminderMessageTemplate IS NULL"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "ReminderMessageTemplate", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Incident SET ReminderMessageTemplate = '' WHERE ReminderMessageTemplate IS NULL"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Incident") - } - if err := addColumnToMySQLTable(e, "IR_Playbook", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "PreviousReminder", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column PreviousReminder to table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_Playbook", "ReminderMessageTemplate", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "ReminderMessageTemplate", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Playbook", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Playbook") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.5.0"), - toVersion: semver.MustParse("0.6.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "CurrentStatus", "VARCHAR(1024) NOT NULL DEFAULT 'Active'"); err != nil { - return errors.Wrapf(err, "failed adding column CurrentStatus to table IR_Incident") - } - if err := addColumnToMySQLTable(e, "IR_StatusPosts", "Status", "VARCHAR(1024) NOT NULL DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column Status to table IR_StatusPosts") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "CurrentStatus", "TEXT NOT NULL DEFAULT 'Active'"); err != nil { - return errors.Wrapf(err, "failed adding column CurrentStatus to table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_StatusPosts", "Status", "TEXT NOT NULL DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column Status to table IR_StatusPosts") - } - } - if _, err := e.Exec("UPDATE IR_Incident SET CurrentStatus = 'Resolved' WHERE EndAt != 0"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderMessageTemplate to table IR_Incident") - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.6.0"), - toVersion: semver.MustParse("0.7.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_TimelineEvent - ( - ID VARCHAR(26) NOT NULL, - IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID), - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - EventAt BIGINT NOT NULL, - EventType VARCHAR(32) NOT NULL DEFAULT '', - Summary VARCHAR(256) NOT NULL DEFAULT '', - Details VARCHAR(4096) NOT NULL DEFAULT '', - PostID VARCHAR(26) NOT NULL DEFAULT '', - SubjectUserID VARCHAR(26) NOT NULL DEFAULT '', - CreatorUserID VARCHAR(26) NOT NULL DEFAULT '', - INDEX IR_TimelineEvent_ID (ID), - INDEX IR_TimelineEvent_IncidentID (IncidentID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_TimelineEvent") - } - - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_TimelineEvent - ( - ID TEXT NOT NULL, - IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID), - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - EventAt BIGINT NOT NULL, - EventType TEXT NOT NULL DEFAULT '', - Summary TEXT NOT NULL DEFAULT '', - Details TEXT NOT NULL DEFAULT '', - PostID TEXT NOT NULL DEFAULT '', - SubjectUserID TEXT NOT NULL DEFAULT '', - CreatorUserID TEXT NOT NULL DEFAULT '' - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_TimelineEvent") - } - - if _, err := e.Exec(createPGIndex("IR_TimelineEvent_ID", "IR_TimelineEvent", "ID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_TimelineEvent_ID") - } - if _, err := e.Exec(createPGIndex("IR_TimelineEvent_IncidentID", "IR_TimelineEvent", "IncidentID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_TimelineEvent_IncidentID") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.7.0"), - toVersion: semver.MustParse("0.8.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ReporterUserID", "varchar(26) NOT NULL DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ReporterUserID to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ReporterUserID", "TEXT NOT NULL DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ReporterUserID to table IR_Incident") - } - } - if _, err := e.Exec(`UPDATE IR_Incident SET ReporterUserID = CommanderUserID WHERE ReporterUserID = ''`); err != nil { - return errors.Wrapf(err, "Failed to migrate ReporterUserID") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.8.0"), - toVersion: semver.MustParse("0.9.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ConcatenatedInvitedUserIDs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Incident") - } - if _, err := e.Exec("UPDATE IR_Incident SET ConcatenatedInvitedUserIDs = '' WHERE ConcatenatedInvitedUserIDs IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedUserIDs of table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedInvitedUserIDs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET ConcatenatedInvitedUserIDs = '' WHERE ConcatenatedInvitedUserIDs IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedUserIDs of table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "InviteUsersEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column InviteUsersEnabled to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ConcatenatedInvitedUserIDs", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedInvitedUserIDs", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedUserIDs to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Playbook", "InviteUsersEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column InviteUsersEnabled to table IR_Playbook") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.9.0"), - toVersion: semver.MustParse("0.10.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "DefaultCommanderID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "DefaultCommanderID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "DefaultCommanderEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column DefaultCommanderEnabled to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "DefaultCommanderID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Incident") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "DefaultCommanderID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column DefaultCommanderID to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "DefaultCommanderEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column DefaultCommanderEnabled to table IR_Playbook") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.10.0"), - toVersion: semver.MustParse("0.11.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - UPDATE IR_Incident - INNER JOIN Channels ON IR_Incident.ChannelID = Channels.ID - SET IR_Incident.CreateAt = Channels.CreateAt, - IR_Incident.DeleteAt = Channels.DeleteAt - WHERE IR_Incident.CreateAt = 0 - AND IR_Incident.DeleteAt = 0 - AND IR_Incident.ChannelID = Channels.ID - `); err != nil { - return errors.Wrap(err, "failed updating table IR_Incident with Channels' CreateAt and DeleteAt values") - } - } else { - if _, err := e.Exec(` - UPDATE IR_Incident - SET CreateAt = Channels.CreateAt, - DeleteAt = Channels.DeleteAt - FROM Channels - WHERE IR_Incident.CreateAt = 0 - AND IR_Incident.DeleteAt = 0 - AND IR_Incident.ChannelID = Channels.ID - `); err != nil { - return errors.Wrap(err, "failed updating table IR_Incident with Channels' CreateAt and DeleteAt values") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.11.0"), - toVersion: semver.MustParse("0.12.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "AnnouncementChannelID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "AnnouncementChannelID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "AnnouncementChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column AnnouncementChannelEnabled to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "AnnouncementChannelID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Incident") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "AnnouncementChannelID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column AnnouncementChannelID to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "AnnouncementChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column AnnouncementChannelEnabled to table IR_Playbook") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.12.0"), - toVersion: semver.MustParse("0.13.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "WebhookOnCreationURL", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Incident") - } - if _, err := e.Exec("UPDATE IR_Incident SET WebhookOnCreationURL = '' WHERE WebhookOnCreationURL IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column WebhookOnCreationURL of table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnCreationURL", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET WebhookOnCreationURL = '' WHERE WebhookOnCreationURL IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column WebhookOnCreationURL of table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnCreationEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnCreationEnabled to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "WebhookOnCreationURL", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Incident") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnCreationURL", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnCreationURL to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnCreationEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnCreationEnabled to table IR_Playbook") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.13.0"), - toVersion: semver.MustParse("0.14.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ConcatenatedInvitedGroupIDs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Incident") - } - if _, err := e.Exec("UPDATE IR_Incident SET ConcatenatedInvitedGroupIDs = '' WHERE ConcatenatedInvitedGroupIDs IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedGroupIDs of table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedInvitedGroupIDs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET ConcatenatedInvitedGroupIDs = '' WHERE ConcatenatedInvitedGroupIDs IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column ConcatenatedInvitedGroupIDs of table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ConcatenatedInvitedGroupIDs", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedInvitedGroupIDs", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedInvitedGroupIDs to table IR_Playbook") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.14.0"), - toVersion: semver.MustParse("0.15.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "Retrospective", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column Retrospective to table IR_Incident") - } - if _, err := e.Exec("UPDATE IR_Incident SET Retrospective = '' WHERE Retrospective IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column Retrospective of table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "Retrospective", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column Retrospective to table IR_Incident") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.15.0"), - toVersion: semver.MustParse("0.16.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "MessageOnJoin", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Playbook") - } - - if _, err := e.Exec("UPDATE IR_Playbook SET MessageOnJoin = '' WHERE MessageOnJoin IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column MessageOnJoin of table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "MessageOnJoinEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column MessageOnJoinEnabled to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Incident", "MessageOnJoin", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Incident") - } - - if _, err := e.Exec("UPDATE IR_Incident SET MessageOnJoin = '' WHERE MessageOnJoin IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column MessageOnJoin of table IR_Incident") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_ViewedChannel - ( - ChannelID VARCHAR(26) NOT NULL, - UserID VARCHAR(26) NOT NULL, - UNIQUE INDEX IR_ViewedChannel_ChannelID_UserID (ChannelID, UserID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_ViewedChannel") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "MessageOnJoin", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "MessageOnJoinEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column MessageOnJoinEnabled to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Incident", "MessageOnJoin", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column MessageOnJoin to table IR_Incident") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_ViewedChannel - ( - ChannelID TEXT NOT NULL, - UserID TEXT NOT NULL - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_ViewedChannel") - } - - if _, err := e.Exec(createUniquePGIndex("IR_ViewedChannel_ChannelID_UserID", "IR_ViewedChannel", "ChannelID, UserID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_ViewedChannel_ChannelID_UserID") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.16.0"), - toVersion: semver.MustParse("0.17.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectivePublishedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectivePublishedAt to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "RetrospectivePublishedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectivePublishedAt to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.17.0"), - toVersion: semver.MustParse("0.18.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Incident") - } - if err := addColumnToMySQLTable(e, "IR_Playbook", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectiveWasCanceled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveWasCanceled to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_Playbook", "RetrospectiveReminderIntervalSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "RetrospectiveWasCanceled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveWasCanceled to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.18.0"), - toVersion: semver.MustParse("0.19.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "RetrospectiveTemplate", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook") - } - - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "RetrospectiveTemplate", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveReminderIntervalSeconds to table IR_Playbook") - } - } - - if _, err := e.Exec("UPDATE IR_Playbook SET RetrospectiveTemplate = '' WHERE RetrospectiveTemplate IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column RetrospectiveTemplate of table IR_Playbook") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.19.0"), - toVersion: semver.MustParse("0.20.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET WebhookOnStatusUpdateURL = '' WHERE WebhookOnStatusUpdateURL IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column WebhookOnStatusUpdateURL of table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "WebhookOnStatusUpdateEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateEnabled to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Incident", "WebhookOnStatusUpdateURL", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Incident") - } - if _, err := e.Exec("UPDATE IR_Incident SET WebhookOnStatusUpdateURL = '' WHERE WebhookOnStatusUpdateURL IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column WebhookOnStatusUpdateURL of table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "WebhookOnStatusUpdateEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateEnabled to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Incident", "WebhookOnStatusUpdateURL", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column WebhookOnStatusUpdateURL to table IR_Incident") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.20.0"), - toVersion: semver.MustParse("0.21.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedSignalAnyKeywords", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedSignalAnyKeywords to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET ConcatenatedSignalAnyKeywords = '' WHERE ConcatenatedSignalAnyKeywords IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column ConcatenatedSignalAnyKeywords of table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "SignalAnyKeywordsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column SignalAnyKeywordsEnabled to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "UpdateAt", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column UpdateAt to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET UpdateAt = CreateAt"); err != nil { - return errors.Wrapf(err, "failed setting default value in column UpdateAt of table IR_Playbook") - } - if _, err := e.Exec(`ALTER TABLE IR_Playbook ADD INDEX IR_Playbook_UpdateAt (UpdateAt)`); err != nil { - me, ok := err.(*mysql.MySQLError) - if !ok || me.Number != 1061 { // not a Duplicate key name error - return errors.Wrapf(err, "failed creating index IR_Playbook_UpdateAt") - } - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedSignalAnyKeywords", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedSignalAnyKeywords to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "SignalAnyKeywordsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column SignalAnyKeywordsEnabled to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "UpdateAt", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column UpdateAt to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET UpdateAt = CreateAt"); err != nil { - return errors.Wrapf(err, "failed setting default value in column UpdateAt of table IR_Playbook") - } - if _, err := e.Exec(createPGIndex("IR_Playbook_UpdateAt", "IR_Playbook", "UpdateAt")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Playbook_UpdateAt") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.21.0"), - toVersion: semver.MustParse("0.22.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "LastStatusUpdateAt", "BIGINT DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column LastStatusUpdateAt to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "LastStatusUpdateAt", "BIGINT DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column LastStatusUpdateAt to table IR_Incident") - } - } - - var lastUpdateAts []struct { - ID string - LastStatusUpdateAt int64 - } - - // Fill in the LastStatusUpdateAt column as either the most recent status post, or - // if no posts: the playbook run's CreateAt. - lastUpdateAtSelect := sqlStore.builder. - Select("i.Id as ID", "COALESCE(MAX(p.CreateAt), i.CreateAt) as LastStatusUpdateAt"). - From("IR_Incident as i"). - LeftJoin("IR_StatusPosts as sp on i.Id = sp.IncidentId"). - LeftJoin("Posts as p on sp.PostId = p.Id"). - GroupBy("i.Id") - - if err := sqlStore.selectBuilder(e, &lastUpdateAts, lastUpdateAtSelect); err != nil { - return errors.Wrapf(err, "failed getting incidents to update their LastStatusUpdateAt") - } - - for _, row := range lastUpdateAts { - incidentUpdate := sqlStore.builder. - Update("IR_Incident"). - Set("LastStatusUpdateAt", row.LastStatusUpdateAt). - Where(sq.Eq{"ID": row.ID}) - - if _, err := sqlStore.execBuilder(e, incidentUpdate); err != nil { - return errors.Wrapf(err, "failed to update incident's LastStatusUpdateAt for id: %s", row.ID) - } - } - - return nil - }, - }, - { - - fromVersion: semver.MustParse("0.22.0"), - toVersion: semver.MustParse("0.23.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - - if err := addColumnToMySQLTable(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column ExportChannelOnArchiveEnabled to table IR_Incident") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.23.0"), - toVersion: semver.MustParse("0.24.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Incident", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Incident", "CategorizeChannelEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column CategorizeChannelEnabled to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.24.0"), - toVersion: semver.MustParse("0.25.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := renameColumnMySQL(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil { - return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Playbook") - } - - if err := renameColumnMySQL(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled", "BOOLEAN NOT NULL DEFAULT FALSE"); err != nil { - return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Incident") - } - - if err := dropColumnMySQL(e, "IR_StatusPosts", "Status"); err != nil { - return errors.Wrap(err, "failed dropping column Status in table IR_StatusPosts") - } - } else { - if err := renameColumnPG(e, "IR_Playbook", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled"); err != nil { - return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Playbook") - } - - if err := renameColumnPG(e, "IR_Incident", "ExportChannelOnArchiveEnabled", "ExportChannelOnFinishedEnabled"); err != nil { - return errors.Wrap(err, "failed changing column ExportChannelOnArchiveEnabled to ExportChannelOnFinishedEnabled in table IR_Incident") - } - - if err := dropColumnPG(e, "IR_StatusPosts", "Status"); err != nil { - return errors.Wrap(err, "failed dropping column Status in table IR_StatusPosts") - } - } - - if _, err := e.Exec(` - UPDATE IR_Incident - SET CurrentStatus = - CASE - WHEN CurrentStatus = 'Archived' - THEN 'Finished' - ELSE 'InProgress' - END; - `); err != nil { - return errors.Wrap(err, "failed changing CurrentStatus to Archived or InProgress in table IR_Incident") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.25.0"), - toVersion: semver.MustParse("0.26.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "CategoryName", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column CategoryName to table IR_Playbook") - } - - if _, err := e.Exec("UPDATE IR_Playbook SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled=1"); err != nil { - return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Incident", "CategoryName", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column CategoryName to table IR_Incident") - } - - if _, err := e.Exec("UPDATE IR_Incident SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled=1"); err != nil { - return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "CategoryName", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column CategoryName to table IR_Playbook") - } - - if _, err := e.Exec("UPDATE IR_Playbook SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled"); err != nil { - return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Incident", "CategoryName", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column CategoryName to table IR_Incident") - } - - if _, err := e.Exec("UPDATE IR_Incident SET CategoryName = 'Playbook Runs' WHERE CategorizeChannelEnabled"); err != nil { - return errors.Wrapf(err, "failed setting default value in column CategoryName of table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.26.0"), - toVersion: semver.MustParse("0.27.0"), - // This deprecates columns BroadcastChannelID (in singular), AnnouncementChannelID and AnnouncementChannelEnabled - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - updateIncidentTableQuery := ` - UPDATE IR_Incident SET - ConcatenatedBroadcastChannelIds = ( - COALESCE( - CONCAT_WS( - ',', - CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END, - CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END - ), - '') - ) - ` - - updatePlaybookTableQuery := ` - UPDATE IR_Playbook SET - ConcatenatedBroadcastChannelIds = ( - COALESCE( - CONCAT_WS( - ',', - CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END, - CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END - ), - '') - ) - , BroadcastEnabled = (CASE - WHEN BroadcastChannelID != '' THEN TRUE - WHEN AnnouncementChannelEnabled = TRUE THEN TRUE - ELSE FALSE - END) - ` - - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Incident") - } - - if _, err := e.Exec(updateIncidentTableQuery); err != nil { - return errors.Wrapf(err, "failed setting value in column ConcatenatedBroadcastChannelIds of table IR_Incident") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Playbook") - } - - if err := addColumnToMySQLTable(e, "IR_Playbook", "BroadcastEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column BroadcastEnabled to table IR_Playbook") - } - - if _, err := e.Exec(updatePlaybookTableQuery); err != nil { - return errors.Wrapf(err, "failed setting value in columns ConcatenatedBroadcastChannelIds and BroadcastEnabled of table IR_Playbook") - } - - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Incident") - } - - if _, err := e.Exec(updateIncidentTableQuery); err != nil { - return errors.Wrapf(err, "failed setting value in column ConcatenatedBroadcastChannelIds of table IR_Incident") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "ConcatenatedBroadcastChannelIds", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ConcatenatedBroadcastChannelIds to table IR_Playbook") - } - - if err := addColumnToPGTable(e, "IR_Playbook", "BroadcastEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column BroadcastEnabled to table IR_Playbook") - } - - if _, err := e.Exec(updatePlaybookTableQuery); err != nil { - return errors.Wrapf(err, "failed setting value in columns ConcatenatedBroadcastChannelIds and BroadcastEnabled of table IR_Playbook") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.27.0"), - toVersion: semver.MustParse("0.28.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ChannelIDToRootID", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelIDToRootID to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ChannelIDToRootID", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelIDToRootID to table IR_Incident") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.28.0"), - toVersion: semver.MustParse("0.29.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(`ALTER TABLE IR_System CONVERT TO CHARACTER SET utf8mb4`); err != nil { - return errors.Wrapf(err, "failed to migrate character set") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.29.0"), - toVersion: semver.MustParse("0.30.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addPrimaryKey(e, sqlStore, "IR_PlaybookMember", "(MemberID, PlaybookID)"); err != nil { - return err - } - if err := dropIndexIfExists(e, sqlStore, "IR_StatusPosts", "posts_unique"); err != nil { - return err - } - if err := addPrimaryKey(e, sqlStore, "IR_StatusPosts", "(IncidentID, PostID)"); err != nil { - return err - } - if err := addPrimaryKey(e, sqlStore, "IR_TimelineEvent", "(ID)"); err != nil { - return err - } - if err := dropIndexIfExists(e, sqlStore, "IR_ViewedChannel", "IR_ViewedChannel_ChannelID_UserID"); err != nil { - return err - } - if err := addPrimaryKey(e, sqlStore, "IR_ViewedChannel", "(ChannelID, UserID)"); err != nil { - return err - } - } else { - if err := addPrimaryKey(e, sqlStore, "ir_playbookmember", "(MemberID, PlaybookID)"); err != nil { - return err - } - if err := addPrimaryKey(e, sqlStore, "ir_statusposts", "(IncidentID, PostID)"); err != nil { - return err - } - if err := addPrimaryKey(e, sqlStore, "ir_timelineevent", "(ID)"); err != nil { - return err - } - if err := addPrimaryKey(e, sqlStore, "ir_viewedchannel", "(ChannelID, UserID)"); err != nil { - return err - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.30.0"), - toVersion: semver.MustParse("0.31.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // Best effort migration so we just log the error to avoid killing the plugin. - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec("UPDATE IGNORE PluginKeyValueStore SET PluginId='playbooks' WHERE PluginId='com.mattermost.plugin-incident-management'"); err != nil { - logrus.WithError(err).Error("failed to migrate KV store plugin id") - } - } else { - - if _, err := e.Exec("UPDATE PluginKeyValueStore k SET PluginId='playbooks' WHERE PluginId='com.mattermost.plugin-incident-management' AND NOT EXISTS ( SELECT 1 FROM PluginKeyValueStore WHERE PluginId='playbooks' AND PKey = k.PKey )"); err != nil { - logrus.WithError(err).Error("failed to migrate KV store plugin id") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.31.0"), - toVersion: semver.MustParse("0.32.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "ReminderTimerDefaultSeconds", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column ReminderTimerDefaultSeconds to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.32.0"), - toVersion: semver.MustParse("0.33.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := renameColumnMySQL(e, "IR_Playbook", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Playbook") - } - - if err := renameColumnMySQL(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Playbook") - } - - if err := renameColumnMySQL(e, "IR_Incident", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Incident") - } - - if err := renameColumnMySQL(e, "IR_Incident", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs", "TEXT"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Incident") - } - } else { - if err := renameColumnPG(e, "IR_Playbook", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Playbook") - } - - if err := renameColumnPG(e, "IR_Playbook", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Playbook") - } - - if err := renameColumnPG(e, "IR_Incident", "WebhookOnCreationURL", "ConcatenatedWebhookOnCreationURLs"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnCreationURL to ConcatenatedWebhookOnCreationURLs in table IR_Incident") - } - - if err := renameColumnPG(e, "IR_Incident", "WebhookOnStatusUpdateURL", "ConcatenatedWebhookOnStatusUpdateURLs"); err != nil { - return errors.Wrapf(err, "failed renaming column WebhookOnStatusUpdateURL to ConcatenatedWebhookOnStatusUpdateURLs in table IR_Incident") - } - - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.33.0"), - toVersion: semver.MustParse("0.34.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_UserInfo - ( - ID VARCHAR(26) PRIMARY KEY, - LastDailyTodoDMAt BIGINT - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_UserInfo") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_UserInfo - ( - ID TEXT PRIMARY KEY, - LastDailyTodoDMAt BIGINT - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_UserInfo") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.34.0"), - toVersion: semver.MustParse("0.35.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_UserInfo", "DigestNotificationSettingsJSON", "JSON"); err != nil { - return errors.Wrapf(err, "failed adding column DigestNotificationSettings to table IR_UserInfo") - } - } else { - if err := addColumnToPGTable(e, "IR_UserInfo", "DigestNotificationSettingsJSON", "JSON"); err != nil { - return errors.Wrapf(err, "failed adding column DigestNotificationSettings to table IR_UserInfo") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.35.0"), - toVersion: semver.MustParse("0.36.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if err := dropIndexIfExists(e, sqlStore, "IR_StatusPosts", "posts_unique"); err != nil { - return err - } - - return dropIndexIfExists(e, sqlStore, "IR_ViewedChannel", "IR_ViewedChannel_ChannelID_UserID") - }, - }, - { - fromVersion: semver.MustParse("0.36.0"), - toVersion: semver.MustParse("0.37.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // Existing runs without a reminder need to have a reminder set; use 1 week from now. - oneWeek := 7 * 24 * time.Hour - - // Get overdue runs - overdueQuery := sqlStore.builder. - Select("ID"). - From("IR_Incident"). - Where(sq.Eq{"CurrentStatus": app.StatusInProgress}). - Where(sq.NotEq{"PreviousReminder": 0}) - if sqlStore.db.DriverName() == model.DatabaseDriverMysql { - overdueQuery = overdueQuery.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(UNIX_TIMESTAMP() * 1000)")) - } else { - overdueQuery = overdueQuery.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(EXTRACT (EPOCH FROM now())::float*1000)")) - } - - var runIDs []string - if err := sqlStore.selectBuilder(sqlStore.db, &runIDs, overdueQuery); err != nil { - return errors.Wrap(err, "failed to query for overdue runs") - } - - // Get runs that never had a status update set - otherQuery := sqlStore.builder. - Select("ID"). - From("IR_Incident"). - Where(sq.Eq{"CurrentStatus": app.StatusInProgress}). - Where(sq.Eq{"PreviousReminder": 0}) - - var otherRunIDs []string - if err := sqlStore.selectBuilder(sqlStore.db, &otherRunIDs, otherQuery); err != nil { - return errors.Wrap(err, "failed to query for overdue runs") - } - - // Set the new reminders - runIDs = append(runIDs, otherRunIDs...) - for _, ID := range runIDs { - // Just in case (so we don't crash out during the migration) remove any old reminders - sqlStore.scheduler.Cancel(ID) - - if _, err := sqlStore.scheduler.ScheduleOnce(ID, time.Now().Add(oneWeek)); err != nil { - return errors.Wrapf(err, "failed to set new schedule for run id: %s", ID) - } - - // Set the PreviousReminder, and pretend that this was a LastStatusUpdateAt so that - // the reminder timers will show the correct time for when a status update is due. - updatePrevReminderAndLastUpdateAt := sqlStore.builder. - Update("IR_Incident"). - SetMap(map[string]interface{}{ - "PreviousReminder": oneWeek, - "LastStatusUpdateAt": model.GetMillis(), - }). - Where(sq.Eq{"ID": ID}) - if _, err := sqlStore.execBuilder(sqlStore.db, updatePrevReminderAndLastUpdateAt); err != nil { - return errors.Wrap(err, "failed to update new PreviousReminder and LastStatusUpdateAt") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.37.0"), - toVersion: semver.MustParse("0.38.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Run_Participants ( - IncidentID VARCHAR(26) NULL REFERENCES IR_Incident(ID), - UserID VARCHAR(26) NOT NULL, - IsFollower BOOLEAN NOT NULL, - INDEX IR_Run_Participants_UserID (UserID), - INDEX IR_Run_Participants_IncidentID (IncidentID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_Run_Participants") - } - if err := addPrimaryKey(e, sqlStore, "IR_Run_Participants", "(IncidentID, UserID)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_Run_Participants") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Run_Participants ( - UserID TEXT NOT NULL, - IncidentID TEXT NULL REFERENCES IR_Incident(ID), - IsFollower BOOLEAN NOT NULL - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_Run_Participants") - } - - if err := addPrimaryKey(e, sqlStore, "ir_run_participants", "(IncidentID, UserID)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for ir_run_participants") - } - - if _, err := e.Exec(createPGIndex("IR_Run_Participants_UserID", "IR_Run_Participants", "UserID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Run_Participants_UserID") - } - - if _, err := e.Exec(createPGIndex("IR_Run_Participants_IncidentID", "IR_Run_Participants", "IncidentID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Run_Participants_IncidentID") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.38.0"), - toVersion: semver.MustParse("0.39.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "RunSummaryTemplate", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column RunSummaryTemplate to table IR_Playbook") - } - if _, err := e.Exec("UPDATE IR_Playbook SET RunSummaryTemplate = '' WHERE RunSummaryTemplate IS NULL"); err != nil { - return errors.Wrapf(err, "failed updating default value of column RunSummaryTemplate from table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "RunSummaryTemplate", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column RunSummaryTemplate to table IR_Playbook") - } - } - - // Copy the values from the Description column, historically used for the run summary template, into the new RunSummaryTemplate column - if _, err := e.Exec("UPDATE IR_Playbook SET RunSummaryTemplate = Description, Description = '' WHERE Description <> ''"); err != nil { - return errors.Wrapf(err, "failed updating default value of column RunSummaryTemplate from table IR_Playbook") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.39.0"), - toVersion: semver.MustParse("0.40.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_PlaybookAutoFollow ( - PlaybookID VARCHAR(26) NULL REFERENCES IR_Playbook(ID), - UserID VARCHAR(26) NOT NULL - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_PlaybookAutoFollow") - } - if err := addPrimaryKey(e, sqlStore, "IR_PlaybookAutoFollow", "(PlaybookID, UserID)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_PlaybookAutoFollow") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_PlaybookAutoFollow ( - PlaybookID TEXT NULL REFERENCES IR_Playbook(ID), - UserID TEXT NOT NULL - ); - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_PlaybookAutoFollow") - } - - if err := addPrimaryKey(e, sqlStore, "ir_playbookautofollow", "(PlaybookID, UserID)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_PlaybookAutoFollow") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.40.0"), - toVersion: semver.MustParse("0.41.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "ChannelNameTemplate", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelNameTemplate to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "ChannelNameTemplate", "TEXT DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelNameTemplate to table IR_Playbook") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.41.0"), - toVersion: semver.MustParse("0.42.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "StatusUpdateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateEnabled to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.42.0"), - toVersion: semver.MustParse("0.43.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "RetrospectiveEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RetrospectiveEnabled to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.43.0"), - toVersion: semver.MustParse("0.44.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_PlaybookMember", "Roles", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Playbook", "Public", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_PlaybookMember", "Roles", "TEXT"); err != nil { - return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Playbook", "Public", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column Roles to table IR_Playbook") - } - } - - // Set all existing members to admins - if _, err := e.Exec("UPDATE IR_PlaybookMember SET Roles = 'playbook_member playbook_admin' WHERE Roles IS NULL"); err != nil { - return errors.Wrapf(err, "failed setting default value in column Roles of table IR_Playbook") - } - - // Set all playbooks with no members as public - if _, err := e.Exec("UPDATE IR_Playbook p SET Public = true WHERE NOT EXISTS(SELECT 1 FROM IR_PlaybookMember as pm WHERE pm.PlaybookID = p.ID)"); err != nil { - return errors.Wrapf(err, "failed setting default value in column ConcatenatedSignalAnyKeywords of table IR_Playbook") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.44.0"), - toVersion: semver.MustParse("0.45.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // Existing runs without a reminder need to have a reminder set; use 1 week from now. - oneWeek := 7 * 24 * time.Hour - - // Get runs whose reminder was dismissed (PreviousReminder was set to 0), but only for those - // that have status updates enabled (or else they can't fix an overdue status update) - dimissedQuery := sqlStore.builder. - Select("ID"). - From("IR_Incident"). - Where(sq.Eq{"CurrentStatus": app.StatusInProgress}). - Where(sq.Eq{"PreviousReminder": 0}). - Where(sq.Eq{"StatusUpdateEnabled": true}) - - var runIDs []string - if err := sqlStore.selectBuilder(sqlStore.db, &runIDs, dimissedQuery); err != nil { - return errors.Wrap(err, "failed to query for overdue runs") - } - - // Set the new reminders - for _, ID := range runIDs { - // Just in case (so we don't crash out during the migration) remove any old reminders - sqlStore.scheduler.Cancel(ID) - - if _, err := sqlStore.scheduler.ScheduleOnce(ID, time.Now().Add(oneWeek)); err != nil { - return errors.Wrapf(err, "failed to set new schedule for run id: %s", ID) - } - - // Set the PreviousReminder, and pretend that this was a LastStatusUpdateAt so that - // the reminder timers will show the correct time for when a status update is due. - updatePrevReminderAndLastUpdateAt := sqlStore.builder. - Update("IR_Incident"). - SetMap(map[string]interface{}{ - "PreviousReminder": oneWeek, - "LastStatusUpdateAt": model.GetMillis(), - }). - Where(sq.Eq{"ID": ID}) - if _, err := sqlStore.execBuilder(sqlStore.db, updatePrevReminderAndLastUpdateAt); err != nil { - return errors.Wrap(err, "failed to update new PreviousReminder and LastStatusUpdateAt") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.45.0"), - toVersion: semver.MustParse("0.46.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "RunSummaryTemplateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RunSummaryTemplateEnabled to table IR_Playbook") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "RunSummaryTemplateEnabled", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RunSummaryTemplateEnabled to table IR_Playbook") - } - } - - // All playbooks that have an empty run summary should have their run summary disabled (it defaults to enabled) - playbookUpdate := sqlStore.builder. - Update("IR_Playbook"). - Set("RunSummaryTemplateEnabled", false). - Where(sq.Eq{"RunSummaryTemplate": ""}) - - if _, err := sqlStore.execBuilder(e, playbookUpdate); err != nil { - return errors.Wrap(err, "failed updating RunSummaryTemplateEnabled") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.46.0"), - toVersion: semver.MustParse("0.47.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // set CurrentStatus = Finished for runs with EndAt > 0 || IsActive == false - updateOldStatuses := sqlStore.builder. - Update("IR_Incident"). - Set("CurrentStatus", app.StatusFinished). - Where(sq.Or{ - sq.Gt{"EndAt": 0}, - sq.Eq{"IsActive": false}, - }) - - if _, err := sqlStore.execBuilder(sqlStore.db, updateOldStatuses); err != nil { - return errors.Wrap(err, "failed to update new CurrentStatus for old runs") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.47.0"), - toVersion: semver.MustParse("0.48.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_MetricConfig ( - ID VARCHAR(26) PRIMARY KEY, - PlaybookID VARCHAR(26) NOT NULL REFERENCES IR_Playbook(ID), - Title VARCHAR(512) NOT NULL, - Description VARCHAR(4096) NOT NULL, - Type VARCHAR(32) NOT NULL, - Target BIGINT NOT NULL, - Ordering TINYINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0, - INDEX IR_MetricConfig_PlaybookID (PlaybookID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_MetricConfig") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Metric ( - IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID), - MetricConfigID VARCHAR(26) NOT NULL REFERENCES IR_MetricConfig(ID), - Value BIGINT NOT NULL, - Published BOOLEAN NOT NULL, - INDEX IR_Metric_IncidentID (IncidentID), - INDEX IR_Metric_MetricConfigID (MetricConfigID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_Metric") - } - - if err := addPrimaryKey(e, sqlStore, "IR_Metric", "(IncidentID, MetricConfigID)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_Metric") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_MetricConfig ( - ID TEXT PRIMARY KEY, - PlaybookID TEXT NOT NULL REFERENCES IR_Playbook(ID), - Title TEXT NOT NULL, - Description TEXT NOT NULL, - Type TEXT NOT NULL, - Target BIGINT NOT NULL, - Ordering SMALLINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0 - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_MetricConfig") - } - - if _, err := e.Exec(createPGIndex("IR_MetricConfig_PlaybookID", "IR_MetricConfig", "PlaybookID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_MetricConfig_PlaybookID") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Metric ( - IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID), - MetricConfigID TEXT NOT NULL REFERENCES IR_MetricConfig(ID), - Value BIGINT NOT NULL, - Published BOOLEAN NOT NULL - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_Metric") - } - - if err := addPrimaryKey(e, sqlStore, "ir_metric", "(IncidentID, MetricConfigID)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_Metric") - } - - if _, err := e.Exec(createPGIndex("IR_Metric_IncidentID", "IR_Metric", "IncidentID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Metric_IncidentID") - } - if _, err := e.Exec(createPGIndex("IR_Metric_MetricConfigID", "IR_Metric", "MetricConfigID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Metric_MetricConfigID") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.48.0"), - toVersion: semver.MustParse("0.49.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(`ALTER TABLE IR_MetricConfig MODIFY COLUMN Target BIGINT`); err != nil { - return errors.Wrapf(err, "failed creating table IR_MetricConfig") - } - if _, err := e.Exec(`ALTER TABLE IR_Metric MODIFY COLUMN Value BIGINT`); err != nil { - return errors.Wrapf(err, "failed creating table IR_MetricConfig") - } - } else { - if _, err := e.Exec(`ALTER TABLE IR_MetricConfig ALTER COLUMN Target DROP NOT NULL`); err != nil { - return errors.Wrapf(err, "failed creating table IR_MetricConfig") - } - if _, err := e.Exec(`ALTER TABLE IR_Metric ALTER COLUMN Value DROP NOT NULL`); err != nil { - return errors.Wrapf(err, "failed creating table IR_MetricConfig") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.49.0"), - toVersion: semver.MustParse("0.50.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_ChannelAction ( - ID VARCHAR(26) PRIMARY KEY, - ChannelID VARCHAR(26), - Enabled BOOLEAN DEFAULT FALSE, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ActionType TEXT NOT NULL, - TriggerType TEXT NOT NULL, - Payload JSON NOT NULL, - INDEX IR_ChannelAction_ChannelID (ChannelID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_ChannelAction") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_ChannelAction ( - ID TEXT PRIMARY KEY, - ChannelID VARCHAR(26), - Enabled BOOLEAN DEFAULT FALSE, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ActionType TEXT NOT NULL, - TriggerType TEXT NOT NULL, - Payload JSON NOT NULL - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_ChannelAction") - } - - if _, err := e.Exec(createPGIndex("IR_ChannelAction_ChannelID", "IR_ChannelAction", "ChannelID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_ChannelAction_ChannelID") - } - } - - // Retrieve the channel ID and welcome message of every run - - selectQuery := sqlStore.builder. - Select("ChannelID", "MessageOnJoin"). - From("IR_Incident"). - Where(sq.And{ - sq.NotEq{"MessageOnJoin": ""}, - }) - - var rows []struct { - ChannelID string - MessageOnJoin string - } - - if err := sqlStore.selectBuilder(e, &rows, selectQuery); err != nil { - return errors.Wrapf(err, "failed to retrieve the ChannelID and MessageOnJoin from IR_Incident") - } - - // Create a new action for every row returned before - - if len(rows) > 0 { - insertQuery := sqlStore.builder. - Insert("IR_ChannelAction"). - Columns("ID", "ChannelID", "Enabled", "ActionType", "TriggerType", "Payload") - - for _, row := range rows { - payload := struct { - Message string - }{row.MessageOnJoin} - - payloadJSON, err := json.Marshal(payload) - if err != nil { - return errors.Wrapf(err, "failed to marshal welcome message payload: %v", payload) - } - - insertQuery = insertQuery.Values(model.NewId(), row.ChannelID, true, "send_welcome_message", "new_member_joins", payloadJSON) - } - - if _, err := sqlStore.execBuilder(e, insertQuery); err != nil { - return errors.Wrapf(err, "failed to create the channel actions for the existing runs") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.50.0"), - toVersion: semver.MustParse("0.51.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // Retrieve the channel ID and category name of every run - - selectQuery := sqlStore.builder. - Select("ChannelID", "CategoryName"). - From("IR_Incident"). - Where(sq.NotEq{"CategoryName": ""}) - - var rows []struct { - ChannelID string - CategoryName string - } - - if err := sqlStore.selectBuilder(e, &rows, selectQuery); err != nil { - return errors.Wrapf(err, "failed to retrieve the ChannelID and CategoryName from IR_Incident") - } - - // Create a new action for every row returned before - - if len(rows) > 0 { - insertQuery := sqlStore.builder. - Insert("IR_ChannelAction"). - Columns("ID", "ChannelID", "Enabled", "ActionType", "TriggerType", "Payload") - - for _, row := range rows { - payload := struct { - CategoryName string `json:"category_name"` - }{row.CategoryName} - - payloadJSON, err := json.Marshal(payload) - if err != nil { - return errors.Wrapf(err, "failed to marshal category name payload: %v", payload) - } - - insertQuery = insertQuery.Values(model.NewId(), row.ChannelID, true, "categorize_channel", "new_member_joins", payloadJSON) - } - - if _, err := sqlStore.execBuilder(e, insertQuery); err != nil { - return errors.Wrapf(err, "failed to create the channel actions for the existing runs") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.51.0"), - toVersion: semver.MustParse("0.52.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // moved migration code to the next version to remove an unnecessary column - return nil - }, - }, - { - fromVersion: semver.MustParse("0.52.0"), - toVersion: semver.MustParse("0.53.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "StatusUpdateBroadcastChannelsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastChannelsEnabled to table IR_Incident") - } - if err := dropColumnMySQL(e, "IR_Incident", "StatusUpdateBroadcastFollowersEnabled"); err != nil { - return errors.Wrapf(err, "failed dropping column StatusUpdateBroadcastFollowersEnabled from table IR_Incident") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "StatusUpdateBroadcastWebhooksEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastWebhooksEnabled to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "StatusUpdateBroadcastChannelsEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastChannelsEnabled to table IR_Incident") - } - if err := dropColumnPG(e, "IR_Incident", "StatusUpdateBroadcastFollowersEnabled"); err != nil { - return errors.Wrapf(err, "failed dropping column StatusUpdateBroadcastFollowersEnabled from table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_Incident", "StatusUpdateBroadcastWebhooksEnabled", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column StatusUpdateBroadcastWebhooksEnabled to table IR_Incident") - } - } - - // enable channels broadcast where channels ids list is not empty - channelsBroadcast := sqlStore.builder. - Update("IR_Incident"). - Set("StatusUpdateBroadcastChannelsEnabled", true). - Where(sq.NotEq{"ConcatenatedBroadcastChannelIDs": ""}) - - if _, err := sqlStore.execBuilder(e, channelsBroadcast); err != nil { - return errors.Wrapf(err, "failed updating the StatusUpdateBroadcastChannelsEnabled column") - } - - // enable webhooks broadcast where webhooks list is not empty - webhooksBroadcast := sqlStore.builder. - Update("IR_Incident"). - Set("StatusUpdateBroadcastWebhooksEnabled", true). - Where(sq.NotEq{"ConcatenatedWebhookOnStatusUpdateURLs": ""}) - - if _, err := sqlStore.execBuilder(e, webhooksBroadcast); err != nil { - return errors.Wrapf(err, "failed updating the StatusUpdateBroadcastWebhooksEnabled column") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.53.0"), - toVersion: semver.MustParse("0.54.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "SummaryModifiedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "SummaryModifiedAt", "BIGINT NOT NULL DEFAULT 0"); err != nil { - return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.54.0"), - toVersion: semver.MustParse("0.55.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Category ( - ID VARCHAR(26) PRIMARY KEY, - Name VARCHAR(512) NOT NULL, - TeamID VARCHAR(26) NOT NULL, - UserID VARCHAR(26) NOT NULL, - Collapsed BOOLEAN DEFAULT FALSE, - CreateAt BIGINT NOT NULL, - UpdateAt BIGINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0, - INDEX IR_Category_TeamID_UserID (TeamID, UserID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_Category") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Category_Item ( - Type VARCHAR(1) NOT NULL, - CategoryID VARCHAR(26) NOT NULL REFERENCES IR_Category(ID), - ItemID VARCHAR(26) NOT NULL, - INDEX IR_Category_Item_CategoryID (CategoryID) - ) - ` + MySQLCharset); err != nil { - return errors.Wrapf(err, "failed creating table IR_Category_Item") - } - - if err := addPrimaryKey(e, sqlStore, "IR_Category_Item", "(CategoryID, ItemID, Type)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_Category_Item") - } - } else { - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Category ( - ID TEXT PRIMARY KEY, - Name TEXT NOT NULL, - TeamID TEXT NOT NULL, - UserID TEXT NOT NULL, - Collapsed BOOLEAN DEFAULT FALSE, - CreateAt BIGINT NOT NULL, - UpdateAt BIGINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0 - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_Category") - } - - if _, err := e.Exec(createPGIndex("IR_Category_TeamID_UserID", "IR_Category", "TeamID, UserID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Category_TeamID_UserID") - } - - if _, err := e.Exec(` - CREATE TABLE IF NOT EXISTS IR_Category_Item ( - Type TEXT NOT NULL, - CategoryID TEXT NOT NULL REFERENCES IR_Category(ID), - ItemID TEXT NOT NULL - ) - `); err != nil { - return errors.Wrapf(err, "failed creating table IR_Category_Item") - } - - if _, err := e.Exec(createPGIndex("IR_Category_Item_CategoryID", "IR_Category_Item", "CategoryID")); err != nil { - return errors.Wrapf(err, "failed creating index IR_Category_Item_CategoryID") - } - - if err := addPrimaryKey(e, sqlStore, "ir_category_item", "(CategoryID, ItemID, Type)"); err != nil { - return errors.Wrapf(err, "failed creating primary key for IR_Category_Item") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.55.0"), - toVersion: semver.MustParse("0.56.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // Find all users who are members of channels where runs have been created. - // Add them as members of the playbook but only if it's a public playbook. - if _, err := e.Exec(` - INSERT INTO IR_PlaybookMember - SELECT DISTINCT - pb.ID as PlaybookID, - cm.UserID as MemberID, - 'playbook_member' as Roles - FROM IR_Playbook as pb - JOIN IR_Incident as run on run.PlaybookID = pb.ID - JOIN ChannelMembers as cm on cm.ChannelID = run.ChannelID - LEFT JOIN IR_PlaybookMember as pm on pm.PlaybookID = pb.ID AND pm.MemberID = cm.UserID - LEFT JOIN Bots as b ON b.UserID = cm.UserID - WHERE - pb.Public = true AND - pb.DeleteAt = 0 AND - pm.PlaybookID IS NULL AND - b.UserId IS NULL - `); err != nil { - // Migration is optional so no failure just logging. (it will not try again) - logrus.WithError(err).Warn("failed to add existing users as playbook members") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.56.0"), - toVersion: semver.MustParse("0.57.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Run_Participants", "IsParticipant", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident") - } - if _, err := e.Exec(`ALTER TABLE IR_Run_Participants ALTER IsFollower SET DEFAULT FALSE`); err != nil { - return errors.Wrapf(err, "failed to set new column default for IsFollower") - } - } else { - if err := addColumnToPGTable(e, "IR_Run_Participants", "IsParticipant", "BOOLEAN DEFAULT FALSE"); err != nil { - return errors.Wrapf(err, "failed adding column SummaryModifiedAt to table IR_Incident") - } - if _, err := e.Exec(`ALTER TABLE IR_Run_Participants ALTER COLUMN IsFollower SET DEFAULT FALSE`); err != nil { - return errors.Wrapf(err, "failed to set new column default for IsFollower") - } - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.57.0"), - toVersion: semver.MustParse("0.58.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - // Find all users who are members of channels where runs have been created and are followers of the run. - // Update them to become members of the playbook run - var err error - if e.DriverName() == model.DatabaseDriverMysql { - _, err = e.Exec(` - UPDATE IR_Run_Participants - INNER JOIN IR_Incident ON IR_Run_Participants.IncidentID = IR_Incident.ID - INNER JOIN ChannelMembers ON ChannelMembers.ChannelID = IR_Incident.ChannelID - SET IR_Run_Participants.IsParticipant = true - WHERE - IR_Run_Participants.UserID = ChannelMembers.UserID - `) - } else { - _, err = e.Exec(` - UPDATE IR_Run_Participants - SET IsParticipant = true - FROM IR_Incident - INNER JOIN ChannelMembers ON ChannelMembers.ChannelID = IR_Incident.ChannelID - WHERE - IR_Run_Participants.UserID = ChannelMembers.UserID AND - IR_Run_Participants.IncidentID = IR_Incident.ID; - `) - } - if err != nil { - // Migration is optional so no failure just logging. (it will not try again) - logrus.WithError(err).Debug("failed to update existing users as playbook members") - } - - // Find all users who are members of channels where runs have been created. - // Add them as members of the playbook run - if _, err := e.Exec(` - INSERT INTO IR_Run_Participants (UserID, IncidentID, IsFollower, IsParticipant) - SELECT DISTINCT - cm.UserID as UserID, - run.ID as IncidentID, - false as IsFollower, - true as IsParticipant - FROM IR_Incident as run - JOIN ChannelMembers as cm on cm.ChannelID = run.ChannelID - LEFT JOIN IR_Run_Participants as rp on rp.IncidentID = run.ID AND rp.UserID = cm.UserID - WHERE - rp.IncidentID IS NULL - `); err != nil { - // Migration is optional so no failure just logging. (it will not try again) - logrus.WithError(err).Debug("failed to add existing users as playbook members") - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.58.0"), - toVersion: semver.MustParse("0.59.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - - type ColTypeChange struct { - ColName string - Size uint32 - } - - // Migrations are only for postgres - if e.DriverName() == model.DatabaseDriverMysql { - return nil - } - - errCollected := []string{} - changes := map[string][]ColTypeChange{ - "ir_incident": { - {"id", 26}, - {"name", 1024}, - {"description", 4096}, - {"commanderuserid", 26}, - {"teamid", 26}, - {"channelid", 26}, - {"postid", 26}, - {"playbookid", 26}, - {"activestagetitle", 1024}, - {"reminderpostid", 26}, - {"broadcastchannelid", 26}, - {"remindermessagetemplate", 65535}, - {"currentstatus", 1024}, - {"reporteruserid", 26}, - {"concatenatedinviteduserids", 65535}, - {"defaultcommanderid", 26}, - {"announcementchannelid", 26}, - {"concatenatedwebhookoncreationurls", 65535}, - {"concatenatedwebhookonstatusupdateurls", 65535}, - {"concatenatedinvitedgroupids", 65535}, - {"retrospective", 65535}, - {"messageonjoin", 65535}, - {"categoryname", 65535}, - {"concatenatedbroadcastchannelids", 65535}, - {"channelidtorootid", 65535}, - }, - "ir_playbook": { - {"id", 26}, - {"title", 1024}, - {"description", 4096}, - {"teamid", 26}, - {"broadcastchannelid", 26}, - {"remindermessagetemplate", 65535}, - {"concatenatedinviteduserids", 65535}, - {"defaultcommanderid", 26}, - {"announcementchannelid", 26}, - {"concatenatedwebhookoncreationurls", 65535}, - {"concatenatedinvitedgroupids", 65535}, - {"messageonjoin", 65535}, - {"retrospectivetemplate", 65535}, - {"concatenatedwebhookonstatusupdateurls", 65535}, - {"concatenatedsignalanykeywords", 65535}, - {"categoryname", 65535}, - {"concatenatedbroadcastchannelids", 65535}, - {"runsummarytemplate", 65535}, - {"channelnametemplate", 65535}, - }, - "ir_statusposts": { - {"incidentid", 26}, - {"postid", 26}, - }, - "ir_category": { - {"id", 26}, - {"name", 512}, - {"teamid", 26}, - {"userid", 26}, - }, - "ir_category_item": { - {"type", 1}, - {"categoryid", 26}, - {"itemid", 26}, - }, - "ir_channelaction": { - {"id", 26}, - {"actiontype", 65535}, - {"triggertype", 65535}, - }, - "ir_metric": { - {"incidentid", 26}, - {"metricconfigid", 26}, - }, - "ir_metricconfig": { - {"id", 26}, - {"playbookid", 26}, - {"title", 512}, - {"description", 4096}, - {"type", 32}, - }, - "ir_playbookautofollow": { - {"playbookid", 26}, - {"userid", 26}, - }, - "ir_playbookmember": { - {"playbookid", 26}, - {"memberid", 26}, - {"roles", 65535}, - }, - "ir_run_participants": { - {"userid", 26}, - {"incidentid", 26}, - }, - "ir_viewedchannel": { - {"userid", 26}, - {"channelid", 26}, - }, - "ir_timelineevent": { - {"id", 26}, - {"incidentid", 26}, - {"eventtype", 32}, - {"summary", 256}, - {"details", 4096}, - {"postid", 26}, - {"subjectuserid", 26}, - {"creatoruserid", 26}, - }, - "ir_userinfo": { - {"id", 26}, - }, - } - - for table, cols := range changes { - for _, col := range cols { - err := changeColumnTypeToPGTable(e, table, col.ColName, fmt.Sprintf("varchar(%d)", col.Size)) - if err != nil { - errCollected = append(errCollected, err.Error()) - } - } - } - - if len(errCollected) > 0 { - return errors.New(strings.Join(errCollected, ",\n ")) - } - - return nil - }, - }, - { - fromVersion: semver.MustParse("0.59.0"), - toVersion: semver.MustParse("0.60.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Incident") - } - if err := addColumnToMySQLTable(e, "IR_Playbook", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Incident", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "CreateChannelMemberOnNewParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column CreateChannelMemberOnNewParticipant to table IR_Incident") - } - if err := addColumnToPGTable(e, "IR_Playbook", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Incident", "RemoveChannelMemberOnRemovedParticipant", "BOOLEAN DEFAULT TRUE"); err != nil { - return errors.Wrapf(err, "failed adding column RemoveChannelMemberOnRemovedParticipant to table IR_Incident") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.60.0"), - toVersion: semver.MustParse("0.61.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Playbook", "ChannelID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelID to table IR_Playbook") - } - if err := addColumnToMySQLTable(e, "IR_Playbook", "ChannelMode", "VARCHAR(32) DEFAULT 'create_new_channel'"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelMode to table IR_Incident") - } - // We drop entirely the unique index for MySQL, there's an additional index on ChannelID that is kept - if err := dropIndexIfExists(e, sqlStore, "IR_Incident", "ChannelID"); err != nil { - return errors.Wrapf(err, "failed to drop ir_incident_channelid_key index on table ir_incident") - } - if _, err := e.Exec("UPDATE IR_Incident i JOIN Channels c ON c.id=i.ChannelID AND i.Name='' SET i.name=c.DisplayName"); err != nil { - return errors.Wrapf(err, "failed to update all old run names from channel names") - } - } else { - if err := addColumnToPGTable(e, "IR_Playbook", "ChannelID", "VARCHAR(26) DEFAULT ''"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelID to table IR_Playbook") - } - if err := addColumnToPGTable(e, "IR_Playbook", "ChannelMode", "VARCHAR(32) DEFAULT 'create_new_channel'"); err != nil { - return errors.Wrapf(err, "failed adding column ChannelMode to table IR_Incident") - } - // Unique constraint is dropped but index is kept - if _, err := e.Exec("ALTER TABLE IR_Incident DROP CONSTRAINT IF EXISTS ir_incident_channelid_key"); err != nil { - return errors.Wrapf(err, "failed to drop constraint ir_incident_channelid_key on table ir_incident") - } - if _, err := e.Exec("UPDATE IR_Incident i SET name=c.DisplayName FROM Channels c WHERE c.id=i.ChannelID AND i.Name=''"); err != nil { - return errors.Wrapf(err, "failed to update all old run names from channel names") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.61.0"), - toVersion: semver.MustParse("0.62.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if _, err := e.Exec(` - UPDATE IR_UserInfo - SET DigestNotificationSettingsJSON = - JSON_SET(DigestNotificationSettingsJSON, '$.disable_weekly_digest', - JSON_EXTRACT(DigestNotificationSettingsJSON, '$.disable_daily_digest')); - `); err != nil { - return errors.Wrapf(err, "failed adding disable_weekly_digest field to IR_UserInfo DigestNotificationSettingsJSON") - } - } else { - if _, err := e.Exec(` - UPDATE IR_UserInfo - SET DigestNotificationSettingsJSON = (DigestNotificationSettingsJSON::jsonb || - jsonb_build_object('disable_weekly_digest', (DigestNotificationSettingsJSON::jsonb->>'disable_daily_digest')::boolean))::json; - - `); err != nil { - return errors.Wrapf(err, "failed adding disable_weekly_digest field to IR_UserInfo DigestNotificationSettingsJSON") - } - } - return nil - }, - }, - { - fromVersion: semver.MustParse("0.62.0"), - toVersion: semver.MustParse("0.63.0"), - migrationFunc: func(e sqlx.Ext, sqlStore *SQLStore) error { - if e.DriverName() == model.DatabaseDriverMysql { - if err := addColumnToMySQLTable(e, "IR_Incident", "RunType", "VARCHAR(32) DEFAULT 'playbook'"); err != nil { - return errors.Wrapf(err, "failed adding column RunType to table IR_Incident") - } - } else { - if err := addColumnToPGTable(e, "IR_Incident", "RunType", "VARCHAR(32) DEFAULT 'playbook'"); err != nil { - return errors.Wrapf(err, "failed adding column RunType to table IR_Incident") - } - } - return nil - }, - }, -} diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.59.down.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.59.down.sql deleted file mode 100644 index df2abdff5d8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.59.down.sql +++ /dev/null @@ -1 +0,0 @@ --- intentionally empty, 0.57 migration is only for postgres diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.59.up.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.59.up.sql deleted file mode 100644 index df2abdff5d8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.59.up.sql +++ /dev/null @@ -1 +0,0 @@ --- intentionally empty, 0.57 migration is only for postgres diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.60.down.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.60.down.sql deleted file mode 100644 index d0e88107a55..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.60.down.sql +++ /dev/null @@ -1,62 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'CreateChannelMemberOnNewParticipant' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN CreateChannelMemberOnNewParticipant;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; - - -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'RemoveChannelMemberOnRemovedParticipant' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN RemoveChannelMemberOnRemovedParticipant;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; - - -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CreateChannelMemberOnNewParticipant' - ), - 'ALTER TABLE IR_Incident DROP COLUMN CreateChannelMemberOnNewParticipant;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; - - -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RemoveChannelMemberOnRemovedParticipant' - ), - 'ALTER TABLE IR_Incident DROP COLUMN RemoveChannelMemberOnRemovedParticipant;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.60.up.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.60.up.sql deleted file mode 100644 index d7c788c6810..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.60.up.sql +++ /dev/null @@ -1,63 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'CreateChannelMemberOnNewParticipant' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN CreateChannelMemberOnNewParticipant BOOLEAN DEFAULT TRUE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - - -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'RemoveChannelMemberOnRemovedParticipant' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN RemoveChannelMemberOnRemovedParticipant BOOLEAN DEFAULT TRUE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - - -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CreateChannelMemberOnNewParticipant' - ), - 'ALTER TABLE IR_Incident ADD COLUMN CreateChannelMemberOnNewParticipant BOOLEAN DEFAULT TRUE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - - -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RemoveChannelMemberOnRemovedParticipant' - ), - 'ALTER TABLE IR_Incident ADD COLUMN RemoveChannelMemberOnRemovedParticipant BOOLEAN DEFAULT TRUE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.61.down.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.61.down.sql deleted file mode 100644 index df63a072458..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.61.down.sql +++ /dev/null @@ -1,45 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ChannelID' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ChannelID;', - 'SELECT 1;' -)); - -PREPARE dropColumnIfExists FROM @preparedStatement; -EXECUTE dropColumnIfExists; -DEALLOCATE PREPARE dropColumnIfExists; - -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ChannelMode' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ChannelMode;', - 'SELECT 1;' -)); - -PREPARE dropColumnIfExists FROM @preparedStatement; -EXECUTE dropColumnIfExists; -DEALLOCATE PREPARE dropColumnIfExists; - -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_Incident' - AND index_schema = DATABASE() - AND index_name = 'ChannelID' - ), - 'CREATE UNIQUE INDEX ChannelID ON IR_Incident(ChannelID);', - 'SELECT 1;' -)); - -PREPARE createIndexIfNotExists FROM @preparedStatement; -EXECUTE createIndexIfNotExists; -DEALLOCATE PREPARE createIndexIfNotExists; - diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.61.up.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.61.up.sql deleted file mode 100644 index 907422838e0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.61.up.sql +++ /dev/null @@ -1,50 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ChannelID' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ChannelID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ChannelMode' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ChannelMode VARCHAR(32) DEFAULT "create_new_channel";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - --- We drop entirely the unique index for MySQL, there's an additional index on ChannelID that is kept -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_Incident' - AND index_schema = DATABASE() - AND index_name = 'ChannelID' - ), - 'DROP INDEX ChannelID ON IR_Incident;', - 'SELECT 1;' -)); - -PREPARE dropIndexIfExists FROM @preparedStatement; -EXECUTE dropIndexIfExists; -DEALLOCATE PREPARE dropIndexIfExists; - --- update names from channel display names -UPDATE IR_Incident i -JOIN Channels c ON c.id=i.ChannelID AND i.Name='' -SET i.name=c.DisplayName diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.62.down.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.62.down.sql deleted file mode 100644 index ec83c76b9c2..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.62.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -UPDATE IR_UserInfo -SET DigestNotificationSettingsJSON = JSON_REMOVE(DigestNotificationSettingsJSON, '$.disable_weekly_digest'); \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.62.up.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.62.up.sql deleted file mode 100644 index 54c20c277f1..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.62.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE IR_UserInfo -SET DigestNotificationSettingsJSON = - JSON_SET(DigestNotificationSettingsJSON, '$.disable_weekly_digest', - JSON_EXTRACT(DigestNotificationSettingsJSON, '$.disable_daily_digest')); \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.63.down.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.63.down.sql deleted file mode 100644 index 9b005a1e6d8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.63.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RunType' - ), - 'ALTER TABLE IR_Incident DROP COLUMN RunType;', - 'SELECT 1;' -)); - -PREPARE dropColumnIfExists FROM @preparedStatement; -EXECUTE dropColumnIfExists; -DEALLOCATE PREPARE dropColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/future/mysql/0.63.up.sql b/server/playbooks/server/sqlstore/migrations/future/mysql/0.63.up.sql deleted file mode 100644 index 2d62d9462d3..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/mysql/0.63.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RunType' - ), - 'ALTER TABLE IR_Incident ADD COLUMN RunType VARCHAR(32) DEFAULT "playbook";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.59.down.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.59.down.sql deleted file mode 100644 index 72e4ad0ca26..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.59.down.sql +++ /dev/null @@ -1,96 +0,0 @@ - -ALTER TABLE ir_incident ALTER COLUMN id TYPE text; -ALTER TABLE ir_incident ALTER COLUMN name TYPE text; -ALTER TABLE ir_incident ALTER COLUMN description TYPE text; -ALTER TABLE ir_incident ALTER COLUMN commanderuserid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN teamid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN channelid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN postid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN playbookid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN activestagetitle TYPE text; -ALTER TABLE ir_incident ALTER COLUMN reminderpostid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN broadcastchannelid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN remindermessagetemplate TYPE text; -ALTER TABLE ir_incident ALTER COLUMN currentstatus TYPE text; -ALTER TABLE ir_incident ALTER COLUMN reporteruserid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN concatenatedinviteduserids TYPE text; -ALTER TABLE ir_incident ALTER COLUMN defaultcommanderid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN announcementchannelid TYPE text; -ALTER TABLE ir_incident ALTER COLUMN concatenatedwebhookoncreationurls TYPE text; -ALTER TABLE ir_incident ALTER COLUMN concatenatedwebhookonstatusupdateurls TYPE text; -ALTER TABLE ir_incident ALTER COLUMN concatenatedinvitedgroupids TYPE text; -ALTER TABLE ir_incident ALTER COLUMN retrospective TYPE text; -ALTER TABLE ir_incident ALTER COLUMN messageonjoin TYPE text; -ALTER TABLE ir_incident ALTER COLUMN categoryname TYPE text; -ALTER TABLE ir_incident ALTER COLUMN concatenatedbroadcastchannelids TYPE text; -ALTER TABLE ir_incident ALTER COLUMN channelidtorootid TYPE text; - -ALTER TABLE ir_playbook ALTER COLUMN id TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN title TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN description TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN teamid TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN broadcastchannelid TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN remindermessagetemplate TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN concatenatedinviteduserids TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN defaultcommanderid TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN announcementchannelid TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN concatenatedwebhookoncreationurls TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN concatenatedinvitedgroupids TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN messageonjoin TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN retrospectivetemplate TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN concatenatedwebhookonstatusupdateurls TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN concatenatedsignalanykeywords TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN categoryname TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN concatenatedbroadcastchannelids TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN runsummarytemplate TYPE text; -ALTER TABLE ir_playbook ALTER COLUMN channelnametemplate TYPE text; - -ALTER TABLE ir_statusposts ALTER COLUMN incidentid TYPE text; -ALTER TABLE ir_statusposts ALTER COLUMN postid TYPE text; - -ALTER TABLE ir_category ALTER COLUMN id TYPE text; -ALTER TABLE ir_category ALTER COLUMN name TYPE text; -ALTER TABLE ir_category ALTER COLUMN teamid TYPE text; -ALTER TABLE ir_category ALTER COLUMN userid TYPE text; - - -ALTER TABLE ir_category_item ALTER COLUMN type TYPE text; -ALTER TABLE ir_category_item ALTER COLUMN categoryid TYPE text; -ALTER TABLE ir_category_item ALTER COLUMN itemid TYPE text; - -ALTER TABLE ir_channelaction ALTER COLUMN id TYPE text; -ALTER TABLE ir_channelaction ALTER COLUMN actiontype TYPE text; -ALTER TABLE ir_channelaction ALTER COLUMN triggertype TYPE text; - -ALTER TABLE ir_metric ALTER COLUMN incidentid TYPE text; -ALTER TABLE ir_metric ALTER COLUMN metricconfigid TYPE text; - -ALTER TABLE ir_metricconfig ALTER COLUMN id TYPE text; -ALTER TABLE ir_metricconfig ALTER COLUMN playbookid TYPE text; -ALTER TABLE ir_metricconfig ALTER COLUMN title TYPE text; -ALTER TABLE ir_metricconfig ALTER COLUMN description TYPE text; -ALTER TABLE ir_metricconfig ALTER COLUMN type TYPE text; - -ALTER TABLE ir_playbookautofollow ALTER COLUMN playbookid TYPE text; -ALTER TABLE ir_playbookautofollow ALTER COLUMN userid TYPE text; - -ALTER TABLE ir_playbookmember ALTER COLUMN playbookid TYPE text; -ALTER TABLE ir_playbookmember ALTER COLUMN memberid TYPE text; -ALTER TABLE ir_playbookmember ALTER COLUMN roles TYPE text; - -ALTER TABLE ir_run_participants ALTER COLUMN userid TYPE text; -ALTER TABLE ir_run_participants ALTER COLUMN incidentid TYPE text; - -ALTER TABLE ir_timelineevent ALTER COLUMN id TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN incidentid TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN eventtype TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN summary TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN details TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN postid TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN subjectuserid TYPE text; -ALTER TABLE ir_timelineevent ALTER COLUMN creatoruserid TYPE text; - -ALTER TABLE ir_userinfo ALTER COLUMN id TYPE text; - -ALTER TABLE ir_viewedchannel ALTER COLUMN userid TYPE text; -ALTER TABLE ir_viewedchannel ALTER COLUMN channelid TYPE text; diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.59.up.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.59.up.sql deleted file mode 100644 index c61b17e57de..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.59.up.sql +++ /dev/null @@ -1,95 +0,0 @@ -ALTER TABLE ir_incident ALTER COLUMN id TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN name TYPE varchar(1024); -ALTER TABLE ir_incident ALTER COLUMN description TYPE varchar(4096); -ALTER TABLE ir_incident ALTER COLUMN commanderuserid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN teamid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN channelid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN postid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN playbookid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN activestagetitle TYPE varchar(1024); -ALTER TABLE ir_incident ALTER COLUMN reminderpostid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN broadcastchannelid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN remindermessagetemplate TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN currentstatus TYPE varchar(1024); -ALTER TABLE ir_incident ALTER COLUMN reporteruserid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN concatenatedinviteduserids TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN defaultcommanderid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN announcementchannelid TYPE varchar(26); -ALTER TABLE ir_incident ALTER COLUMN concatenatedwebhookoncreationurls TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN concatenatedwebhookonstatusupdateurls TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN concatenatedinvitedgroupids TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN retrospective TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN messageonjoin TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN categoryname TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN concatenatedbroadcastchannelids TYPE varchar(65535); -ALTER TABLE ir_incident ALTER COLUMN channelidtorootid TYPE varchar(65535); - - -ALTER TABLE ir_playbook ALTER COLUMN id TYPE varchar(26); -ALTER TABLE ir_playbook ALTER COLUMN title TYPE varchar(1024); -ALTER TABLE ir_playbook ALTER COLUMN description TYPE varchar(4096); -ALTER TABLE ir_playbook ALTER COLUMN teamid TYPE varchar(26); -ALTER TABLE ir_playbook ALTER COLUMN broadcastchannelid TYPE varchar(26); -ALTER TABLE ir_playbook ALTER COLUMN remindermessagetemplate TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN concatenatedinviteduserids TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN defaultcommanderid TYPE varchar(26); -ALTER TABLE ir_playbook ALTER COLUMN announcementchannelid TYPE varchar(26); -ALTER TABLE ir_playbook ALTER COLUMN concatenatedwebhookoncreationurls TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN concatenatedinvitedgroupids TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN messageonjoin TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN retrospectivetemplate TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN concatenatedwebhookonstatusupdateurls TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN concatenatedsignalanykeywords TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN categoryname TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN concatenatedbroadcastchannelids TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN runsummarytemplate TYPE varchar(65535); -ALTER TABLE ir_playbook ALTER COLUMN channelnametemplate TYPE varchar(65535); - -ALTER TABLE ir_statusposts ALTER COLUMN incidentid TYPE varchar(26); -ALTER TABLE ir_statusposts ALTER COLUMN postid TYPE varchar(26); - -ALTER TABLE ir_category ALTER COLUMN id TYPE varchar(26); -ALTER TABLE ir_category ALTER COLUMN name TYPE varchar(512); -ALTER TABLE ir_category ALTER COLUMN teamid TYPE varchar(26); -ALTER TABLE ir_category ALTER COLUMN userid TYPE varchar(26); - -ALTER TABLE ir_category_item ALTER COLUMN type TYPE varchar(1); -ALTER TABLE ir_category_item ALTER COLUMN categoryid TYPE varchar(26); -ALTER TABLE ir_category_item ALTER COLUMN itemid TYPE varchar(26); - -ALTER TABLE ir_channelaction ALTER COLUMN id TYPE varchar(26); -ALTER TABLE ir_channelaction ALTER COLUMN actiontype TYPE varchar(65535); -ALTER TABLE ir_channelaction ALTER COLUMN triggertype TYPE varchar(65535); - -ALTER TABLE ir_metric ALTER COLUMN incidentid TYPE varchar(26); -ALTER TABLE ir_metric ALTER COLUMN metricconfigid TYPE varchar(26); - -ALTER TABLE ir_metricconfig ALTER COLUMN id TYPE varchar(26); -ALTER TABLE ir_metricconfig ALTER COLUMN playbookid TYPE varchar(26); -ALTER TABLE ir_metricconfig ALTER COLUMN title TYPE varchar(512); -ALTER TABLE ir_metricconfig ALTER COLUMN description TYPE varchar(4096); -ALTER TABLE ir_metricconfig ALTER COLUMN type TYPE varchar(32); - -ALTER TABLE ir_playbookautofollow ALTER COLUMN playbookid TYPE varchar(26); -ALTER TABLE ir_playbookautofollow ALTER COLUMN userid TYPE varchar(26); - -ALTER TABLE ir_playbookmember ALTER COLUMN playbookid TYPE varchar(26); -ALTER TABLE ir_playbookmember ALTER COLUMN memberid TYPE varchar(26); -ALTER TABLE ir_playbookmember ALTER COLUMN roles TYPE varchar(65535); - -ALTER TABLE ir_run_participants ALTER COLUMN userid TYPE varchar(26); -ALTER TABLE ir_run_participants ALTER COLUMN incidentid TYPE varchar(26); - -ALTER TABLE ir_timelineevent ALTER COLUMN id TYPE varchar(26); -ALTER TABLE ir_timelineevent ALTER COLUMN incidentid TYPE varchar(26); -ALTER TABLE ir_timelineevent ALTER COLUMN eventtype TYPE varchar(32); -ALTER TABLE ir_timelineevent ALTER COLUMN summary TYPE varchar(256); -ALTER TABLE ir_timelineevent ALTER COLUMN details TYPE varchar(4096); -ALTER TABLE ir_timelineevent ALTER COLUMN postid TYPE varchar(26); -ALTER TABLE ir_timelineevent ALTER COLUMN subjectuserid TYPE varchar(26); -ALTER TABLE ir_timelineevent ALTER COLUMN creatoruserid TYPE varchar(26); - -ALTER TABLE ir_userinfo ALTER COLUMN id TYPE varchar(26); - -ALTER TABLE ir_viewedchannel ALTER COLUMN userid TYPE varchar(26); -ALTER TABLE ir_viewedchannel ALTER COLUMN channelid TYPE varchar(26); diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.60.down.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.60.down.sql deleted file mode 100644 index 74e8f56af6b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.60.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS CreateChannelMemberOnNewParticipant; -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS RemoveChannelMemberOnRemovedParticipant; -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS CreateChannelMemberOnNewParticipant; -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS RemoveChannelMemberOnRemovedParticipant; diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.60.up.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.60.up.sql deleted file mode 100644 index 56ca7e51156..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.60.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS CreateChannelMemberOnNewParticipant BOOLEAN DEFAULT TRUE; -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS RemoveChannelMemberOnRemovedParticipant BOOLEAN DEFAULT TRUE; -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS CreateChannelMemberOnNewParticipant BOOLEAN DEFAULT TRUE; -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS RemoveChannelMemberOnRemovedParticipant BOOLEAN DEFAULT TRUE; diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.61.down.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.61.down.sql deleted file mode 100644 index fbb4795c8e8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.61.down.sql +++ /dev/null @@ -1,18 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ChannelID; -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ChannelMode; - --- add unique constraint to channelid index -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT INDEXNAME FROM PG_INDEXES - WHERE TABLENAME = 'ir_incident' - AND INDEXNAME = 'ir_incident_channelid_key' - ) THEN - ALTER TABLE IR_Incident ADD CONSTRAINT ir_incident_channelid_key UNIQUE(ChannelID); - END IF; -END -$$; - - diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.61.up.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.61.up.sql deleted file mode 100644 index 34c23c61e24..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.61.up.sql +++ /dev/null @@ -1,11 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ChannelID VARCHAR(26) DEFAULT ''; -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ChannelMode VARCHAR(32) DEFAULT 'create_new_channel'; - --- Drop unique constraint and kee the index -ALTER TABLE IR_Incident DROP CONSTRAINT IF EXISTS ir_incident_channelid_key: - --- update empty names on incident table with channels data -UPDATE IR_Incident i -SET name=c.DisplayName -FROM Channels c -WHERE c.id=i.ChannelID AND i.Name=''; diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.62.down.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.62.down.sql deleted file mode 100644 index d027b0cfb0c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.62.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -UPDATE IR_UserInfo -SET DigestNotificationSettingsJSON = (DigestNotificationSettingsJSON::jsonb - 'DisableWeeklyDigest')::json; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.62.up.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.62.up.sql deleted file mode 100644 index 2a9437c0f62..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.62.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE IR_UserInfo -SET DigestNotificationSettingsJSON = (DigestNotificationSettingsJSON::jsonb || - jsonb_build_object('disable_weekly_digest', (DigestNotificationSettingsJSON::json->>'disable_daily_digest')::boolean))::json; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.63.down.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.63.down.sql deleted file mode 100644 index 5d61964ad9f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.63.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS RunType; diff --git a/server/playbooks/server/sqlstore/migrations/future/postgres/0.63.up.sql b/server/playbooks/server/sqlstore/migrations/future/postgres/0.63.up.sql deleted file mode 100644 index 5d78d9282a7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/future/postgres/0.63.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS RunType VARCHAR(32) DEFAULT 'playbook'; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.down.sql deleted file mode 100644 index aec796afa5c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_System; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.up.sql deleted file mode 100644 index fcc65fceac4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000001_create_IR_system.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_System ( - SKey VARCHAR(64) PRIMARY KEY, - SValue VARCHAR(1024) NULL -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.down.sql deleted file mode 100644 index c8af882bb49..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_Incident; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.up.sql deleted file mode 100644 index 42101700423..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000002_create_IR_incident.up.sql +++ /dev/null @@ -1,19 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_Incident ( - ID VARCHAR(26) PRIMARY KEY, - Name VARCHAR(1024) NOT NULL, - Description VARCHAR(4096) NOT NULL, - IsActive BOOLEAN NOT NULL, - CommanderUserID VARCHAR(26) NOT NULL, - TeamID VARCHAR(26) NOT NULL, - ChannelID VARCHAR(26) NOT NULL UNIQUE, - CreateAt BIGINT NOT NULL, - EndAt BIGINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ActiveStage BIGINT NOT NULL, - PostID VARCHAR(26) NOT NULL DEFAULT '', - PlaybookID VARCHAR(26) NOT NULL DEFAULT '', - ChecklistsJSON TEXT NOT NULL, - INDEX IR_Incident_TeamID (TeamID), - INDEX IR_Incident_TeamID_CommanderUserID (TeamID, CommanderUserID), - INDEX IR_Incident_ChannelID (ChannelID) -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.down.sql deleted file mode 100644 index 79e50aab1a4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_Playbook; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.up.sql deleted file mode 100644 index 6d1325d2999..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000003_create_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_Playbook ( - ID VARCHAR(26) PRIMARY KEY, - Title VARCHAR(1024) NOT NULL, - Description VARCHAR(4096) NOT NULL, - TeamID VARCHAR(26) NOT NULL, - CreatePublicIncident BOOLEAN NOT NULL, - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ChecklistsJSON TEXT NOT NULL, - NumStages BIGINT NOT NULL DEFAULT 0, - NumSteps BIGINT NOT NULL DEFAULT 0, - INDEX IR_Playbook_TeamID (TeamID), - INDEX IR_PlaybookMember_PlaybookID (ID) -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.down.sql deleted file mode 100644 index 6edfe5ff386..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_PlaybookMember; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.up.sql deleted file mode 100644 index 28e3951ffd9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000004_create_ir_playbook_member.up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_PlaybookMember ( - PlaybookID VARCHAR(26) NOT NULL REFERENCES IR_Playbook(ID), - MemberID VARCHAR(26) NOT NULL, - INDEX IR_PlaybookMember_PlaybookID (PlaybookID), - INDEX IR_PlaybookMember_MemberID (MemberID) -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.down.sql deleted file mode 100644 index d98873c9ba5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ActiveStageTitle' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ActiveStageTitle;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.up.sql deleted file mode 100644 index 54fc500bafb..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000005_add_active_stage_title_to_ir_incident.up.sql +++ /dev/null @@ -1,22 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ActiveStageTitle' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ActiveStageTitle VARCHAR(1024) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - - -UPDATE IR_Incident -SET ActiveStageTitle = JSON_UNQUOTE(JSON_EXTRACT(JSON_EXTRACT(`ChecklistsJSON`, CONCAT('$[', activestage, ']')), '$.title')) -WHERE JSON_VALID(ChecklistsJSON) = 1 -AND JSON_TYPE(ChecklistsJSON) = 'ARRAY' -AND JSON_LENGTH(ChecklistsJSON) > ActiveStage -AND ActiveStage >= 0; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.down.sql deleted file mode 100644 index 8fc426811d9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_StatusPosts; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.up.sql deleted file mode 100644 index 979e4124ae7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000006_create_ir_status_posts.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_StatusPosts ( - IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID), - PostID VARCHAR(26) NOT NULL, - CONSTRAINT posts_unique UNIQUE (IncidentID, PostID), - INDEX IR_StatusPosts_IncidentID (IncidentID), - INDEX IR_StatusPosts_PostID (PostID) -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.down.sql deleted file mode 100644 index e30e5e17e13..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReminderPostID' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ReminderPostID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.up.sql deleted file mode 100644 index f187bca335a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000007_add_reminder_post_id_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReminderPostID' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ReminderPostID VARCHAR(26);', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.down.sql deleted file mode 100644 index 7b00a9ff30b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'BroadcastChannelID' - ), - 'ALTER TABLE IR_Incident DROP COLUMN BroadcastChannelID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.up.sql deleted file mode 100644 index af26b4a844b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000008_add_broadcast_channel_id_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'BroadcastChannelID' - ), - 'ALTER TABLE IR_Incident ADD COLUMN BroadcastChannelID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.down.sql deleted file mode 100644 index f67b8002127..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'BroadcastChannelID' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN BroadcastChannelID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.up.sql deleted file mode 100644 index 4027455f399..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000009_add_broadcast_channel_id_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'BroadcastChannelID' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN BroadcastChannelID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.down.sql deleted file mode 100644 index d371a2bb333..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'PreviousReminder' - ), - 'ALTER TABLE IR_Incident DROP COLUMN PreviousReminder;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.up.sql deleted file mode 100644 index 8e40e4ed85d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000010_add_previous_reminder_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'PreviousReminder' - ), - 'ALTER TABLE IR_Incident ADD COLUMN PreviousReminder BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.down.sql deleted file mode 100644 index 46360ddefc0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ReminderMessageTemplate' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ReminderMessageTemplate;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.up.sql deleted file mode 100644 index e205f45f185..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000011_add_reminder_message_template_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ReminderMessageTemplate' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ReminderMessageTemplate TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET ReminderMessageTemplate = '' -WHERE ReminderMessageTemplate IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.down.sql deleted file mode 100644 index 7e967bbc197..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReminderMessageTemplate' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ReminderMessageTemplate;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.up.sql deleted file mode 100644 index 771e3156d32..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000012_add_reminder_message_template_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReminderMessageTemplate' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ReminderMessageTemplate TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET ReminderMessageTemplate = '' -WHERE ReminderMessageTemplate IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql deleted file mode 100644 index 1c7e5710ae4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ReminderTimerDefaultSeconds' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ReminderTimerDefaultSeconds;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql deleted file mode 100644 index d544b0ad694..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ReminderTimerDefaultSeconds' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ReminderTimerDefaultSeconds BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.down.sql deleted file mode 100644 index d918035fd0d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CurrentStatus' - ), - 'ALTER TABLE IR_Incident DROP COLUMN CurrentStatus;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.up.sql deleted file mode 100644 index 0c06974dac4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000014_add_current_status_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CurrentStatus' - ), - 'ALTER TABLE IR_Incident ADD COLUMN CurrentStatus VARCHAR(1024) NOT NULL DEFAULT "Active";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET CurrentStatus = 'Resolved' -WHERE EndAt != 0 diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.down.sql deleted file mode 100644 index 2099192fce0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND column_name = 'Status' - ), - 'ALTER TABLE IR_StatusPosts DROP COLUMN Status;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.up.sql deleted file mode 100644 index 4d719ebdcfd..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000015_add_status_to_ir_status_posts.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND column_name = 'Status' - ), - 'ALTER TABLE IR_StatusPosts ADD COLUMN Status VARCHAR(1024) NOT NULL DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.down.sql deleted file mode 100644 index d218e43a0b0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_TimelineEvent; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.up.sql deleted file mode 100644 index 0891c88ede7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000016_create_ir_timeline_event.up.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_TimelineEvent ( - ID VARCHAR(26) NOT NULL, - IncidentID VARCHAR(26) NOT NULL REFERENCES IR_Incident(ID), - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - EventAt BIGINT NOT NULL, - EventType VARCHAR(32) NOT NULL DEFAULT '', - Summary VARCHAR(256) NOT NULL DEFAULT '', - Details VARCHAR(4096) NOT NULL DEFAULT '', - PostID VARCHAR(26) NOT NULL DEFAULT '', - SubjectUserID VARCHAR(26) NOT NULL DEFAULT '', - CreatorUserID VARCHAR(26) NOT NULL DEFAULT '', - INDEX IR_TimelineEvent_ID (ID), - INDEX IR_TimelineEvent_IncidentID (IncidentID) -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.down.sql deleted file mode 100644 index ee746a60827..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReporterUserID' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ReporterUserID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.up.sql deleted file mode 100644 index 81c4d562779..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000017_add_reporter_user_id_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReporterUserID' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ReporterUserID VARCHAR(26) NOT NULL DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET ReporterUserID = CommanderUserID -WHERE ReporterUserID = '' diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql deleted file mode 100644 index 9c4564e69a5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedUserIDs' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ConcatenatedInvitedUserIDs;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql deleted file mode 100644 index e3b876d9ad0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedUserIDs' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ConcatenatedInvitedUserIDs TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET ConcatenatedInvitedUserIDs = '' -WHERE ConcatenatedInvitedUserIDs IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql deleted file mode 100644 index ece6b291d96..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedUserIDs' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ConcatenatedInvitedUserIDs;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql deleted file mode 100644 index d30b4e91ee0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedUserIDs' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ConcatenatedInvitedUserIDs TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET ConcatenatedInvitedUserIDs = '' -WHERE ConcatenatedInvitedUserIDs IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 224fcf16fbc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'InviteUsersEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN InviteUsersEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.up.sql deleted file mode 100644 index b4725e0b544..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000020_add_invite_users_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'InviteUsersEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN InviteUsersEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.down.sql deleted file mode 100644 index e3de29fd20d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'DefaultCommanderID' - ), - 'ALTER TABLE IR_Incident DROP COLUMN DefaultCommanderID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.up.sql deleted file mode 100644 index a9854a3736f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000021_add_default_commander_id_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'DefaultCommanderID' - ), - 'ALTER TABLE IR_Incident ADD COLUMN DefaultCommanderID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.down.sql deleted file mode 100644 index 43c3882cf32..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'DefaultCommanderID' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN DefaultCommanderID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.up.sql deleted file mode 100644 index b126c64c143..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000022_add_default_commander_id_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'DefaultCommanderID' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN DefaultCommanderID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 3cadcc48b90..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'DefaultCommanderEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN DefaultCommanderEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 9219754dec7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000023_add_default_commander_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'DefaultCommanderEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN DefaultCommanderEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.down.sql deleted file mode 100644 index 027b7d63fdb..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.up.sql deleted file mode 100644 index d8bb7ee0d81..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000024_update_created_at_deleted_at_in_ir_incident.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -UPDATE IR_Incident -INNER JOIN Channels ON IR_Incident.ChannelID = Channels.ID -SET IR_Incident.CreateAt = Channels.CreateAt, - IR_Incident.DeleteAt = Channels.DeleteAt -WHERE IR_Incident.CreateAt = 0 - AND IR_Incident.DeleteAt = 0 - AND IR_Incident.ChannelID = Channels.ID - diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.down.sql deleted file mode 100644 index e1e2f172621..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'AnnouncementChannelID' - ), - 'ALTER TABLE IR_Incident DROP COLUMN AnnouncementChannelID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.up.sql deleted file mode 100644 index edcf5289ea9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000025_add_announcement_channel_id_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'AnnouncementChannelID' - ), - 'ALTER TABLE IR_Incident ADD COLUMN AnnouncementChannelID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.down.sql deleted file mode 100644 index 9d51f6da909..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'AnnouncementChannelID' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN AnnouncementChannelID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.up.sql deleted file mode 100644 index 2cad986eeee..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000026_add_announcement_channel_id_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'AnnouncementChannelID' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN AnnouncementChannelID VARCHAR(26) DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 58eecc3ad67..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'AnnouncementChannelEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN AnnouncementChannelEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql deleted file mode 100644 index e7f1e879370..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'AnnouncementChannelEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN AnnouncementChannelEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.down.sql deleted file mode 100644 index 826f1ae46dd..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationURL' - ), - 'ALTER TABLE IR_Incident DROP COLUMN WebhookOnCreationURL;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.up.sql deleted file mode 100644 index b1ac7024552..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000028_add_webhook_on_creation_url_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationURL' - ), - 'ALTER TABLE IR_Incident ADD COLUMN WebhookOnCreationURL TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET WebhookOnCreationURL = '' -WHERE WebhookOnCreationURL IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql deleted file mode 100644 index 7f1e2adde76..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationURL' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN WebhookOnCreationURL;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql deleted file mode 100644 index f0893df97aa..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationURL' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN WebhookOnCreationURL TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET WebhookOnCreationURL = '' -WHERE WebhookOnCreationURL IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 2f947f3a0db..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN WebhookOnCreationEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 2c2c113e7e1..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN WebhookOnCreationEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql deleted file mode 100644 index 027d7e64148..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedGroupIDs' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ConcatenatedInvitedGroupIDs;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql deleted file mode 100644 index c1a9ac83e8c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedGroupIDs' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ConcatenatedInvitedGroupIDs TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET ConcatenatedInvitedGroupIDs = '' -WHERE ConcatenatedInvitedGroupIDs IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql deleted file mode 100644 index 856b3511841..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedGroupIDs' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ConcatenatedInvitedGroupIDs;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql deleted file mode 100644 index ace0117f0c9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedInvitedGroupIDs' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ConcatenatedInvitedGroupIDs TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET ConcatenatedInvitedGroupIDs = '' -WHERE ConcatenatedInvitedGroupIDs IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.down.sql deleted file mode 100644 index 718ad220dee..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'Retrospective' - ), - 'ALTER TABLE IR_Incident DROP COLUMN Retrospective;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.up.sql deleted file mode 100644 index 7f1972e17fc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000033_add_retrospective_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'Retrospective' - ), - 'ALTER TABLE IR_Incident ADD COLUMN Retrospective TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET Retrospective = '' -WHERE Retrospective IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.down.sql deleted file mode 100644 index 1417ff26be5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'MessageOnJoin' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN MessageOnJoin;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.up.sql deleted file mode 100644 index bd5ea9199df..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000034_add_message_on_join_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'MessageOnJoin' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN MessageOnJoin TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET MessageOnJoin = '' -WHERE MessageOnJoin IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 2feb6325cef..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'MessageOnJoinEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN MessageOnJoinEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.up.sql deleted file mode 100644 index e24c1b10a7a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000035_add_message_on_join_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'MessageOnJoinEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN MessageOnJoinEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.down.sql deleted file mode 100644 index c4df4cbbcb6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'MessageOnJoin' - ), - 'ALTER TABLE IR_Incident DROP COLUMN MessageOnJoin;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.up.sql deleted file mode 100644 index 90654a8bba8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000036_add_message_on_join_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'MessageOnJoin' - ), - 'ALTER TABLE IR_Incident ADD COLUMN MessageOnJoin TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET MessageOnJoin = '' -WHERE MessageOnJoin IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.down.sql deleted file mode 100644 index a7917dc65eb..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_ViewedChannel; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.up.sql deleted file mode 100644 index 25929b1de2b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000037_create_ir_viewed_channel.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_ViewedChannel ( - ChannelID VARCHAR(26) NOT NULL, - UserID VARCHAR(26) NOT NULL, - UNIQUE INDEX IR_ViewedChannel_ChannelID_UserID (ChannelID, UserID) -) DEFAULT CHARACTER SET utf8mb4; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.down.sql deleted file mode 100644 index fccf7ef1c03..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RetrospectivePublishedAt' - ), - 'ALTER TABLE IR_Incident DROP COLUMN RetrospectivePublishedAt;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.up.sql deleted file mode 100644 index 4fc959a4b77..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000038_add_retrospective_published_at_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RetrospectivePublishedAt' - ), - 'ALTER TABLE IR_Incident ADD COLUMN RetrospectivePublishedAt BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql deleted file mode 100644 index de56898181c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveReminderIntervalSeconds' - ), - 'ALTER TABLE IR_Incident DROP COLUMN RetrospectiveReminderIntervalSeconds;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql deleted file mode 100644 index 7139bdd354e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveReminderIntervalSeconds' - ), - 'ALTER TABLE IR_Incident ADD COLUMN RetrospectiveReminderIntervalSeconds BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql deleted file mode 100644 index 9685a955db4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveReminderIntervalSeconds' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN RetrospectiveReminderIntervalSeconds;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql deleted file mode 100644 index 0003c706f5a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveReminderIntervalSeconds' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN RetrospectiveReminderIntervalSeconds BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.down.sql deleted file mode 100644 index 649ba9b6e82..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveWasCanceled' - ), - 'ALTER TABLE IR_Incident DROP COLUMN RetrospectiveWasCanceled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.up.sql deleted file mode 100644 index a94fe5a551e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000041_add_retrospective_was_canceled_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveWasCanceled' - ), - 'ALTER TABLE IR_Incident ADD COLUMN RetrospectiveWasCanceled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.down.sql deleted file mode 100644 index 17fcabd6d0c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveTemplate' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN RetrospectiveTemplate;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.up.sql deleted file mode 100644 index 24fa8a39771..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000042_add_retrospective_template_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'RetrospectiveTemplate' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN RetrospectiveTemplate TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET RetrospectiveTemplate = '' -WHERE RetrospectiveTemplate IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql deleted file mode 100644 index 24889a806a8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateURL' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN WebhookOnStatusUpdateURL;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql deleted file mode 100644 index 9df93193eb8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateURL' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN WebhookOnStatusUpdateURL TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET WebhookOnStatusUpdateURL = '' -WHERE WebhookOnStatusUpdateURL IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql deleted file mode 100644 index fde0160deaf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN WebhookOnStatusUpdateEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 2d0ed508b83..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN WebhookOnStatusUpdateEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql deleted file mode 100644 index a8cbd133002..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateURL' - ), - 'ALTER TABLE IR_Incident DROP COLUMN WebhookOnStatusUpdateURL;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql deleted file mode 100644 index a9de94ca43d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateURL' - ), - 'ALTER TABLE IR_Incident ADD COLUMN WebhookOnStatusUpdateURL TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET WebhookOnStatusUpdateURL = '' -WHERE WebhookOnStatusUpdateURL IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql deleted file mode 100644 index b5eafd2eee7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedSignalAnyKeywords' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ConcatenatedSignalAnyKeywords;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql deleted file mode 100644 index 60624508070..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedSignalAnyKeywords' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ConcatenatedSignalAnyKeywords TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET ConcatenatedSignalAnyKeywords = '' -WHERE ConcatenatedSignalAnyKeywords IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 1bcc7b8bdd7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'SignalAnyKeywordsEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN SignalAnyKeywordsEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql deleted file mode 100644 index b671ea5cbd0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'SignalAnyKeywordsEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN SignalAnyKeywordsEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.down.sql deleted file mode 100644 index 6f712e2a5b6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'UpdateAt' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN UpdateAt;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.up.sql deleted file mode 100644 index b14d15e9108..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000048_add_update_at_to_ir_playbook.up.sql +++ /dev/null @@ -1,32 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'UpdateAt' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN UpdateAt BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET UpdateAt = CreateAt; - -SET @preparedStatement = (SELECT IF( - ( - SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND index_name = 'IR_Playbook_UpdateAt' - ) > 0, - 'SELECT 1', - 'CREATE INDEX IR_Playbook_UpdateAt ON IR_Playbook(UpdateAt);' -)); - -PREPARE createIndexIfNotExists FROM @preparedStatement; -EXECUTE createIndexIfNotExists; -DEALLOCATE PREPARE createIndexIfNotExists; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.down.sql deleted file mode 100644 index 7ed73d30525..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'LastStatusUpdateAt' - ), - 'ALTER TABLE IR_Incident DROP COLUMN LastStatusUpdateAt;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.up.sql deleted file mode 100644 index 59c55b8f430..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000049_add_last_status_update_at_to_ir_incident.up.sql +++ /dev/null @@ -1,25 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'LastStatusUpdateAt' - ), - 'ALTER TABLE IR_Incident ADD COLUMN LastStatusUpdateAt BIGINT DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident as dest, -( - SELECT i.ID as ID, COALESCE(MAX(p.CreateAt), i.CreateAt) as LastStatusUpdateAt - FROM IR_Incident as i - LEFT JOIN IR_StatusPosts as sp on i.ID = sp.IncidentID - LEFT JOIN Posts as p on sp.PostID = p.Id - GROUP BY i.ID -) as src -SET dest.LastStatusUpdateAt = src.LastStatusUpdateAt -WHERE dest.ID = src.ID; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 2d908a8e9e2..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnArchiveEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ExportChannelOnArchiveEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql deleted file mode 100644 index a4279d346a8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnArchiveEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ExportChannelOnArchiveEnabled BOOLEAN NOT NULL DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql deleted file mode 100644 index 74ee87be9f7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnArchiveEnabled' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ExportChannelOnArchiveEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql deleted file mode 100644 index 9077c62b1cc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnArchiveEnabled' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ExportChannelOnArchiveEnabled BOOLEAN NOT NULL DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql deleted file mode 100644 index ad9debe5682..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'CategorizeChannelEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN CategorizeChannelEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 4945104effc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'CategorizeChannelEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN CategorizeChannelEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql deleted file mode 100644 index 9bbccd4243b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CategorizeChannelEnabled' - ), - 'ALTER TABLE IR_Incident DROP COLUMN CategorizeChannelEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql deleted file mode 100644 index 643f8889a31..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CategorizeChannelEnabled' - ), - 'ALTER TABLE IR_Incident ADD COLUMN CategorizeChannelEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql deleted file mode 100644 index 616f4936104..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnFinishedEnabled' - ), - 'ALTER TABLE IR_Playbook CHANGE COLUMN ExportChannelOnFinishedEnabled ExportChannelOnArchiveEnabled BOOLEAN NOT NULL DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql deleted file mode 100644 index ccd6dbaa229..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnArchiveEnabled' - ), - 'ALTER TABLE IR_Playbook CHANGE COLUMN ExportChannelOnArchiveEnabled ExportChannelOnFinishedEnabled BOOLEAN NOT NULL DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql deleted file mode 100644 index 8ffaf405559..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnFinishedEnabled' - ), - 'ALTER TABLE IR_Incident CHANGE COLUMN ExportChannelOnFinishedEnabled ExportChannelOnArchiveEnabled BOOLEAN NOT NULL DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql deleted file mode 100644 index ef6be06d5a6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ExportChannelOnArchiveEnabled' - ), - 'ALTER TABLE IR_Incident CHANGE COLUMN ExportChannelOnArchiveEnabled ExportChannelOnFinishedEnabled BOOLEAN NOT NULL DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.down.sql deleted file mode 100644 index 4d719ebdcfd..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND column_name = 'Status' - ), - 'ALTER TABLE IR_StatusPosts ADD COLUMN Status VARCHAR(1024) NOT NULL DEFAULT "";', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.up.sql deleted file mode 100644 index 2099192fce0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000056_drop_status_from_ir_status_posts.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND column_name = 'Status' - ), - 'ALTER TABLE IR_StatusPosts DROP COLUMN Status;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.down.sql deleted file mode 100644 index 27477f18708..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.down.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE IR_Incident -SET CurrentStatus = - CASE - WHEN CurrentStatus = 'Finished' - THEN 'Archived' - ELSE 'InProgress' - END; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.up.sql deleted file mode 100644 index 8e7dd8e8b2d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000057_update_current_status_in_ir_incident.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE IR_Incident -SET CurrentStatus = - CASE - WHEN CurrentStatus = 'Archived' - THEN 'Finished' - ELSE 'InProgress' - END; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.down.sql deleted file mode 100644 index 5392be61386..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'CategoryName' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN CategoryName;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.up.sql deleted file mode 100644 index 4cff2fa21f3..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000058_add_category_name_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'CategoryName' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN CategoryName TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Playbook -SET CategoryName = 'Playbook Runs' -WHERE CategorizeChannelEnabled=1; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.down.sql deleted file mode 100644 index 1a894d2e7c1..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CategoryName' - ), - 'ALTER TABLE IR_Incident DROP COLUMN CategoryName;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.up.sql deleted file mode 100644 index 1551dc75450..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000059_add_category_name_to_ir_incident.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'CategoryName' - ), - 'ALTER TABLE IR_Incident ADD COLUMN CategoryName TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident -SET CategoryName = 'Playbook Runs' -WHERE CategorizeChannelEnabled=1; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql deleted file mode 100644 index d4489be5844..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedBroadcastChannelIds' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ConcatenatedBroadcastChannelIds;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql deleted file mode 100644 index 87c649f9ddd..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql +++ /dev/null @@ -1,25 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedBroadcastChannelIds' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ConcatenatedBroadcastChannelIds TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -UPDATE IR_Incident SET - ConcatenatedBroadcastChannelIds = ( - COALESCE( - CONCAT_WS( - ',', - CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END, - CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END - ), - '') - ); diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql deleted file mode 100644 index 845e3d6d9fa..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql +++ /dev/null @@ -1,29 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedBroadcastChannelIds' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN ConcatenatedBroadcastChannelIds;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; - -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'BroadcastEnabled' - ), - 'ALTER TABLE IR_Playbook DROP COLUMN BroadcastEnabled;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql deleted file mode 100644 index 2714d6d751d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql +++ /dev/null @@ -1,46 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedBroadcastChannelIds' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN ConcatenatedBroadcastChannelIds TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'BroadcastEnabled' - ), - 'ALTER TABLE IR_Playbook ADD COLUMN BroadcastEnabled BOOLEAN DEFAULT FALSE;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; - - -UPDATE IR_Playbook SET - ConcatenatedBroadcastChannelIds = ( - COALESCE( - CONCAT_WS( - ',', - CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END, - CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END - ), - '') - ) -, BroadcastEnabled = (CASE - WHEN BroadcastChannelID != '' THEN TRUE - WHEN AnnouncementChannelEnabled = TRUE THEN TRUE - ELSE FALSE -END) diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.down.sql deleted file mode 100644 index faa79bfdee1..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ChannelIDToRootID' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ChannelIDToRootID;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.up.sql deleted file mode 100644 index a1e0441d86a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000062_add_channel_id_to_root_id_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ChannelIDToRootID' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ChannelIDToRootID TEXT;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.down.sql deleted file mode 100644 index e0ac49d1ecf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.up.sql deleted file mode 100644 index bb46dfd8d0d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000063_convert_charset_of_ir_system.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_System' - AND table_schema = DATABASE() - ), - 'ALTER TABLE IR_System CONVERT TO CHARACTER SET utf8mb4;', - 'SELECT 1;' -)); - -PREPARE changeTableCharacterSetIfExists FROM @preparedStatement; -EXECUTE changeTableCharacterSetIfExists; -DEALLOCATE PREPARE changeTableCharacterSetIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.down.sql deleted file mode 100644 index 58c23655fe4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_PlaybookMember' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_PlaybookMember DROP PRIMARY KEY;', - 'SELECT 1;' -)); - -PREPARE dropPrimaryKeyIExists FROM @preparedStatement; -EXECUTE dropPrimaryKeyIExists; -DEALLOCATE PREPARE dropPrimaryKeyIExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.up.sql deleted file mode 100644 index 2bb6f2641bc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000064_add_pr_key_ir_playbook_member.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_PlaybookMember' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_PlaybookMember ADD PRIMARY KEY (MemberID, PlaybookID);', - 'SELECT 1;' -)); - -PREPARE addPrimaryKeyIfNotExists FROM @preparedStatement; -EXECUTE addPrimaryKeyIfNotExists; -DEALLOCATE PREPARE addPrimaryKeyIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.down.sql deleted file mode 100644 index b9c9fca2933..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND index_name = 'posts_unique' - ), - 'ALTER TABLE IR_StatusPosts ADD CONSTRAINT posts_unique UNIQUE (IncidentID, PostID)', - 'SELECT 1' -)); - -PREPARE createIndexIfNotExists FROM @preparedStatement; -EXECUTE createIndexIfNotExists; -DEALLOCATE PREPARE createIndexIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.up.sql deleted file mode 100644 index 9d02549490b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000065_drop_index_posts_unique.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND index_name = 'posts_unique' - ), - 'DROP INDEX posts_unique ON IR_StatusPosts;', - 'SELECT 1' -)); - -PREPARE removeIndexIfExists FROM @preparedStatement; -EXECUTE removeIndexIfExists; -DEALLOCATE PREPARE removeIndexIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.down.sql deleted file mode 100644 index a0a5fc272ee..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_StatusPosts DROP PRIMARY KEY;', - 'SELECT 1;' -)); - -PREPARE dropPrimaryKeyIExists FROM @preparedStatement; -EXECUTE dropPrimaryKeyIExists; -DEALLOCATE PREPARE dropPrimaryKeyIExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.up.sql deleted file mode 100644 index af6cfa56091..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000066_add_pr_key_ir_status_posts.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_StatusPosts ADD PRIMARY KEY (IncidentID, PostID);', - 'SELECT 1;' -)); - -PREPARE addPrimaryKeyIfNotExists FROM @preparedStatement; -EXECUTE addPrimaryKeyIfNotExists; -DEALLOCATE PREPARE addPrimaryKeyIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.down.sql deleted file mode 100644 index 65cbcb6db77..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_TimelineEvent' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_TimelineEvent DROP PRIMARY KEY;', - 'SELECT 1;' -)); - -PREPARE dropPrimaryKeyIExists FROM @preparedStatement; -EXECUTE dropPrimaryKeyIExists; -DEALLOCATE PREPARE dropPrimaryKeyIExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.up.sql deleted file mode 100644 index 1d94d4a71da..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000067_add_pr_key_ir_timeline_event.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_TimelineEvent' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_TimelineEvent ADD PRIMARY KEY (ID);', - 'SELECT 1;' -)); - -PREPARE addPrimaryKeyIfNotExists FROM @preparedStatement; -EXECUTE addPrimaryKeyIfNotExists; -DEALLOCATE PREPARE addPrimaryKeyIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql deleted file mode 100644 index bb3e149df42..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_ViewedChannel' - AND table_schema = DATABASE() - AND index_name = 'IR_ViewedChannel_ChannelID_UserID' - ) , - 'CREATE UNIQUE INDEX IR_ViewedChannel_ChannelID_UserID ON IR_ViewedChannel (ChannelID, UserID)', - 'SELECT 1' -)); - -PREPARE createIndexIfNotExists FROM @preparedStatement; -EXECUTE createIndexIfNotExists; -DEALLOCATE PREPARE createIndexIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql deleted file mode 100644 index 82815c2ffb5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_ViewedChannel' - AND table_schema = DATABASE() - AND index_name = 'IR_ViewedChannel_ChannelID_UserID' - ) , - 'DROP INDEX IR_ViewedChannel_ChannelID_UserID ON IR_ViewedChannel', - 'SELECT 1' -)); - -PREPARE removeIndexIfExists FROM @preparedStatement; -EXECUTE removeIndexIfExists; -DEALLOCATE PREPARE removeIndexIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.down.sql deleted file mode 100644 index 8914111dd93..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_ViewedChannel' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_ViewedChannel DROP PRIMARY KEY;', - 'SELECT 1;' -)); - -PREPARE dropPrimaryKeyIExists FROM @preparedStatement; -EXECUTE dropPrimaryKeyIExists; -DEALLOCATE PREPARE dropPrimaryKeyIExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.up.sql deleted file mode 100644 index 4a3842f98bf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000069_add_pr_key_ir_viewed_channel.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE table_name = 'IR_ViewedChannel' - AND table_schema = (SELECT DATABASE()) - AND constraint_type = 'PRIMARY KEY' - ), - 'ALTER TABLE IR_ViewedChannel ADD PRIMARY KEY (ChannelID, UserID);', - 'SELECT 1;' -)); - -PREPARE addPrimaryKeyIfNotExists FROM @preparedStatement; -EXECUTE addPrimaryKeyIfNotExists; -DEALLOCATE PREPARE addPrimaryKeyIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql deleted file mode 100644 index 27f3363dff2..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE IGNORE PluginKeyValueStore -SET PluginId='com.mattermost.plugin-incident-management' -WHERE PluginId='playbooks' diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql deleted file mode 100644 index 43f6fbd82bf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE IGNORE PluginKeyValueStore -SET PluginId='playbooks' -WHERE PluginId='com.mattermost.plugin-incident-management' diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql deleted file mode 100644 index 2478d213aa9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReminderTimerDefaultSeconds' - ), - 'ALTER TABLE IR_Incident DROP COLUMN ReminderTimerDefaultSeconds;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql deleted file mode 100644 index eb488798cae..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ReminderTimerDefaultSeconds' - ), - 'ALTER TABLE IR_Incident ADD COLUMN ReminderTimerDefaultSeconds BIGINT NOT NULL DEFAULT 0;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.down.sql deleted file mode 100644 index 432498284a1..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedWebhookOnCreationURLs' - ), - 'ALTER TABLE IR_Playbook CHANGE COLUMN ConcatenatedWebhookOnCreationURLs WebhookOnCreationURL TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.up.sql deleted file mode 100644 index 6c80ac5213c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000072_rename_webhook_on_creation_url_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationURL' - ), - 'ALTER TABLE IR_Playbook CHANGE COLUMN WebhookOnCreationURL ConcatenatedWebhookOnCreationURLs TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql deleted file mode 100644 index 1b02d0fa769..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedWebhookOnStatusUpdateURLs' - ), - 'ALTER TABLE IR_Playbook CHANGE COLUMN ConcatenatedWebhookOnStatusUpdateURLs WebhookOnStatusUpdateURL TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql deleted file mode 100644 index f13afeb30b7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Playbook' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateURL' - ), - 'ALTER TABLE IR_Playbook CHANGE COLUMN WebhookOnStatusUpdateURL ConcatenatedWebhookOnStatusUpdateURLs TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.down.sql deleted file mode 100644 index c0f7e61ff3f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedWebhookOnCreationURLs' - ), - 'ALTER TABLE IR_Incident CHANGE COLUMN ConcatenatedWebhookOnCreationURLs WebhookOnCreationURL TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.up.sql deleted file mode 100644 index c4969ffc250..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000074_rename_webhook_on_creation_url_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnCreationURL' - ), - 'ALTER TABLE IR_Incident CHANGE COLUMN WebhookOnCreationURL ConcatenatedWebhookOnCreationURLs TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.down.sql deleted file mode 100644 index a5534ee983b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'ConcatenatedWebhookOnStatusUpdateURLs' - ), - 'ALTER TABLE IR_Incident CHANGE COLUMN ConcatenatedWebhookOnStatusUpdateURLs WebhookOnStatusUpdateURL TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.up.sql deleted file mode 100644 index cd6effdee23..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000075_rename_webhook_on_status_update_url_ir_incident.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_Incident' - AND table_schema = DATABASE() - AND column_name = 'WebhookOnStatusUpdateURL' - ), - 'ALTER TABLE IR_Incident CHANGE COLUMN WebhookOnStatusUpdateURL ConcatenatedWebhookOnStatusUpdateURLs TEXT;', - 'SELECT 1;' -)); - -PREPARE renameColumnIfExists FROM @preparedStatement; -EXECUTE renameColumnIfExists; -DEALLOCATE PREPARE renameColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.down.sql deleted file mode 100644 index 89750d3b6d6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_UserInfo; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.up.sql deleted file mode 100644 index 0976f84492a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000076_create_ir_userinfo.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_UserInfo ( - ID VARCHAR(26) PRIMARY KEY, - LastDailyTodoDMAt BIGINT -) DEFAULT CHARACTER SET utf8mb4; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql deleted file mode 100644 index ba4fa592e98..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_UserInfo' - AND table_schema = DATABASE() - AND column_name = 'DigestNotificationSettingsJSON' - ), - 'ALTER TABLE IR_UserInfo DROP COLUMN DigestNotificationSettingsJSON;', - 'SELECT 1;' -)); - -PREPARE removeColumnIfExists FROM @preparedStatement; -EXECUTE removeColumnIfExists; -DEALLOCATE PREPARE removeColumnIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql deleted file mode 100644 index ce8d0ee78f8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - NOT EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_name = 'IR_UserInfo' - AND table_schema = DATABASE() - AND column_name = 'DigestNotificationSettingsJSON' - ), - 'ALTER TABLE IR_UserInfo ADD COLUMN DigestNotificationSettingsJSON JSON;', - 'SELECT 1;' -)); - -PREPARE addColumnIfNotExists FROM @preparedStatement; -EXECUTE addColumnIfNotExists; -DEALLOCATE PREPARE addColumnIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.down.sql deleted file mode 100644 index 952bfa56469..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.down.sql +++ /dev/null @@ -1,20 +0,0 @@ -SELECT 1; - --- we retroactively dropped this index in "0.29.0 > 0.30.0" to fix the blocked migration, --- and then repeated it in a new migration("0.35.0 > 0.36.0") too so that the schemas remain in sync. --- so we don't need to restore index here - --- SET @preparedStatement = (SELECT IF( --- NOT EXISTS( --- SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS --- WHERE table_name = 'IR_StatusPosts' --- AND table_schema = DATABASE() --- AND index_name = 'posts_unique' --- ), --- 'ALTER TABLE IR_StatusPosts ADD CONSTRAINT posts_unique UNIQUE (IncidentID, PostID)', --- 'SELECT 1' --- )); - --- PREPARE createIndexIfNotExists FROM @preparedStatement; --- EXECUTE createIndexIfNotExists; --- DEALLOCATE PREPARE createIndexIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.up.sql deleted file mode 100644 index 622b7229e4d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000078_drop_index_ir_statusposts_posts_unique2.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_StatusPosts' - AND table_schema = DATABASE() - AND index_name = 'posts_unique' - ) , - 'DROP INDEX posts_unique ON IR_StatusPosts', - 'SELECT 1' -)); - -PREPARE removeIndexIfExists FROM @preparedStatement; -EXECUTE removeIndexIfExists; -DEALLOCATE PREPARE removeIndexIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql b/server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql deleted file mode 100644 index c77bc4c4f1d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql +++ /dev/null @@ -1,20 +0,0 @@ -SELECT 1; - --- we retroactively dropped this index in "0.29.0 > 0.30.0" to fix the blocked migration, --- and then repeated it in a new migration("0.35.0 > 0.36.0") too so that the schemas remain in sync. --- so we don't need to restore index here - --- SET @preparedStatement = (SELECT IF( --- NOT EXISTS( --- SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS --- WHERE table_name = 'IR_ViewedChannel' --- AND table_schema = DATABASE() --- AND index_name = 'IR_ViewedChannel_ChannelID_UserID' --- ) , --- 'CREATE UNIQUE INDEX IR_ViewedChannel_ChannelID_UserID ON IR_ViewedChannel (ChannelID, UserID)', --- 'SELECT 1' --- )); - --- PREPARE createIndexIfNotExists FROM @preparedStatement; --- EXECUTE createIndexIfNotExists; --- DEALLOCATE PREPARE createIndexIfNotExists; diff --git a/server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql b/server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql deleted file mode 100644 index 82815c2ffb5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/mysql/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -SET @preparedStatement = (SELECT IF( - EXISTS( - SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_name = 'IR_ViewedChannel' - AND table_schema = DATABASE() - AND index_name = 'IR_ViewedChannel_ChannelID_UserID' - ) , - 'DROP INDEX IR_ViewedChannel_ChannelID_UserID ON IR_ViewedChannel', - 'SELECT 1' -)); - -PREPARE removeIndexIfExists FROM @preparedStatement; -EXECUTE removeIndexIfExists; -DEALLOCATE PREPARE removeIndexIfExists; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.down.sql deleted file mode 100644 index aec796afa5c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_System; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.up.sql deleted file mode 100644 index 9662fa26f45..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000001_create_IR_system.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_System ( - SKey VARCHAR(64) PRIMARY KEY, - SValue VARCHAR(1024) NULL -); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.down.sql deleted file mode 100644 index c8af882bb49..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_Incident; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.up.sql deleted file mode 100644 index 5baaa4fd2b6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000002_create_IR_incident.up.sql +++ /dev/null @@ -1,20 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_Incident ( - ID TEXT PRIMARY KEY, - Name TEXT NOT NULL, - Description TEXT NOT NULL, - IsActive BOOLEAN NOT NULL, - CommanderUserID TEXT NOT NULL, - TeamID TEXT NOT NULL, - ChannelID TEXT NOT NULL UNIQUE, - CreateAt BIGINT NOT NULL, - EndAt BIGINT NOT NULL DEFAULT 0, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ActiveStage BIGINT NOT NULL, - PostID TEXT NOT NULL DEFAULT '', - PlaybookID TEXT NOT NULL DEFAULT '', - ChecklistsJSON JSON NOT NULL -); - -CREATE INDEX IF NOT EXISTS IR_Incident_TeamID ON IR_Incident (TeamID); -CREATE INDEX IF NOT EXISTS IR_Incident_TeamID_CommanderUserID ON IR_Incident (TeamID, CommanderUserID); -CREATE INDEX IF NOT EXISTS IR_Incident_ChannelID ON IR_Incident (ChannelID); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.down.sql deleted file mode 100644 index 79e50aab1a4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_Playbook; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.up.sql deleted file mode 100644 index 37d5e562f50..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000003_create_ir_playbook.up.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_Playbook ( - ID TEXT PRIMARY KEY, - Title TEXT NOT NULL, - Description TEXT NOT NULL, - TeamID TEXT NOT NULL, - CreatePublicIncident BOOLEAN NOT NULL, - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - ChecklistsJSON JSON NOT NULL, - NumStages BIGINT NOT NULL DEFAULT 0, - NumSteps BIGINT NOT NULL DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS IR_Playbook_TeamID ON IR_Playbook (TeamID); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.down.sql deleted file mode 100644 index 6edfe5ff386..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_PlaybookMember; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.up.sql deleted file mode 100644 index ae48fb50569..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000004_create_ir_playbookmember.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_PlaybookMember ( - PlaybookID TEXT NOT NULL REFERENCES IR_Playbook(ID), - MemberID TEXT NOT NULL, - UNIQUE (PlaybookID, MemberID) -); - -CREATE INDEX IF NOT EXISTS IR_PlaybookMember_PlaybookID ON IR_PlaybookMember (PlaybookID); -CREATE INDEX IF NOT EXISTS IR_PlaybookMember_MemberID ON IR_PlaybookMember (MemberID); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.down.sql deleted file mode 100644 index c3e29d13116..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ActiveStageTitle; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.up.sql deleted file mode 100644 index 44a6ecdf5d3..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000005_add_active_stage_title_to_ir_incident.up.sql +++ /dev/null @@ -1,22 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ActiveStageTitle TEXT DEFAULT ''; - -CREATE OR REPLACE function json_array_length_safe(p_json text) -RETURNS integer -LANGUAGE plpgsql -AS -' -BEGIN - RETURN json_array_length(p_json::json); -EXCEPTION - WHEN OTHERS THEN - RETURN -1; -END; -' -IMMUTABLE; - -UPDATE ir_incident -SET activestagetitle = checklistsjson::json->(activestage::INTEGER)->>'title' -WHERE json_array_length_safe(checklistsjson::text) > activestage -AND activestage >= 0; - -DROP FUNCTION IF EXISTS json_array_length_safe; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.down.sql deleted file mode 100644 index 8fc426811d9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_StatusPosts; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.up.sql deleted file mode 100644 index 0e929235898..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000006_create_ir_status_posts.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_StatusPosts ( - IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID), - PostID TEXT NOT NULL, - UNIQUE (IncidentID, PostID) -); - -CREATE INDEX IF NOT EXISTS IR_StatusPosts_IncidentID ON IR_StatusPosts (IncidentID); -CREATE INDEX IF NOT EXISTS IR_StatusPosts_PostID ON IR_StatusPosts (PostID); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.down.sql deleted file mode 100644 index 0f4ac419a19..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ReminderPostID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.up.sql deleted file mode 100644 index 9222ffacf17..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000007_add_reminder_post_id_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ReminderPostID TEXT; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.down.sql deleted file mode 100644 index 6e56cdf972b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS BroadcastChannelID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.up.sql deleted file mode 100644 index a43aed79092..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000008_add_broadcast_channel_id_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS BroadcastChannelID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.down.sql deleted file mode 100644 index e393812c445..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS BroadcastChannelID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.up.sql deleted file mode 100644 index 1747d6acf3f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000009_add_broadcast_channel_id_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS BroadcastChannelID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.down.sql deleted file mode 100644 index 2bb89a8b6c7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS PreviousReminder; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.up.sql deleted file mode 100644 index 58157c376db..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000010_add_previous_reminder_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS PreviousReminder BIGINT NOT NULL DEFAULT 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.down.sql deleted file mode 100644 index abba3ec7054..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ReminderMessageTemplate; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.up.sql deleted file mode 100644 index f7629f4e5d6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000011_add_reminder_message_template_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ReminderMessageTemplate TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.down.sql deleted file mode 100644 index 84bef579318..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ReminderMessageTemplate; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.up.sql deleted file mode 100644 index 95262270ab8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000012_add_reminder_message_template_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ReminderMessageTemplate TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql deleted file mode 100644 index 73febaa808e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ReminderTimerDefaultSeconds; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql deleted file mode 100644 index 3840003c9d5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000013_add_reminder_timer_default_seconds_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ReminderTimerDefaultSeconds BIGINT NOT NULL DEFAULT 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.down.sql deleted file mode 100644 index 56dacd78845..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS CurrentStatus; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.up.sql deleted file mode 100644 index 1511fe8a05d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000014_add_current_status_to_ir_incident.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS CurrentStatus TEXT NOT NULL DEFAULT 'Active'; - -UPDATE IR_Incident -SET CurrentStatus = 'Resolved' -WHERE EndAt != 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.down.sql deleted file mode 100644 index f1178910aa9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_StatusPosts DROP COLUMN IF EXISTS Status; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.up.sql deleted file mode 100644 index 7f527034472..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000015_add_status_to_ir_status_posts.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_StatusPosts ADD COLUMN IF NOT EXISTS Status TEXT NOT NULL DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.down.sql deleted file mode 100644 index d218e43a0b0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_TimelineEvent; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.up.sql deleted file mode 100644 index fe745d1a594..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000016_create_ir_timeline_event.up.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_TimelineEvent ( - ID TEXT NOT NULL, - IncidentID TEXT NOT NULL REFERENCES IR_Incident(ID), - CreateAt BIGINT NOT NULL, - DeleteAt BIGINT NOT NULL DEFAULT 0, - EventAt BIGINT NOT NULL, - EventType TEXT NOT NULL DEFAULT '', - Summary TEXT NOT NULL DEFAULT '', - Details TEXT NOT NULL DEFAULT '', - PostID TEXT NOT NULL DEFAULT '', - SubjectUserID TEXT NOT NULL DEFAULT '', - CreatorUserID TEXT NOT NULL DEFAULT '' -); - -CREATE INDEX IF NOT EXISTS IR_TimelineEvent_ID ON IR_TimelineEvent (ID); -CREATE INDEX IF NOT EXISTS IR_TimelineEvent_IncidentID ON IR_TimelineEvent (IncidentID); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.down.sql deleted file mode 100644 index 10528c28373..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ReporterUserID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.up.sql deleted file mode 100644 index 81d67f53679..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000017_add_reporter_user_id_to_ir_incident.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ReporterUserID TEXT NOT NULL DEFAULT ''; - -UPDATE IR_Incident -SET ReporterUserID = CommanderUserID -WHERE ReporterUserID = '' diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql deleted file mode 100644 index 16bc36a9f7c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ConcatenatedInvitedUserIDs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql deleted file mode 100644 index 5df47836b9d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000018_add_concatenated_invited_user_ids_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ConcatenatedInvitedUserIDs TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql deleted file mode 100644 index 78be99543c2..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ConcatenatedInvitedUserIDs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql deleted file mode 100644 index 1bab4380d28..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000019_add_concatenated_invited_useri_ds_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ConcatenatedInvitedUserIDs TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.down.sql deleted file mode 100644 index b7cb847a428..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS InviteUsersEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.up.sql deleted file mode 100644 index d187798a091..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000020_add_invite_users_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS InviteUsersEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.down.sql deleted file mode 100644 index 243c84ccd75..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS DefaultCommanderID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.up.sql deleted file mode 100644 index c8df14fba20..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000021_add_default_commander_id_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS DefaultCommanderID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.down.sql deleted file mode 100644 index 23285a2ba82..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS DefaultCommanderID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.up.sql deleted file mode 100644 index 7613a2057f8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000022_add_default_commander_id_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS DefaultCommanderID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 3ee611defd8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS DefaultCommanderEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 49bc6327435..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000023_add_default_commander_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS DefaultCommanderEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.down.sql deleted file mode 100644 index 027b7d63fdb..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.up.sql deleted file mode 100644 index c5e8fdf090e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000024_update_created_at_deleted_at_in_ir_incident.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE IR_Incident -SET CreateAt = Channels.CreateAt, - DeleteAt = Channels.DeleteAt -FROM Channels -WHERE IR_Incident.CreateAt = 0 - AND IR_Incident.DeleteAt = 0 - AND IR_Incident.ChannelID = Channels.ID \ No newline at end of file diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.down.sql deleted file mode 100644 index ee3068c7fd3..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS AnnouncementChannelID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.up.sql deleted file mode 100644 index f9088cf3df7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000025_add_announcement_channel_id_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS AnnouncementChannelID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.down.sql deleted file mode 100644 index a3ba1ef599c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS AnnouncementChannelID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.up.sql deleted file mode 100644 index aaf197369d1..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000026_add_announcement_channel_id_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS AnnouncementChannelID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql deleted file mode 100644 index af18aac6edc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS AnnouncementChannelEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql deleted file mode 100644 index b5158b4dc16..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000027_add_announcement_channel_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS AnnouncementChannelEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.down.sql deleted file mode 100644 index f7279af9ae9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS WebhookOnCreationURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.up.sql deleted file mode 100644 index 95e46b3cee4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000028_add_webhook_on_creation_url_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS WebhookOnCreationURL TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql deleted file mode 100644 index a68aa83d2e6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS WebhookOnCreationURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql deleted file mode 100644 index cc5619fc444..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000029_add_webhook_on_creation_url_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS WebhookOnCreationURL TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 2575f431b48..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS WebhookOnCreationEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 3e3b1ec4371..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000030_add_webhook_on_creation_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS WebhookOnCreationEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql deleted file mode 100644 index 19d66eac35d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ConcatenatedInvitedGroupIDs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql deleted file mode 100644 index 1695a9bf8a7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000031_add_concatenated_invited_group_ids_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ConcatenatedInvitedGroupIDs TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql deleted file mode 100644 index 7ee261e27f8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ConcatenatedInvitedGroupIDs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql deleted file mode 100644 index 237efee2670..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000032_add_concatenated_invited_group_ids_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ConcatenatedInvitedGroupIDs TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.down.sql deleted file mode 100644 index 97874fd05ff..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS Retrospective; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.up.sql deleted file mode 100644 index 2a44e1b2de4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000033_add_retrospective_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS Retrospective TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.down.sql deleted file mode 100644 index b92c6772dc6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS MessageOnJoin; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.up.sql deleted file mode 100644 index 66a4f775daf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000034_add_message_on_join_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS MessageOnJoin TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 3346af40d47..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS MessageOnJoinEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 7c9e1cf20d7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000035_add_message_on_join_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS MessageOnJoinEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.down.sql deleted file mode 100644 index aab8d48a405..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS MessageOnJoin; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.up.sql deleted file mode 100644 index a1fb4dab15e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000036_add_message_on_join_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS MessageOnJoin TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.down.sql deleted file mode 100644 index a7917dc65eb..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_ViewedChannel; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.up.sql deleted file mode 100644 index 8bd7721b423..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000037_create_ir_viewed_channel.up.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_ViewedChannel ( - ChannelID TEXT NOT NULL, - UserID TEXT NOT NULL -); - -CREATE UNIQUE INDEX IF NOT EXISTS IR_ViewedChannel_ChannelID_UserID ON IR_ViewedChannel (ChannelID, UserID); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.down.sql deleted file mode 100644 index 83f1212a77f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS RetrospectivePublishedAt; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.up.sql deleted file mode 100644 index 399c7a8bc78..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000038_add_retrospective_published_at_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS RetrospectivePublishedAt BIGINT NOT NULL DEFAULT 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql deleted file mode 100644 index c06d8550168..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS RetrospectiveReminderIntervalSeconds; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql deleted file mode 100644 index e1f812aa858..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000039_add_retrospective_reminder_interval_seconds_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS RetrospectiveReminderIntervalSeconds BIGINT NOT NULL DEFAULT 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql deleted file mode 100644 index 68e7d892358..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS RetrospectiveReminderIntervalSeconds; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql deleted file mode 100644 index cb84119bf72..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000040_add_retrospective_reminder_interval_seconds_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS RetrospectiveReminderIntervalSeconds BIGINT NOT NULL DEFAULT 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.down.sql deleted file mode 100644 index 588a4372e5a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS RetrospectiveWasCanceled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.up.sql deleted file mode 100644 index da44629e95c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000041_add_retrospective_was_canceled_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS RetrospectiveWasCanceled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.down.sql deleted file mode 100644 index a0884903831..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS RetrospectiveTemplate; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.up.sql deleted file mode 100644 index 2528df86fbf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000042_add_retrospective_template_to_ir_playbook.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS RetrospectiveTemplate TEXT; - -UPDATE IR_Playbook -SET RetrospectiveTemplate = '' -WHERE RetrospectiveTemplate IS NULL diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql deleted file mode 100644 index 0ab6d28637b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS WebhookOnStatusUpdateURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql deleted file mode 100644 index 51c396ca97a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000043_add_webhook_on_status_update_url_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS WebhookOnStatusUpdateURL TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql deleted file mode 100644 index ae94bbf23c7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS WebhookOnStatusUpdateEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 293523dac60..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000044_add_webhook_on_status_update_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS WebhookOnStatusUpdateEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql deleted file mode 100644 index 7745ff2c60d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS WebhookOnStatusUpdateURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql deleted file mode 100644 index 3dc61087d3a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000045_add_webhook_on_status_update_url_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS WebhookOnStatusUpdateURL TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql deleted file mode 100644 index c922fc7e22f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ConcatenatedSignalAnyKeywords; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql deleted file mode 100644 index 4300ae89d29..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000046_add_concatenated_signal_any_keywords_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ConcatenatedSignalAnyKeywords TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 76782ee7ae4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS SignalAnyKeywordsEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 51ff2505e8e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000047_add_signal_any_keywords_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS SignalAnyKeywordsEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.down.sql deleted file mode 100644 index 5ee4c2dfdf8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS UpdateAt; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.up.sql deleted file mode 100644 index 21a8fe1cf16..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000048_add_update_at_to_ir_playbook.up.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS UpdateAt BIGINT NOT NULL DEFAULT 0; - -UPDATE IR_Playbook -SET UpdateAt = CreateAt; - -CREATE INDEX IF NOT EXISTS IR_Playbook_UpdateAt ON IR_Playbook (UpdateAt); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.down.sql deleted file mode 100644 index 3265d95e563..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS LastStatusUpdateAt; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.up.sql deleted file mode 100644 index f06c0f32267..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000049_add_last_status_update_at_to_ir_incident.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS LastStatusUpdateAt BIGINT DEFAULT 0; - -UPDATE IR_Incident as dest -SET LastStatusUpdateAt = src.LastStatusUpdateAt -FROM ( - SELECT i.ID as ID, COALESCE(MAX(p.CreateAt), i.CreateAt) as LastStatusUpdateAt - FROM IR_Incident as i - LEFT JOIN IR_StatusPosts as sp on i.ID = sp.IncidentID - LEFT JOIN Posts as p on sp.PostID = p.Id - GROUP BY i.ID -) as src -WHERE dest.ID = src.ID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql deleted file mode 100644 index dc3f1a54e9a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ExportChannelOnArchiveEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql deleted file mode 100644 index f9d97e12f89..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000050_add_export_channel_on_archive_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ExportChannelOnArchiveEnabled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql deleted file mode 100644 index 161870f9d94..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ExportChannelOnArchiveEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql deleted file mode 100644 index 2fa1ef328fe..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000051_add_export_channel_on_archive_enabled_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ExportChannelOnArchiveEnabled BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql deleted file mode 100644 index 3c87e883b65..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS CategorizeChannelEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql deleted file mode 100644 index 8a7748471b9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000052_add_categoriz_echannel_enabled_to_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS CategorizeChannelEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql deleted file mode 100644 index 1e8b7aeb86a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS CategorizeChannelEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql deleted file mode 100644 index 754c3c6ea63..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000053_add_categoriz_echannel_enabled_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS CategorizeChannelEnabled BOOLEAN DEFAULT FALSE; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql deleted file mode 100644 index facdf65acd5..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook RENAME COLUMN ExportChannelOnFinishedEnabled TO ExportChannelOnArchiveEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql deleted file mode 100644 index e18a9a3cbed..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000054_rename_export_channel_on_archive_enabled_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook RENAME COLUMN ExportChannelOnArchiveEnabled TO ExportChannelOnFinishedEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql deleted file mode 100644 index 65ba928ccbf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident RENAME COLUMN ExportChannelOnFinishedEnabled TO ExportChannelOnArchiveEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql deleted file mode 100644 index 7783fbdadb8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000055_rename_export_channel_on_archive_enabled_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident RENAME COLUMN ExportChannelOnArchiveEnabled TO ExportChannelOnFinishedEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.down.sql deleted file mode 100644 index 7f527034472..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_StatusPosts ADD COLUMN IF NOT EXISTS Status TEXT NOT NULL DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.up.sql deleted file mode 100644 index f1178910aa9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000056_drop_status_from_ir_status_posts.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_StatusPosts DROP COLUMN IF EXISTS Status; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.down.sql deleted file mode 100644 index 27477f18708..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.down.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE IR_Incident -SET CurrentStatus = - CASE - WHEN CurrentStatus = 'Finished' - THEN 'Archived' - ELSE 'InProgress' - END; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.up.sql deleted file mode 100644 index 8e7dd8e8b2d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000057_update_current_status_in_ir_incident.up.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE IR_Incident -SET CurrentStatus = - CASE - WHEN CurrentStatus = 'Archived' - THEN 'Finished' - ELSE 'InProgress' - END; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.down.sql deleted file mode 100644 index 88b039ce8cc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS CategoryName; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.up.sql deleted file mode 100644 index ecc46370731..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000058_add_category_name_to_ir_playbook.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS CategoryName TEXT DEFAULT ''; - -UPDATE IR_Playbook -SET CategoryName = 'Playbook Runs' -WHERE CategorizeChannelEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.down.sql deleted file mode 100644 index 91e6351ceb4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS CategoryName; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.up.sql deleted file mode 100644 index 0e1e498a767..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000059_add_category_name_to_ir_incident.up.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS CategoryName TEXT DEFAULT ''; - -UPDATE IR_Incident -SET CategoryName = 'Playbook Runs' -WHERE CategorizeChannelEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql deleted file mode 100644 index 784c33a720f..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ConcatenatedBroadcastChannelIds; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql deleted file mode 100644 index ae00e35a5d0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000060_add_concatenated_broadcast_channel_ids_to_ir_incident.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ConcatenatedBroadcastChannelIds TEXT; - -UPDATE IR_Incident SET - ConcatenatedBroadcastChannelIds = ( - COALESCE( - CONCAT_WS( - ',', - CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END, - CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END - ), - '') - ); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql deleted file mode 100644 index e2a40fa0d0d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS ConcatenatedBroadcastChannelIds; -ALTER TABLE IR_Playbook DROP COLUMN IF EXISTS BroadcastEnabled; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql deleted file mode 100644 index c7cdf8ce0c0..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000061_add_concatenated_broadcast_channel_ids_to_ir_playbook.up.sql +++ /dev/null @@ -1,18 +0,0 @@ -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS ConcatenatedBroadcastChannelIds TEXT; -ALTER TABLE IR_Playbook ADD COLUMN IF NOT EXISTS BroadcastEnabled BOOLEAN DEFAULT FALSE; - -UPDATE IR_Playbook SET - ConcatenatedBroadcastChannelIds = ( - COALESCE( - CONCAT_WS( - ',', - CASE WHEN AnnouncementChannelID = '' THEN NULL ELSE AnnouncementChannelID END, - CASE WHEN BroadcastChannelID = '' OR BroadcastChannelID = AnnouncementChannelID THEN NULL ELSE BroadcastChannelID END - ), - '') - ) -, BroadcastEnabled = (CASE - WHEN BroadcastChannelID != '' THEN TRUE - WHEN AnnouncementChannelEnabled = TRUE THEN TRUE - ELSE FALSE -END) diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.down.sql deleted file mode 100644 index eef441dffcc..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ChannelIDToRootID; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.up.sql deleted file mode 100644 index ee343919fef..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000062_add_channel_id_to_root_id_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ChannelIDToRootID TEXT DEFAULT ''; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.down.sql deleted file mode 100644 index e0ac49d1ecf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.down.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.up.sql deleted file mode 100644 index e0ac49d1ecf..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000063_convert_charset_of_ir_system.up.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT 1; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.down.sql deleted file mode 100644 index cef22a24d2b..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.down.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_playbookmember' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_PlaybookMember DROP CONSTRAINT ir_playbookmember_pkey; - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.up.sql deleted file mode 100644 index f41bf89b412..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000064_add_pr_key_ir_playbook_member.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_playbookmember' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_PlaybookMember ADD PRIMARY KEY (MemberID, PlaybookID); - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000065_drop_index_posts_unique.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000065_drop_index_posts_unique.down.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000065_drop_index_posts_unique.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000065_drop_index_posts_unique.up.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.down.sql deleted file mode 100644 index 02589810779..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.down.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_statusposts' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_StatusPosts DROP CONSTRAINT ir_statusposts_pkey; - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.up.sql deleted file mode 100644 index 1910a7fc2f4..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000066_add_pr_key_ir_status_posts.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_statusposts' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_StatusPosts ADD PRIMARY KEY (IncidentID, PostID); - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.down.sql deleted file mode 100644 index 9c4820e5e8a..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.down.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_timelineevent' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_TimelineEvent DROP CONSTRAINT ir_timelineevent_pkey; - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.up.sql deleted file mode 100644 index 716b3430559..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000067_add_pr_key_ir_timeline_event.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_timelineevent' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_TimelineEvent ADD PRIMARY KEY (ID); - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000068_drop_index_ir_viewed_channel_channelid_userid.down.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000068_drop_index_ir_viewed_channel_channelid_userid.up.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.down.sql deleted file mode 100644 index 24a3f3d5ae8..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.down.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_viewedchannel' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_ViewedChannel DROP CONSTRAINT ir_viewedchannel_pkey; - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.up.sql deleted file mode 100644 index ea82c37f514..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000069_add_pr_key_ir_viewed_channel.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -DO -$$ -BEGIN - IF NOT EXISTS ( - SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME = 'ir_viewedchannel' - AND CONSTRAINT_TYPE = 'PRIMARY KEY' - AND TABLE_CATALOG = (SELECT CURRENT_DATABASE()) - ) THEN - ALTER TABLE IR_ViewedChannel ADD PRIMARY KEY (ChannelID, UserID); - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql deleted file mode 100644 index 5c8a141fad9..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.down.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE PluginKeyValueStore k -SET PluginId='com.mattermost.plugin-incident-management' -WHERE PluginId='playbooks' -AND NOT EXISTS ( SELECT 1 FROM PluginKeyValueStore WHERE PluginId='com.mattermost.plugin-incident-management' AND PKey = k.PKey ) diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql deleted file mode 100644 index 6f6f60dfa64..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000070_update_update_plugin_id_in_plugin_key_value_store.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -UPDATE PluginKeyValueStore k -SET PluginId='playbooks' -WHERE PluginId='com.mattermost.plugin-incident-management' -AND NOT EXISTS ( SELECT 1 FROM PluginKeyValueStore WHERE PluginId='playbooks' AND PKey = k.PKey ) diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql deleted file mode 100644 index 80d14858178..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident DROP COLUMN IF EXISTS ReminderTimerDefaultSeconds; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql deleted file mode 100644 index 8ea51f46e5d..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000071_add_reminder_timer_default_seconds_to_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident ADD COLUMN IF NOT EXISTS ReminderTimerDefaultSeconds BIGINT NOT NULL DEFAULT 0; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.down.sql deleted file mode 100644 index da13a837598..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook RENAME COLUMN ConcatenatedWebhookOnCreationURLs TO WebhookOnCreationURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.up.sql deleted file mode 100644 index ffeb85f30ab..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000072_rename_webhook_on_creation_url_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook RENAME COLUMN WebhookOnCreationURL TO ConcatenatedWebhookOnCreationURLs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql deleted file mode 100644 index 798677a1e48..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook RENAME COLUMN ConcatenatedWebhookOnStatusUpdateURLs TO WebhookOnStatusUpdateURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql deleted file mode 100644 index 01ef9cb4727..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000073_rename_webhook_on_status_update_url_ir_playbook.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Playbook RENAME COLUMN WebhookOnStatusUpdateURL TO ConcatenatedWebhookOnStatusUpdateURLs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.down.sql deleted file mode 100644 index 292098b835e..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident RENAME COLUMN ConcatenatedWebhookOnCreationURLs TO WebhookOnCreationURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.up.sql deleted file mode 100644 index c0578e48698..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000074_rename_webhook_on_creation_url_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident RENAME COLUMN WebhookOnCreationURL TO ConcatenatedWebhookOnCreationURLs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.down.sql deleted file mode 100644 index 870128b6647..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident RENAME COLUMN ConcatenatedWebhookOnStatusUpdateURLs TO WebhookOnStatusUpdateURL; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.up.sql deleted file mode 100644 index bce7697a65c..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000075_rename_webhook_on_status_update_url_ir_incident.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_Incident RENAME COLUMN WebhookOnStatusUpdateURL TO ConcatenatedWebhookOnStatusUpdateURLs; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.down.sql deleted file mode 100644 index 89750d3b6d6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS IR_UserInfo; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.up.sql deleted file mode 100644 index 7d1a01d0ee7..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000076_create_ir_userinfo.up.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE IF NOT EXISTS IR_UserInfo ( - ID TEXT PRIMARY KEY, - LastDailyTodoDMAt BIGINT -); diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql deleted file mode 100644 index 00f3c3e0df6..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_UserInfo DROP COLUMN IF EXISTS DigestNotificationSettingsJSON; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql deleted file mode 100644 index 6ab9c8034c2..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000077_add_digest_notification_settings_json_to_ir_userinfo.up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE IR_UserInfo ADD COLUMN IF NOT EXISTS DigestNotificationSettingsJSON JSON; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000078_drop_index_ir_statusposts_posts_unique2.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000078_drop_index_ir_statusposts_posts_unique2.down.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000078_drop_index_ir_statusposts_posts_unique2.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000078_drop_index_ir_statusposts_posts_unique2.up.sql deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql b/server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql deleted file mode 100644 index 67b866165ef..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.down.sql +++ /dev/null @@ -1,8 +0,0 @@ -DO -$$ -BEGIN - IF to_regclass('IR_ViewedChannel_ChannelID_UserID') IS NULL THEN - CREATE UNIQUE INDEX IR_ViewedChannel_ChannelID_UserID ON IR_ViewedChannel (ChannelID, UserID); - END IF; -END -$$; diff --git a/server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql b/server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql deleted file mode 100644 index 56b47123307..00000000000 --- a/server/playbooks/server/sqlstore/migrations/postgres/000079_drop_index_ir_viewed_channel_channelid_userid2.up.sql +++ /dev/null @@ -1 +0,0 @@ -DROP INDEX IF EXISTS ir_viewedchannel_channelid_userid; diff --git a/server/playbooks/server/sqlstore/migrations_test.go b/server/playbooks/server/sqlstore/migrations_test.go deleted file mode 100644 index bbde83e8ffe..00000000000 --- a/server/playbooks/server/sqlstore/migrations_test.go +++ /dev/null @@ -1,827 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "fmt" - "testing" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/morph" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" -) - -type MigrationMapping struct { - Name string - LegacyMigrationIndex int - MorphMigrationLimit int -} - -var migrationsMapping = []MigrationMapping{ - { - Name: "0.0.0 > 0.0.1", - LegacyMigrationIndex: 0, - MorphMigrationLimit: 4, // 000001 <> 000004 - }, - { - Name: "0.2.0 > 0.3.0", - LegacyMigrationIndex: 2, - MorphMigrationLimit: 1, // 000005 - }, - { - Name: "0.3.0 > 0.4.0", - LegacyMigrationIndex: 3, - MorphMigrationLimit: 4, // 000006 <> 000009 - }, - { - Name: "0.4.0 > 0.5.0", - LegacyMigrationIndex: 4, - MorphMigrationLimit: 4, // 000010 <> 000013 - }, - { - Name: "0.5.0 > 0.6.0", - LegacyMigrationIndex: 5, - MorphMigrationLimit: 2, // 000014 <> 000015 - }, - { - Name: "0.6.0 > 0.7.0", - LegacyMigrationIndex: 6, - MorphMigrationLimit: 1, // 000016 - }, - { - Name: "0.7.0 > 0.8.0", - LegacyMigrationIndex: 7, - MorphMigrationLimit: 1, // 000017 - }, - { - Name: "0.8.0 > 0.9.0", - LegacyMigrationIndex: 8, - MorphMigrationLimit: 3, // 000018 <> 000020 - }, - { - Name: "0.9.0 > 0.10.0", - LegacyMigrationIndex: 9, - MorphMigrationLimit: 3, // 000021 <> 000023 - }, - { - Name: "0.11.0 > 0.12.0", - LegacyMigrationIndex: 11, - MorphMigrationLimit: 4, // 000024 <> 000027 - }, - { - Name: "0.12.0 > 0.13.0", - LegacyMigrationIndex: 12, - MorphMigrationLimit: 3, // 000028 <> 000030 - }, - - { - Name: "0.13.0 > 0.14.0", - LegacyMigrationIndex: 13, - MorphMigrationLimit: 2, // 000031 <> 000032 - }, - { - Name: "0.14.0 > 0.15.0", - LegacyMigrationIndex: 14, - MorphMigrationLimit: 1, // 000033 - }, - { - Name: "0.15.0 > 0.16.0", - LegacyMigrationIndex: 15, - MorphMigrationLimit: 4, // 000034-000037 - }, - { - Name: "0.16.0 > 0.17.0", - LegacyMigrationIndex: 16, - MorphMigrationLimit: 1, // 000038 - }, - { - Name: "0.17.0 > 0.18.0", - LegacyMigrationIndex: 17, - MorphMigrationLimit: 3, // 000039-000041 - }, - { - Name: "0.18.0 > 0.19.0", - LegacyMigrationIndex: 18, - MorphMigrationLimit: 1, // 000042 - }, - { - Name: "0.19.0 > 0.20.0", - LegacyMigrationIndex: 19, - MorphMigrationLimit: 3, // 000043-00045 - }, - { - Name: "0.20.0 > 0.21.0", - LegacyMigrationIndex: 20, - MorphMigrationLimit: 3, // 000046-00048 - }, - { - Name: "0.21.0 > 0.22.0", - LegacyMigrationIndex: 21, - MorphMigrationLimit: 1, // 000049 - }, - { - Name: "0.22.0 > 0.23.0", - LegacyMigrationIndex: 22, - MorphMigrationLimit: 2, // 000050-000051 - }, - { - Name: "0.23.0 > 0.24.0", - LegacyMigrationIndex: 23, - MorphMigrationLimit: 2, // 000052-000053 - }, - { - Name: "0.24.0 > 0.25.0", - LegacyMigrationIndex: 24, - MorphMigrationLimit: 4, // 000054-000057 - }, - { - Name: "0.25.0 > 0.26.0", - LegacyMigrationIndex: 25, - MorphMigrationLimit: 2, // 000058-000059 - }, - { - Name: "0.26.0 > 0.27.0", - LegacyMigrationIndex: 26, - MorphMigrationLimit: 2, // 000060-000061 - }, - { - Name: "0.27.0 > 0.28.0", - LegacyMigrationIndex: 27, - MorphMigrationLimit: 1, // 000062 - }, - { - Name: "0.28.0 > 0.29.0", - LegacyMigrationIndex: 28, - MorphMigrationLimit: 1, // 000063 - }, - { - Name: "0.29.0 > 0.30.0", - LegacyMigrationIndex: 29, - MorphMigrationLimit: 6, // 000064-000069 - }, - { - Name: "0.30.0 > 0.31.0", - LegacyMigrationIndex: 30, - MorphMigrationLimit: 1, // 000070 - }, - { - Name: "0.31.0 > 0.32.0", - LegacyMigrationIndex: 31, - MorphMigrationLimit: 1, // 000071 - }, - { - Name: "0.32.0 > 0.33.0", - LegacyMigrationIndex: 32, - MorphMigrationLimit: 4, // 000072-000075 - }, - { - Name: "0.33.0 > 0.34.0", - LegacyMigrationIndex: 33, - MorphMigrationLimit: 1, // 000076 - }, - { - Name: "0.34.0 > 0.35.0", - LegacyMigrationIndex: 34, - MorphMigrationLimit: 1, // 000077 - }, - { - Name: "0.35.0 > 0.36.0", - LegacyMigrationIndex: 35, - MorphMigrationLimit: 2, // 000078-000079 - }, -} - -func TestDBSchema(t *testing.T) { - driverName := getDriverName() - tableInfoList, indexInfoList, constraintInfo := dbInfoAfterEachLegacyMigration(t, driverName, migrationsMapping) - - // create database for morph migration - db := setupTestDB(t) - store := setupTables(t, db) - - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - for i, migration := range migrationsMapping { - t.Run(fmt.Sprintf("validate migration up: %s", migration.Name), func(t *testing.T) { - runMigrationUp(t, store, engine, migration.MorphMigrationLimit) - // compare table schemas - dbSchemaMorph, err := getDBSchemaInfo(store) - require.NoError(t, err) - // this way it's easier to find out why test fails - for j := range dbSchemaMorph { - require.Equal(t, tableInfoList[i+1][j], dbSchemaMorph[j], driverName) - } - - // compare indexes - dbIndexesMorph, err := getDBIndexesInfo(store) - require.NoError(t, err) - require.Equal(t, indexInfoList[i+1], dbIndexesMorph, driverName) - - // compare constraints - dbConstraintsMorph, err := getDBConstraintsInfo(store) - require.NoError(t, err) - require.Equal(t, constraintInfo[i+1], dbConstraintsMorph, driverName) - }) - } - - for i := range migrationsMapping { - migrationIndex := len(migrationsMapping) - i - 1 - migration := migrationsMapping[migrationIndex] - t.Run(fmt.Sprintf("validate migration down: %s", migration.Name), func(t *testing.T) { - runMigrationDown(t, store, engine, migration.MorphMigrationLimit) - // compare table schemas - dbSchemaMorph, err := getDBSchemaInfo(store) - require.NoError(t, err) - - // this way it's easier to find out why test fails - for j := range dbSchemaMorph { - require.Equal(t, tableInfoList[migrationIndex][j], dbSchemaMorph[j], driverName) - } - - // compare indexes - dbIndexesMorph, err := getDBIndexesInfo(store) - require.NoError(t, err) - require.Equal(t, indexInfoList[migrationIndex], dbIndexesMorph, driverName) - - // compare constraints - dbConstraintsMorph, err := getDBConstraintsInfo(store) - require.NoError(t, err) - require.Equal(t, constraintInfo[migrationIndex], dbConstraintsMorph, driverName) - }) - } -} - -func TestMigration_000005(t *testing.T) { - testData := []struct { - Name string - ActiveStage int - ChecklistJSON string - }{ - { - Name: "0", - ActiveStage: 0, - ChecklistJSON: "{][", - }, - { - Name: "1", - ActiveStage: 0, - ChecklistJSON: "{}", - }, - { - Name: "2", - ActiveStage: 0, - ChecklistJSON: "\"key\"", - }, - { - Name: "3", - ActiveStage: -1, - ChecklistJSON: "[]", - }, - { - Name: "4", - ActiveStage: 0, - ChecklistJSON: "", - }, - { - Name: "5", - ActiveStage: 1, - ChecklistJSON: `[{"title":"title50"}, {"title":"title51"}, {"title":"title52"}]`, - }, - { - Name: "6", - ActiveStage: 3, - ChecklistJSON: `[{"title":"title60"}, {"title":"title61"}, {"title":"title62"}]`, - }, - { - Name: "7", - ActiveStage: 2, - ChecklistJSON: `[{"title":"title70"}, {"title":"title71"}, {"title":"title72"}]`, - }, - } - - insertData := func(store *SQLStore) int { - numRuns := 0 - for _, d := range testData { - err := InsertRun(store, NewRunMapBuilder(). - WithName(d.Name). - WithActiveStage(d.ActiveStage). - WithChecklists(d.ChecklistJSON).ToRunAsMap()) - if err == nil { - numRuns++ - } - } - - return numRuns - } - - type Run struct { - ID string - Name string - ChecklistsJSON string - ActiveStage int - ActiveStageTitle string - } - - validateAfter := func(t *testing.T, store *SQLStore, numRuns int) { - var runs []Run - err := store.selectBuilder(store.db, &runs, store.builder. - Select("ID", "Name", "ChecklistsJSON", "ActiveStage", "ActiveStageTitle"). - From("IR_Incident")) - - require.NoError(t, err) - require.Len(t, runs, numRuns) - expectedStageTitles := map[string]string{ - "5": "title51", - "7": "title72", - } - for _, r := range runs { - require.Equal(t, expectedStageTitles[r.Name], r.ActiveStageTitle) - } - } - - validateBefore := func(t *testing.T, store *SQLStore, numRuns int) { - activeStageTitleExist, err := columnExists(store, "IR_Incident", "ActiveStageTitle") - require.NoError(t, err) - require.False(t, activeStageTitleExist) - } - - t.Run("run migration up", func(t *testing.T) { - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 4) - numRuns := insertData(store) - runMigrationUp(t, store, engine, 1) - validateAfter(t, store, numRuns) - }) - - t.Run("run migration down", func(t *testing.T) { - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 4) - numRuns := insertData(store) - runMigrationUp(t, store, engine, 1) - validateAfter(t, store, numRuns) - runMigrationDown(t, store, engine, 1) - validateBefore(t, store, numRuns) - }) -} - -func TestMigration_000014(t *testing.T) { - insertData := func(t *testing.T, store *SQLStore) { - err := InsertRun(store, NewRunMapBuilder().WithName("0").ToRunAsMap()) - require.NoError(t, err) - err = InsertRun(store, NewRunMapBuilder().WithName("1").WithEndAt(100000000000).ToRunAsMap()) - require.NoError(t, err) - err = InsertRun(store, NewRunMapBuilder().WithName("2").WithEndAt(0).ToRunAsMap()) - require.NoError(t, err) - err = InsertRun(store, NewRunMapBuilder().WithName("3").WithEndAt(123861298332).ToRunAsMap()) - require.NoError(t, err) - } - - type Run struct { - Name string - CurrentStatus string - EndAt int64 - } - - validateAfter := func(t *testing.T, store *SQLStore) { - var runs []Run - err := store.selectBuilder(store.db, &runs, store.builder. - Select("Name", "CurrentStatus", "EndAt"). - From("IR_Incident")) - - require.NoError(t, err) - require.Len(t, runs, 4) - - runsStatuses := map[string]string{ - "0": "Active", - "2": "Active", - "1": "Resolved", - "3": "Resolved", - } - for _, r := range runs { - require.Equal(t, runsStatuses[r.Name], r.CurrentStatus) - } - } - - t.Run("run migration up", func(t *testing.T) { - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 13) - insertData(t, store) - runMigrationUp(t, store, engine, 1) - validateAfter(t, store) - }) -} - -func TestMigration_000049(t *testing.T) { - numRuns := 5 - numPosts := 10 - - getPostCreatedAtByIndex := func(i int) int64 { return int64(100000000 + i*100) } - - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 48) - - // insert test data - runsIDs := []string{} - postsIDs := []string{} - for i := 0; i < numRuns; i++ { - run := NewRunMapBuilder().WithName(fmt.Sprintf("run %d", i)).ToRunAsMap() - err = InsertRun(store, run) - require.NoError(t, err) - runsIDs = append(runsIDs, run["ID"].(string)) - } - - for i := 0; i < numPosts; i++ { - postsIDs = append(postsIDs, model.NewId()) - err = InsertPost(store, postsIDs[i], getPostCreatedAtByIndex(i)) - require.NoError(t, err) - } - - _ = InsertStatusPost(store, runsIDs[0], postsIDs[2]) - _ = InsertStatusPost(store, runsIDs[0], postsIDs[3]) - _ = InsertStatusPost(store, runsIDs[0], postsIDs[0]) - _ = InsertStatusPost(store, runsIDs[0], postsIDs[1]) - - _ = InsertStatusPost(store, runsIDs[1], postsIDs[4]) - _ = InsertStatusPost(store, runsIDs[1], postsIDs[5]) - - _ = InsertStatusPost(store, runsIDs[2], postsIDs[7]) - _ = InsertStatusPost(store, runsIDs[2], postsIDs[6]) - - runMigrationUp(t, store, engine, 1) - - // validate migration - type Run struct { - ID string - Name string - CreateAt int64 - LastStatusUpdateAt int64 - } - - var runs []Run - err = store.selectBuilder(store.db, &runs, store.builder. - Select("ID", "Name", "CreateAt", "LastStatusUpdateAt"). - From("IR_Incident"). - OrderBy("Name ASC")) - - require.NoError(t, err) - require.Len(t, runs, numRuns) - - require.Equal(t, getPostCreatedAtByIndex(3), runs[0].LastStatusUpdateAt) - require.Equal(t, getPostCreatedAtByIndex(5), runs[1].LastStatusUpdateAt) - require.Equal(t, getPostCreatedAtByIndex(7), runs[2].LastStatusUpdateAt) - require.Equal(t, runs[3].CreateAt, runs[3].LastStatusUpdateAt) - require.Equal(t, runs[4].CreateAt, runs[4].LastStatusUpdateAt) -} - -func TestMigration_000058(t *testing.T) { - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 57) - - // insert test data - _ = InsertPlaybook(store, NewPBMapBuilder().WithTitle("pb0").WithCategorizeChannelEnabled(true).ToRunAsMap()) - _ = InsertPlaybook(store, NewPBMapBuilder().WithTitle("pb1").WithCategorizeChannelEnabled(false).ToRunAsMap()) - _ = InsertPlaybook(store, NewPBMapBuilder().WithTitle("pb2").ToRunAsMap()) - - runMigrationUp(t, store, engine, 1) - - // validate migration - type Playbook struct { - ID string - Title string - CategorizeChannelEnabled bool - CategoryName *string - } - - var playbooks []Playbook - err = store.selectBuilder(store.db, &playbooks, store.builder. - Select("ID", "Title", "CategorizeChannelEnabled", "CategoryName"). - From("IR_Playbook"). - OrderBy("Title ASC")) - - require.NoError(t, err) - require.Len(t, playbooks, 3) - require.True(t, playbooks[0].CategorizeChannelEnabled) - require.False(t, playbooks[1].CategorizeChannelEnabled) - require.False(t, playbooks[2].CategorizeChannelEnabled) - require.Equal(t, "Playbook Runs", *playbooks[0].CategoryName) - require.True(t, playbooks[1].CategoryName == nil || *playbooks[1].CategoryName == "") - require.True(t, playbooks[2].CategoryName == nil || *playbooks[2].CategoryName == "") -} - -func TestMigration_000059(t *testing.T) { - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 58) - - // insert test data - _ = InsertRun(store, NewRunMapBuilder().WithName("run0").WithCategorizeChannelEnabled(true).ToRunAsMap()) - _ = InsertRun(store, NewRunMapBuilder().WithName("run1").WithCategorizeChannelEnabled(false).ToRunAsMap()) - _ = InsertRun(store, NewRunMapBuilder().WithName("run2").ToRunAsMap()) - - runMigrationUp(t, store, engine, 1) - - // validate migration - type Run struct { - ID string - Name string - CategorizeChannelEnabled bool - CategoryName *string - } - - var runs []Run - err = store.selectBuilder(store.db, &runs, store.builder. - Select("ID", "Name", "CategorizeChannelEnabled", "CategoryName"). - From("IR_Incident"). - OrderBy("Name ASC")) - - require.NoError(t, err) - require.Len(t, runs, 3) - require.True(t, runs[0].CategorizeChannelEnabled) - require.False(t, runs[1].CategorizeChannelEnabled) - require.False(t, runs[2].CategorizeChannelEnabled) - require.Equal(t, "Playbook Runs", *runs[0].CategoryName) - require.True(t, runs[1].CategoryName == nil || *runs[1].CategoryName == "") - require.True(t, runs[2].CategoryName == nil || *runs[2].CategoryName == "") -} - -func TestMigration_000063(t *testing.T) { - driverName := getDriverName() - if driverName != model.DatabaseDriverMysql { - t.Skip("TestMigration_000063 is for MySQL only") - } - - encodingQuery := ` - SELECT TABLE_COLLATION FROM information_schema.TABLES - WHERE table_name = 'IR_System' - AND table_schema = DATABASE(); - ` - - // run legacy migrations and get IR_System table encoding - db := setupTestDB(t) - store := setupTables(t, db) - - for i := 0; i <= 28; i++ { - runLegacyMigration(t, store, 0) - } - var encodingExpected []string - err := store.db.Select(&encodingExpected, encodingQuery) - require.NoError(t, err) - - // run morph migrations on new db and get IR_System table encoding - db = setupTestDB(t) - store = setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 63) - var encodingActual []string - err = store.db.Select(&encodingActual, encodingQuery) - require.NoError(t, err) - require.Equal(t, encodingExpected, encodingActual) -} - -func TestMigration_000070(t *testing.T) { - db := setupTestDB(t) - store := setupTables(t, db) - engine, err := store.createMorphEngine() - require.NoError(t, err) - defer engine.Close() - - runMigrationUp(t, store, engine, 69) - - // insert test data - rows := [][]string{{"1", "com.mattermost.plugin-incident-management"}, {"1", "playbooks"}, {"2", "com.mattermost.plugin-incident-management"}, {"3", "playbooks"}} - for i := range rows { - _, err = store.execBuilder(store.db, sq. - Insert("PluginKeyValueStore"). - SetMap( - map[string]interface{}{ - "PKey": rows[i][0], - "PluginId": rows[i][1], - }, - )) - require.NoError(t, err) - } - - runMigrationUp(t, store, engine, 1) - - // validate migration - type Data struct { - PKey string - PluginID string - } - - var res []Data - err = store.selectBuilder(store.db, &res, store.builder. - Select("PKey", "PluginId as PluginID"). - From("PluginKeyValueStore"). - OrderBy("PKey ASC"). - OrderBy("PluginId ASC")) - - require.NoError(t, err) - require.Len(t, res, 4) - require.Equal(t, "com.mattermost.plugin-incident-management", res[0].PluginID) - require.Equal(t, "playbooks", res[1].PluginID) - require.Equal(t, "playbooks", res[2].PluginID) - require.Equal(t, "playbooks", res[3].PluginID) - - // roll back migration - runMigrationDown(t, store, engine, 1) - res = nil - err = store.selectBuilder(store.db, &res, store.builder. - Select("PKey", "PluginId as PluginID"). - From("PluginKeyValueStore"). - OrderBy("PKey ASC"). - OrderBy("PluginId ASC")) - - require.NoError(t, err) - require.Len(t, res, 4) - require.Equal(t, "com.mattermost.plugin-incident-management", res[0].PluginID) - require.Equal(t, "playbooks", res[1].PluginID) - require.Equal(t, "com.mattermost.plugin-incident-management", res[2].PluginID) - require.Equal(t, "com.mattermost.plugin-incident-management", res[3].PluginID) -} - -func runMigrationUp(t *testing.T, store *SQLStore, engine *morph.Morph, limit int) { - applied, err := engine.Apply(limit) - require.NoError(t, err) - require.Equal(t, applied, limit) -} - -func runMigrationDown(t *testing.T, store *SQLStore, engine *morph.Morph, limit int) { - applied, err := engine.ApplyDown(limit) - require.NoError(t, err) - require.Equal(t, applied, limit) -} - -func runLegacyMigration(t *testing.T, store *SQLStore, index int) { - err := store.migrate(migrations[index]) - require.NoError(t, err) -} - -// dbInfoAfterEachLegacyMigration runs legacy migrations, extracts database schema, indexes and constraints info after each migration -// and returns the list. The first and last elements in the list describe DB before and after running all migrations. -func dbInfoAfterEachLegacyMigration(t *testing.T, driverName string, migrationsToRun []MigrationMapping) ([][]TableInfo, [][]IndexInfo, [][]ConstraintsInfo) { - // create database for legacy migration - db := setupTestDB(t) - store := setupTables(t, db) - - schemaInfo := make([][]TableInfo, len(migrationsToRun)+1) - indexInfo := make([][]IndexInfo, len(migrationsToRun)+1) - constraintInfo := make([][]ConstraintsInfo, len(migrationsToRun)+1) - - schema, err := getDBSchemaInfo(store) - require.NoError(t, err) - schemaInfo[0] = schema - - indexes, err := getDBIndexesInfo(store) - require.NoError(t, err) - indexInfo[0] = indexes - - constraints, err := getDBConstraintsInfo(store) - require.NoError(t, err) - constraintInfo[0] = constraints - - for i, mm := range migrationsToRun { - runLegacyMigration(t, store, mm.LegacyMigrationIndex) - - schema, err = getDBSchemaInfo(store) - require.NoError(t, err) - schemaInfo[i+1] = schema - - indexes, err = getDBIndexesInfo(store) - require.NoError(t, err) - indexInfo[i+1] = indexes - - constraints, err = getDBConstraintsInfo(store) - require.NoError(t, err) - constraintInfo[i+1] = constraints - } - - return schemaInfo, indexInfo, constraintInfo -} - -type RunMapBuilder struct { - runAsMap map[string]interface{} -} - -func NewRunMapBuilder() *RunMapBuilder { - return &RunMapBuilder{ - runAsMap: map[string]interface{}{ - "ID": model.NewId(), - "CreateAt": model.GetMillis(), - "Description": "test description", - "Name": fmt.Sprintf("run- %v", model.GetMillis()), - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": model.NewId(), - "ActiveStage": 0, - "ChecklistsJSON": "[]", - }, - } -} - -func (b *RunMapBuilder) WithName(name string) *RunMapBuilder { - b.runAsMap["Name"] = name - return b -} - -func (b *RunMapBuilder) WithActiveStage(activeStage int) *RunMapBuilder { - b.runAsMap["ActiveStage"] = activeStage - return b -} - -func (b *RunMapBuilder) WithChecklists(checklistJSON string) *RunMapBuilder { - b.runAsMap["ChecklistsJSON"] = checklistJSON - return b -} - -func (b *RunMapBuilder) WithEndAt(endAt int64) *RunMapBuilder { - b.runAsMap["EndAt"] = endAt - return b -} - -func (b *RunMapBuilder) WithCategorizeChannelEnabled(enabled bool) *RunMapBuilder { - b.runAsMap["CategorizeChannelEnabled"] = enabled - return b -} - -func (b *RunMapBuilder) ToRunAsMap() map[string]interface{} { - return b.runAsMap -} - -type PlaybookMapBuilder struct { - playbookAsMap map[string]interface{} -} - -func NewPBMapBuilder() *PlaybookMapBuilder { - timeNow := model.GetMillis() - return &PlaybookMapBuilder{ - playbookAsMap: map[string]interface{}{ - "ID": model.NewId(), - "Title": "base playbook", - "Description": "", - "TeamID": model.NewId(), - "CreatePublicIncident": false, - "CreateAt": model.GetMillis(), - "UpdateAt": timeNow, - "DeleteAt": 0, - "ChecklistsJSON": "{}", - "NumStages": 0, - "NumSteps": 0, - "ReminderTimerDefaultSeconds": 0, - "RetrospectiveReminderIntervalSeconds": 0, - "ExportChannelOnFinishedEnabled": false, - }, - } -} - -func (pb *PlaybookMapBuilder) WithCategorizeChannelEnabled(enabled bool) *PlaybookMapBuilder { - pb.playbookAsMap["CategorizeChannelEnabled"] = enabled - return pb -} - -func (pb *PlaybookMapBuilder) WithTitle(name string) *PlaybookMapBuilder { - pb.playbookAsMap["Title"] = name - return pb -} - -func (pb *PlaybookMapBuilder) ToRunAsMap() map[string]interface{} { - return pb.playbookAsMap -} diff --git a/server/playbooks/server/sqlstore/migrations_tests_utils.go b/server/playbooks/server/sqlstore/migrations_tests_utils.go deleted file mode 100644 index c592dfd6782..00000000000 --- a/server/playbooks/server/sqlstore/migrations_tests_utils.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import sq "github.com/Masterminds/squirrel" - -func InsertRun(sqlStore *SQLStore, run map[string]interface{}) error { - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(run)) - - return err -} - -func InsertPlaybook(sqlStore *SQLStore, playbook map[string]interface{}) error { - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Playbook"). - SetMap(playbook)) - - return err -} - -func InsertPost(sqlStore *SQLStore, id string, createdAt int64) error { - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("Posts"). - SetMap(map[string]interface{}{ - "Id": id, - "CreateAt": createdAt, - })) - - return err -} - -func InsertStatusPost(sqlStore *SQLStore, incidentID, postID string) error { - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_StatusPosts"). - SetMap(map[string]interface{}{ - "IncidentID": incidentID, - "PostID": postID, - })) - - return err -} diff --git a/server/playbooks/server/sqlstore/migrations_utils.go b/server/playbooks/server/sqlstore/migrations_utils.go deleted file mode 100644 index bef4fe12c9d..00000000000 --- a/server/playbooks/server/sqlstore/migrations_utils.go +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "fmt" - "strings" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" - - "github.com/jmoiron/sqlx" -) - -// 'IF NOT EXISTS' syntax is not supported in Postgres 9.4, so we need -// this workaround to make the migration idempotent -var createPGIndex = func(indexName, tableName, columns string) string { - return fmt.Sprintf(` - DO - $$ - BEGIN - IF to_regclass('%s') IS NULL THEN - CREATE INDEX %s ON %s (%s); - END IF; - END - $$; - `, indexName, indexName, tableName, columns) -} - -// 'IF NOT EXISTS' syntax is not supported in Postgres 9.4, so we need -// this workaround to make the migration idempotent -var createUniquePGIndex = func(indexName, tableName, columns string) string { - return fmt.Sprintf(` - DO - $$ - BEGIN - IF to_regclass('%s') IS NULL THEN - CREATE UNIQUE INDEX %s ON %s (%s); - END IF; - END - $$; - `, indexName, indexName, tableName, columns) -} - -var addColumnToPGTable = func(e sqlx.Ext, tableName, columnName, columnType string) error { - _, err := e.Exec(fmt.Sprintf(` - DO - $$ - BEGIN - ALTER TABLE %s ADD %s %s; - EXCEPTION - WHEN duplicate_column THEN - RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" already exists in table "%s".'; - END - $$; - `, tableName, columnName, columnType, columnName, tableName)) - - return err -} - -var changeColumnTypeToPGTable = func(e sqlx.Ext, tableName, columnName, columnType string) error { - _, err := e.Exec(fmt.Sprintf(` - DO - $$ - BEGIN - ALTER TABLE %s ALTER COLUMN %s TYPE %s; - EXCEPTION - WHEN others THEN - RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" can not be changed to type %s in table "%s".'; - END - $$; - `, tableName, columnName, columnType, columnName, columnType, tableName)) - - return err -} - -var addColumnToMySQLTable = func(e sqlx.Ext, tableName, columnName, columnType string) error { - var result int - err := e.QueryRowx( - "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?", - tableName, - columnName, - ).Scan(&result) - - // Only alter the table if we don't find the column - if err == sql.ErrNoRows { - _, err = e.Exec(fmt.Sprintf("ALTER TABLE %s ADD %s %s", tableName, columnName, columnType)) - } - - return err -} - -var renameColumnMySQL = func(e sqlx.Ext, tableName, oldColName, newColName, colDatatype string) error { - var result int - err := e.QueryRowx( - "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?", - tableName, - newColName, - ).Scan(&result) - - // Only alter the table if we don't find the column - if err == sql.ErrNoRows { - _, err = e.Exec(fmt.Sprintf("ALTER TABLE %s CHANGE %s %s %s", tableName, oldColName, newColName, colDatatype)) - } - - return err -} - -var renameColumnPG = func(e sqlx.Ext, tableName, oldColName, newColName string) error { - _, err := e.Exec(fmt.Sprintf(` - DO - $$ - BEGIN - ALTER TABLE %s RENAME COLUMN %s TO %s; - EXCEPTION - WHEN others THEN - RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" does not exist in table "%s".'; - END - $$; - `, tableName, oldColName, newColName, oldColName, tableName)) - - return err -} - -var dropColumnMySQL = func(e sqlx.Ext, tableName, colName string) error { - var result int - err := e.QueryRowx( - "SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?", - tableName, - colName, - ).Scan(&result) - - if err == sql.ErrNoRows { - return nil - } - - // Only alter the table if we find the column - if err == nil && result == 1 { - _, err = e.Exec(fmt.Sprintf("ALTER TABLE %s DROP COLUMN %s", tableName, colName)) - } - - return err -} - -var dropColumnPG = func(e sqlx.Ext, tableName, colName string) error { - _, err := e.Exec(fmt.Sprintf(` - DO - $$ - BEGIN - ALTER TABLE %s DROP COLUMN %s; - EXCEPTION - WHEN others THEN - RAISE NOTICE 'Ignoring ALTER TABLE statement. Column "%s" does not exist in table "%s".'; - END - $$; - `, tableName, colName, colName, tableName)) - - return err -} - -func addPrimaryKey(e sqlx.Ext, sqlStore *SQLStore, tableName, primaryKey string) error { - hasPK := 0 - - dbSelectionLine := "AND tco.table_schema = (SELECT DATABASE())" - if e.DriverName() == model.DatabaseDriverPostgres { - dbSelectionLine = "AND tco.table_catalog = (SELECT current_database())" - } - - if err := sqlStore.db.Get(&hasPK, fmt.Sprintf(` - SELECT 1 FROM information_schema.table_constraints tco - WHERE tco.table_name = '%s' - %s - AND tco.constraint_type = 'PRIMARY KEY' - `, tableName, dbSelectionLine)); err != nil && err != sql.ErrNoRows { - return errors.Wrap(err, "unable to determine if a primary key exists") - } - - if hasPK == 0 { - if _, err := e.Exec(fmt.Sprintf(` - ALTER TABLE %s ADD PRIMARY KEY %s - `, tableName, primaryKey)); err != nil { - return errors.Wrap(err, "unable to add a primary key") - } - } - - return nil -} - -func dropIndexIfExists(e sqlx.Ext, sqlStore *SQLStore, tableName, indexName string) error { - hasIndex := 0 - - if e.DriverName() == model.DatabaseDriverMysql { - if err := sqlStore.db.Get(&hasIndex, fmt.Sprintf(` - SELECT 1 FROM information_schema.statistics s - WHERE s.table_name = '%s' - AND s.index_schema = (SELECT DATABASE()) - AND index_name = '%s' - `, tableName, indexName)); err != nil && err != sql.ErrNoRows { - return errors.Wrapf(err, "unable to determine if index %s on table %s exists", indexName, tableName) - } - - if hasIndex == 1 { - if _, err := e.Exec(fmt.Sprintf("DROP INDEX %s ON %s", indexName, tableName)); err != nil { - return errors.Wrapf(err, "failed to drop index %s on table %s", indexName, tableName) - } - } - } else if e.DriverName() == model.DatabaseDriverPostgres { - if _, err := e.Exec(fmt.Sprintf("DROP INDEX IF EXISTS %s", indexName)); err != nil { - return errors.Wrapf(err, "failed to drop index %s on table %s", indexName, tableName) - } - } - - return nil -} - -func columnExists(sqlStore *SQLStore, tableName, columnName string) (bool, error) { - results := []string{} - var err error - if sqlStore.db.DriverName() == model.DatabaseDriverMysql { - err = sqlStore.db.Select(&results, ` - SELECT COLUMN_NAME - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = ? - AND COLUMN_NAME = ? - `, tableName, columnName) - } else if sqlStore.db.DriverName() == model.DatabaseDriverPostgres { - err = sqlStore.db.Select(&results, ` - SELECT COLUMN_NAME - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_NAME = $1 - AND COLUMN_NAME = $2 - `, strings.ToLower(tableName), strings.ToLower(columnName)) - } - - return len(results) > 0, err -} - -type TableInfo struct { - TableName string - ColumnName string - DataType string - IsNullable string - ColumnKey string - ColumnDefault *string - Extra string - CharacterMaximumLength *string -} - -// getDBSchemaInfo returns info for each table created by Playbook plugin -func getDBSchemaInfo(store *SQLStore) ([]TableInfo, error) { - var results []TableInfo - var err error - - if store.db.DriverName() == model.DatabaseDriverMysql { - err = store.db.Select(&results, ` - SELECT - TABLE_NAME as TableName, COLUMN_NAME as ColumnName, DATA_TYPE as DataType, - IS_NULLABLE as IsNullable, COLUMN_KEY as ColumnKey, COLUMN_DEFAULT as ColumnDefault, - EXTRA as Extra, CHARACTER_MAXIMUM_LENGTH as CharacterMaximumLength - - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME LIKE 'IR_%' - AND TABLE_NAME != 'IR_db_migrations' - ORDER BY TABLE_NAME ASC, ORDINAL_POSITION ASC - `) - } else if store.db.DriverName() == model.DatabaseDriverPostgres { - err = store.db.Select(&results, ` - SELECT - TABLE_NAME as TableName, COLUMN_NAME as ColumnName, DATA_TYPE as DataType, - IS_NULLABLE as IsNullable, COLUMN_DEFAULT as ColumnDefault, CHARACTER_MAXIMUM_LENGTH as CharacterMaximumLength - - FROM INFORMATION_SCHEMA.COLUMNS - WHERE table_schema = 'public' - AND TABLE_NAME LIKE 'ir_%' - AND TABLE_NAME != 'ir_db_migrations' - ORDER BY TABLE_NAME ASC, ORDINAL_POSITION ASC - `) - } - - return results, err -} - -type IndexInfo struct { - TableName string - IndexName string - - // Postgres specific field - IndexDef string - - // MySQL specific fields - ColumnName string -} - -// getDBIndexesInfo returns index info for each table created by Playbook plugin -func getDBIndexesInfo(store *SQLStore) ([]IndexInfo, error) { - var results []IndexInfo - var err error - - if store.db.DriverName() == model.DatabaseDriverMysql { - err = store.db.Select(&results, ` - SELECT TABLE_NAME as TableName, INDEX_NAME as IndexName, COLUMN_NAME as ColumnName - FROM INFORMATION_SCHEMA.STATISTICS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME LIKE 'ir_%' - AND TABLE_NAME != 'ir_db_migrations' - ORDER BY TABLE_NAME ASC, COLUMN_NAME ASC, INDEX_NAME ASC; - `) - } else if store.db.DriverName() == model.DatabaseDriverPostgres { - err = store.db.Select(&results, ` - SELECT TABLENAME as TableName, INDEXNAME as IndexName, INDEXDEF as IndexDef - FROM pg_indexes - WHERE SCHEMANAME = 'public' - AND TABLENAME LIKE 'ir_%' - AND TABLENAME != 'ir_db_migrations' - ORDER BY TABLENAME ASC, INDEXNAME ASC; - `) - } - - return results, err -} - -type ConstraintsInfo struct { - ConstraintName string - TableName string - ConstraintType string -} - -// getDBIndexesInfo returns index info for each table created by Playbook plugin -func getDBConstraintsInfo(store *SQLStore) ([]ConstraintsInfo, error) { - var results []ConstraintsInfo - var err error - - if store.db.DriverName() == model.DatabaseDriverMysql { - err = store.db.Select(&results, ` - SELECT CONSTRAINT_NAME as ConstraintName, TABLE_NAME as TableName, CONSTRAINT_TYPE as ConstraintType - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS - WHERE TABLE_NAME LIKE 'ir_%' - AND TABLE_NAME != 'ir_db_migrations' - AND TABLE_SCHEMA = (SELECT DATABASE()) - ORDER BY CONSTRAINT_NAME ASC, TABLE_NAME ASC; - `) - } else if store.db.DriverName() == model.DatabaseDriverPostgres { - err = store.db.Select(&results, ` - SELECT conname as ConstraintName, contype as ConstraintType - FROM pg_constraint - WHERE conname LIKE 'ir_%' - AND conname NOT LIKE 'ir_db_migrations%' - ORDER BY conname ASC, contype ASC; - `) - } - - return results, err -} diff --git a/server/playbooks/server/sqlstore/mockmocks/mock_storeapi.go b/server/playbooks/server/sqlstore/mockmocks/mock_storeapi.go deleted file mode 100644 index 5924a4fd966..00000000000 --- a/server/playbooks/server/sqlstore/mockmocks/mock_storeapi.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore (interfaces: StoreAPI) - -// Package mock_sqlstore is a generated GoMock package. -package mock_sqlstore - -import ( - sql "database/sql" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockStoreAPI is a mock of StoreAPI interface -type MockStoreAPI struct { - ctrl *gomock.Controller - recorder *MockStoreAPIMockRecorder -} - -// MockStoreAPIMockRecorder is the mock recorder for MockStoreAPI -type MockStoreAPIMockRecorder struct { - mock *MockStoreAPI -} - -// NewMockStoreAPI creates a new mock instance -func NewMockStoreAPI(ctrl *gomock.Controller) *MockStoreAPI { - mock := &MockStoreAPI{ctrl: ctrl} - mock.recorder = &MockStoreAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockStoreAPI) EXPECT() *MockStoreAPIMockRecorder { - return m.recorder -} - -// DriverName mocks base method -func (m *MockStoreAPI) DriverName() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DriverName") - ret0, _ := ret[0].(string) - return ret0 -} - -// DriverName indicates an expected call of DriverName -func (mr *MockStoreAPIMockRecorder) DriverName() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DriverName", reflect.TypeOf((*MockStoreAPI)(nil).DriverName)) -} - -// GetMasterDB mocks base method -func (m *MockStoreAPI) GetMasterDB() (*sql.DB, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMasterDB") - ret0, _ := ret[0].(*sql.DB) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMasterDB indicates an expected call of GetMasterDB -func (mr *MockStoreAPIMockRecorder) GetMasterDB() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockStoreAPI)(nil).GetMasterDB)) -} diff --git a/server/playbooks/server/sqlstore/mocks/mock_configurationapi.go b/server/playbooks/server/sqlstore/mocks/mock_configurationapi.go deleted file mode 100644 index 8115a701907..00000000000 --- a/server/playbooks/server/sqlstore/mocks/mock_configurationapi.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore (interfaces: ConfigurationAPI) - -// Package mock_sqlstore is a generated GoMock package. -package mock_sqlstore - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" - model "github.com/mattermost/mattermost/server/public/model" -) - -// MockConfigurationAPI is a mock of ConfigurationAPI interface. -type MockConfigurationAPI struct { - ctrl *gomock.Controller - recorder *MockConfigurationAPIMockRecorder -} - -// MockConfigurationAPIMockRecorder is the mock recorder for MockConfigurationAPI. -type MockConfigurationAPIMockRecorder struct { - mock *MockConfigurationAPI -} - -// NewMockConfigurationAPI creates a new mock instance. -func NewMockConfigurationAPI(ctrl *gomock.Controller) *MockConfigurationAPI { - mock := &MockConfigurationAPI{ctrl: ctrl} - mock.recorder = &MockConfigurationAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockConfigurationAPI) EXPECT() *MockConfigurationAPIMockRecorder { - return m.recorder -} - -// GetConfig mocks base method. -func (m *MockConfigurationAPI) GetConfig() *model.Config { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetConfig") - ret0, _ := ret[0].(*model.Config) - return ret0 -} - -// GetConfig indicates an expected call of GetConfig. -func (mr *MockConfigurationAPIMockRecorder) GetConfig() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockConfigurationAPI)(nil).GetConfig)) -} diff --git a/server/playbooks/server/sqlstore/mocks/mock_kvapi.go b/server/playbooks/server/sqlstore/mocks/mock_kvapi.go deleted file mode 100644 index c3c234c2396..00000000000 --- a/server/playbooks/server/sqlstore/mocks/mock_kvapi.go +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore (interfaces: KVAPI) - -// Package mock_sqlstore is a generated GoMock package. -package mock_sqlstore - -import ( - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockKVAPI is a mock of KVAPI interface. -type MockKVAPI struct { - ctrl *gomock.Controller - recorder *MockKVAPIMockRecorder -} - -// MockKVAPIMockRecorder is the mock recorder for MockKVAPI. -type MockKVAPIMockRecorder struct { - mock *MockKVAPI -} - -// NewMockKVAPI creates a new mock instance. -func NewMockKVAPI(ctrl *gomock.Controller) *MockKVAPI { - mock := &MockKVAPI{ctrl: ctrl} - mock.recorder = &MockKVAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockKVAPI) EXPECT() *MockKVAPIMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockKVAPI) Get(arg0 string, arg1 interface{}) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// Get indicates an expected call of Get. -func (mr *MockKVAPIMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKVAPI)(nil).Get), arg0, arg1) -} diff --git a/server/playbooks/server/sqlstore/mocks/mock_storeapi.go b/server/playbooks/server/sqlstore/mocks/mock_storeapi.go deleted file mode 100644 index 6d9c23123e5..00000000000 --- a/server/playbooks/server/sqlstore/mocks/mock_storeapi.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore (interfaces: StoreAPI) - -// Package mock_sqlstore is a generated GoMock package. -package mock_sqlstore - -import ( - sql "database/sql" - reflect "reflect" - - gomock "github.com/golang/mock/gomock" -) - -// MockStoreAPI is a mock of StoreAPI interface. -type MockStoreAPI struct { - ctrl *gomock.Controller - recorder *MockStoreAPIMockRecorder -} - -// MockStoreAPIMockRecorder is the mock recorder for MockStoreAPI. -type MockStoreAPIMockRecorder struct { - mock *MockStoreAPI -} - -// NewMockStoreAPI creates a new mock instance. -func NewMockStoreAPI(ctrl *gomock.Controller) *MockStoreAPI { - mock := &MockStoreAPI{ctrl: ctrl} - mock.recorder = &MockStoreAPIMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStoreAPI) EXPECT() *MockStoreAPIMockRecorder { - return m.recorder -} - -// DriverName mocks base method. -func (m *MockStoreAPI) DriverName() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DriverName") - ret0, _ := ret[0].(string) - return ret0 -} - -// DriverName indicates an expected call of DriverName. -func (mr *MockStoreAPIMockRecorder) DriverName() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DriverName", reflect.TypeOf((*MockStoreAPI)(nil).DriverName)) -} - -// GetMasterDB mocks base method. -func (m *MockStoreAPI) GetMasterDB() (*sql.DB, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMasterDB") - ret0, _ := ret[0].(*sql.DB) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetMasterDB indicates an expected call of GetMasterDB. -func (mr *MockStoreAPIMockRecorder) GetMasterDB() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockStoreAPI)(nil).GetMasterDB)) -} diff --git a/server/playbooks/server/sqlstore/playbook.go b/server/playbooks/server/sqlstore/playbook.go deleted file mode 100644 index 805b80550f6..00000000000 --- a/server/playbooks/server/sqlstore/playbook.go +++ /dev/null @@ -1,1232 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - "fmt" - "math" - "strings" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -type sqlPlaybook struct { - app.Playbook - ChecklistsJSON json.RawMessage - ConcatenatedInvitedUserIDs string - ConcatenatedInvitedGroupIDs string - ConcatenatedSignalAnyKeywords string - ConcatenatedBroadcastChannelIDs string - ConcatenatedWebhookOnCreationURLs string - ConcatenatedWebhookOnStatusUpdateURLs string -} - -// playbookStore is a sql store for playbooks. Use NewPlaybookStore to create it. -type playbookStore struct { - pluginAPI PluginAPIClient - store *SQLStore - queryBuilder sq.StatementBuilderType - playbookSelect sq.SelectBuilder - membersSelect sq.SelectBuilder - metricsSelect sq.SelectBuilder -} - -// Ensure playbookStore implements the playbook.Store interface. -var _ app.PlaybookStore = (*playbookStore)(nil) - -type playbookMember struct { - PlaybookID string - MemberID string - Roles string -} - -// definied to call a common insights query builder for both user and team insights -const insightsQueryTypeUser = "insights_query_type_user" -const insightsQueryTypeTeam = "insights_query_type_team" - -func applyPlaybookFilterOptionsSort(builder sq.SelectBuilder, options app.PlaybookFilterOptions) (sq.SelectBuilder, error) { - var sort string - switch options.Sort { - case app.SortByID: - sort = "ID" - case app.SortByTitle: - sort = "Title" - case app.SortByStages: - sort = "NumStages" - case app.SortBySteps: - sort = "NumSteps" - case app.SortByRuns: - sort = "NumRuns" - case app.SortByCreateAt: - sort = "CreateAt" - case app.SortByLastRunAt: - sort = "LastRunAt" - case app.SortByActiveRuns: - sort = "ActiveRuns" - case "": - // Default to a stable sort if none explicitly provided. - sort = "ID" - default: - return sq.SelectBuilder{}, errors.Errorf("unsupported sort parameter '%s'", options.Sort) - } - - var direction string - switch options.Direction { - case app.DirectionAsc: - direction = "ASC" - case app.DirectionDesc: - direction = "DESC" - case "": - // Default to an ascending sort if none explicitly provided. - direction = "ASC" - default: - return sq.SelectBuilder{}, errors.Errorf("unsupported direction parameter '%s'", options.Direction) - } - - builder = builder.OrderByClause(fmt.Sprintf("%s %s", sort, direction)) - - page := options.Page - perPage := options.PerPage - if page < 0 { - page = 0 - } - if perPage < 0 { - perPage = 0 - } - - builder = builder. - Offset(uint64(page * perPage)). - Limit(uint64(perPage)) - - return builder, nil -} - -// NewPlaybookStore creates a new store for playbook service. -func NewPlaybookStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.PlaybookStore { - playbookSelect := sqlStore.builder. - Select( - "p.ID", - "p.Title", - "p.Description", - "p.Public", - "p.TeamID", - "p.CreatePublicIncident AS CreatePublicPlaybookRun", - "p.CreateAt", - "p.UpdateAt", - "p.DeleteAt", - "p.NumStages", - "p.NumSteps", - `( - 1 + -- Channel creation is hard-coded - CASE WHEN p.InviteUsersEnabled THEN 1 ELSE 0 END + - CASE WHEN p.DefaultCommanderEnabled THEN 1 ELSE 0 END + - CASE WHEN p.BroadcastEnabled THEN 1 ELSE 0 END + - CASE WHEN p.WebhookOnCreationEnabled THEN 1 ELSE 0 END + - CASE WHEN p.MessageOnJoinEnabled THEN 1 ELSE 0 END + - CASE WHEN p.WebhookOnStatusUpdateEnabled THEN 1 ELSE 0 END + - CASE WHEN p.SignalAnyKeywordsEnabled THEN 1 ELSE 0 END + - CASE WHEN p.CategorizeChannelEnabled THEN 1 ELSE 0 END + - CASE WHEN p.CreateChannelMemberOnNewParticipant THEN 1 ELSE 0 END + - CASE WHEN p.RemoveChannelMemberOnRemovedParticipant THEN 1 ELSE 0 END - ) AS NumActions`, - "COALESCE(p.ReminderMessageTemplate, '') ReminderMessageTemplate", - "p.ReminderTimerDefaultSeconds", - "p.StatusUpdateEnabled", - "p.ConcatenatedInvitedUserIDs", - "p.ConcatenatedInvitedGroupIDs", - "p.InviteUsersEnabled", - "p.DefaultCommanderID AS DefaultOwnerID", - "p.DefaultCommanderEnabled AS DefaultOwnerEnabled", - "p.ConcatenatedBroadcastChannelIDs", - "p.BroadcastEnabled", - "p.ConcatenatedWebhookOnCreationURLs", - "p.WebhookOnCreationEnabled", - "p.MessageOnJoin", - "p.MessageOnJoinEnabled", - "p.RetrospectiveReminderIntervalSeconds", - "p.RetrospectiveTemplate", - "p.RetrospectiveEnabled", - "p.ConcatenatedWebhookOnStatusUpdateURLs", - "p.WebhookOnStatusUpdateEnabled", - "p.ConcatenatedSignalAnyKeywords", - "p.SignalAnyKeywordsEnabled", - "p.CategorizeChannelEnabled", - "p.CreateChannelMemberOnNewParticipant", - "p.RemoveChannelMemberOnRemovedParticipant", - "p.ChannelID", - "p.ChannelMode", - "p.ChecklistsJSON", - "COALESCE(p.CategoryName, '') CategoryName", - "p.RunSummaryTemplateEnabled", - "COALESCE(p.RunSummaryTemplate, '') RunSummaryTemplate", - "COALESCE(p.ChannelNameTemplate, '') ChannelNameTemplate", - "COALESCE(s.DefaultPlaybookAdminRole, 'playbook_admin') DefaultPlaybookAdminRole", - "COALESCE(s.DefaultPlaybookMemberRole, 'playbook_member') DefaultPlaybookMemberRole", - "COALESCE(s.DefaultRunAdminRole, 'run_admin') DefaultRunAdminRole", - "COALESCE(s.DefaultRunMemberRole, 'run_member') DefaultRunMemberRole", - ). - From("IR_Playbook p"). - LeftJoin("Teams t ON t.Id = p.TeamID"). - LeftJoin("Schemes s ON t.SchemeId = s.Id") - - membersSelect := sqlStore.builder. - Select( - "PlaybookID", - "MemberID", - "Roles", - ). - From("IR_PlaybookMember"). - OrderBy("MemberID ASC") // Entirely for consistency for the tests - - metricsSelect := sqlStore.builder. - Select( - "ID", - "PlaybookID", - "Title", - "Description", - "Type", - "Target", - ). - From("IR_MetricConfig"). - Where(sq.Eq{"DeleteAt": 0}). - OrderBy("Ordering ASC") - - newStore := &playbookStore{ - pluginAPI: pluginAPI, - store: sqlStore, - queryBuilder: sqlStore.builder, - playbookSelect: playbookSelect, - membersSelect: membersSelect, - metricsSelect: metricsSelect, - } - return newStore -} - -// Create creates a new playbook -func (p *playbookStore) Create(playbook app.Playbook) (id string, err error) { - if playbook.ID != "" { - return "", errors.New("ID should be empty") - } - playbook.ID = model.NewId() - - rawPlaybook, err := toSQLPlaybook(playbook) - if err != nil { - return "", err - } - - tx, err := p.store.db.Beginx() - if err != nil { - return "", errors.Wrap(err, "could not begin transaction") - } - defer p.store.finalizeTransaction(tx) - - _, err = p.store.execBuilder(tx, sq. - Insert("IR_Playbook"). - SetMap(map[string]interface{}{ - "ID": rawPlaybook.ID, - "Title": rawPlaybook.Title, - "Description": rawPlaybook.Description, - "TeamID": rawPlaybook.TeamID, - "Public": rawPlaybook.Public, - "CreatePublicIncident": rawPlaybook.CreatePublicPlaybookRun, - "CreateAt": rawPlaybook.CreateAt, - "UpdateAt": rawPlaybook.UpdateAt, - "DeleteAt": rawPlaybook.DeleteAt, - "ChecklistsJSON": rawPlaybook.ChecklistsJSON, - "NumStages": len(rawPlaybook.Checklists), - "NumSteps": getSteps(rawPlaybook.Playbook), - "ReminderMessageTemplate": rawPlaybook.ReminderMessageTemplate, - "ReminderTimerDefaultSeconds": rawPlaybook.ReminderTimerDefaultSeconds, - "StatusUpdateEnabled": rawPlaybook.StatusUpdateEnabled, - "ConcatenatedInvitedUserIDs": rawPlaybook.ConcatenatedInvitedUserIDs, - "ConcatenatedInvitedGroupIDs": rawPlaybook.ConcatenatedInvitedGroupIDs, - "InviteUsersEnabled": rawPlaybook.InviteUsersEnabled, - "DefaultCommanderID": rawPlaybook.DefaultOwnerID, - "DefaultCommanderEnabled": rawPlaybook.DefaultOwnerEnabled, - "ConcatenatedBroadcastChannelIDs": rawPlaybook.ConcatenatedBroadcastChannelIDs, - "BroadcastEnabled": rawPlaybook.BroadcastEnabled, //nolint - "ConcatenatedWebhookOnCreationURLs": rawPlaybook.ConcatenatedWebhookOnCreationURLs, - "WebhookOnCreationEnabled": rawPlaybook.WebhookOnCreationEnabled, - "MessageOnJoin": rawPlaybook.MessageOnJoin, - "MessageOnJoinEnabled": rawPlaybook.MessageOnJoinEnabled, - "RetrospectiveReminderIntervalSeconds": rawPlaybook.RetrospectiveReminderIntervalSeconds, - "RetrospectiveTemplate": rawPlaybook.RetrospectiveTemplate, - "RetrospectiveEnabled": rawPlaybook.RetrospectiveEnabled, - "ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs, - "WebhookOnStatusUpdateEnabled": rawPlaybook.WebhookOnStatusUpdateEnabled, - "ConcatenatedSignalAnyKeywords": rawPlaybook.ConcatenatedSignalAnyKeywords, - "SignalAnyKeywordsEnabled": rawPlaybook.SignalAnyKeywordsEnabled, - "CategorizeChannelEnabled": rawPlaybook.CategorizeChannelEnabled, - "CategoryName": rawPlaybook.CategoryName, - "RunSummaryTemplateEnabled": rawPlaybook.RunSummaryTemplateEnabled, - "RunSummaryTemplate": rawPlaybook.RunSummaryTemplate, - "ChannelNameTemplate": rawPlaybook.ChannelNameTemplate, - "CreateChannelMemberOnNewParticipant": rawPlaybook.CreateChannelMemberOnNewParticipant, - "RemoveChannelMemberOnRemovedParticipant": rawPlaybook.RemoveChannelMemberOnRemovedParticipant, - "ChannelID": rawPlaybook.ChannelID, - "ChannelMode": rawPlaybook.ChannelMode, - })) - if err != nil { - return "", errors.Wrap(err, "failed to store new playbook") - } - - if err = p.replacePlaybookMembers(tx, rawPlaybook.Playbook); err != nil { - return "", errors.Wrap(err, "failed to replace playbook members") - } - - if err = p.replacePlaybookMetrics(tx, rawPlaybook.Playbook); err != nil { - return "", errors.Wrap(err, "failed to replace playbook metrics configs") - } - - if err = tx.Commit(); err != nil { - return "", errors.Wrap(err, "could not commit transaction") - } - - return rawPlaybook.ID, nil -} - -// Get retrieves a playbook -func (p *playbookStore) Get(id string) (app.Playbook, error) { - if id == "" { - return app.Playbook{}, errors.New("ID cannot be empty") - } - - tx, err := p.store.db.Beginx() - if err != nil { - return app.Playbook{}, errors.Wrap(err, "could not begin transaction") - } - defer p.store.finalizeTransaction(tx) - - var rawPlaybook sqlPlaybook - err = p.store.getBuilder(tx, &rawPlaybook, p.playbookSelect.Where(sq.Eq{"p.ID": id})) - if err == sql.ErrNoRows { - return app.Playbook{}, errors.Wrapf(app.ErrNotFound, "playbook does not exist for id '%s'", id) - } else if err != nil { - return app.Playbook{}, errors.Wrapf(err, "failed to get playbook by id '%s'", id) - } - - playbook, err := toPlaybook(rawPlaybook) - if err != nil { - return app.Playbook{}, err - } - - var members []playbookMember - err = p.store.selectBuilder(tx, &members, p.membersSelect.Where(sq.Eq{"PlaybookID": id})) - if err != nil && err != sql.ErrNoRows { - return app.Playbook{}, errors.Wrapf(err, "failed to get memberIDs for playbook with id '%s'", id) - } - - var metrics []app.PlaybookMetricConfig - err = p.store.selectBuilder(tx, &metrics, p.metricsSelect.Where(sq.Eq{"PlaybookID": id})) - if err != nil && err != sql.ErrNoRows { - return app.Playbook{}, errors.Wrapf(err, "failed to get metrics configs for playbook with id '%s'", id) - } - - if err = tx.Commit(); err != nil { - return app.Playbook{}, errors.Wrap(err, "could not commit transaction") - } - - addMembersToPlaybook(members, &playbook) - playbook.Metrics = metrics - return playbook, nil -} - -// GetPlaybooks retrieves all playbooks that are not deleted. -// Members are not retrieved for this as the query would be large and we don't need it for this for now. -// This is only used for the keywords feature -func (p *playbookStore) GetPlaybooks() ([]app.Playbook, error) { - tx, err := p.store.db.Beginx() - if err != nil { - return nil, errors.Wrap(err, "could not begin transaction") - } - defer p.store.finalizeTransaction(tx) - - var playbooks []app.Playbook - err = p.store.selectBuilder(tx, &playbooks, p.store.builder. - Select( - "p.ID", - "p.Title", - "p.Description", - "p.TeamID", - "p.Public", - "p.CreatePublicIncident AS CreatePublicPlaybookRun", - "p.CreateAt", - "p.DeleteAt", - "p.NumStages", - "p.NumSteps", - "COUNT(i.ID) AS NumRuns", - "COALESCE(MAX(i.CreateAt), 0) AS LastRunAt", - `( - 1 + -- Channel creation is hard-coded - CASE WHEN p.InviteUsersEnabled THEN 1 ELSE 0 END + - CASE WHEN p.DefaultCommanderEnabled THEN 1 ELSE 0 END + - CASE WHEN p.BroadcastEnabled THEN 1 ELSE 0 END + - CASE WHEN p.WebhookOnCreationEnabled THEN 1 ELSE 0 END + - CASE WHEN p.MessageOnJoinEnabled THEN 1 ELSE 0 END + - CASE WHEN p.WebhookOnStatusUpdateEnabled THEN 1 ELSE 0 END + - CASE WHEN p.SignalAnyKeywordsEnabled THEN 1 ELSE 0 END + - CASE WHEN p.CategorizeChannelEnabled THEN 1 ELSE 0 END + - CASE WHEN p.CreateChannelMemberOnNewParticipant THEN 1 ELSE 0 END + - CASE WHEN p.RemoveChannelMemberOnRemovedParticipant THEN 1 ELSE 0 END - ) AS NumActions`, - "COALESCE(ChannelNameTemplate, '') ChannelNameTemplate", - "COALESCE(s.DefaultPlaybookAdminRole, 'playbook_admin') DefaultPlaybookAdminRole", - "COALESCE(s.DefaultPlaybookMemberRole, 'playbook_member') DefaultPlaybookMemberRole", - "COALESCE(s.DefaultRunAdminRole, 'run_admin') DefaultRunAdminRole", - "COALESCE(s.DefaultRunMemberRole, 'run_member') DefaultRunMemberRole", - ). - From("IR_Playbook AS p"). - LeftJoin("IR_Incident AS i ON p.ID = i.PlaybookID"). - LeftJoin("Teams t ON t.Id = p.TeamID"). - LeftJoin("Schemes s ON t.SchemeId = s.Id"). - Where(sq.Eq{"p.DeleteAt": 0}). - GroupBy("p.ID"). - GroupBy("s.Id")) - - if err == sql.ErrNoRows { - return nil, errors.Wrap(app.ErrNotFound, "no playbooks found") - } else if err != nil { - return nil, errors.Wrap(err, "failed to get playbooks") - } - - return playbooks, nil -} - -// GetPlaybooksForTeam retrieves all playbooks on the specified team given the provided options. -func (p *playbookStore) GetPlaybooksForTeam(requesterInfo app.RequesterInfo, teamID string, opts app.PlaybookFilterOptions) (app.GetPlaybooksResults, error) { - // Check that you are a playbook member or there are no restrictions. - permissionsAndFilter := sq.Expr(`( - EXISTS(SELECT 1 - FROM IR_PlaybookMember as pm - WHERE pm.PlaybookID = p.ID - AND pm.MemberID = ?) - )`, requesterInfo.UserID) - if !opts.WithMembershipOnly { // return all public playbooks and private ones user is member of - permissionsAndFilter = sq.Or{sq.Expr(`p.Public = true`), permissionsAndFilter} - } - teamLimitExpr := buildTeamLimitExpr(requesterInfo, teamID, "p") - - queryForResults := p.store.builder. - Select( - "p.ID", - "p.Title", - "p.Description", - "p.TeamID", - "p.Public", - "p.CreatePublicIncident AS CreatePublicPlaybookRun", - "p.CreateAt", - "p.DeleteAt", - "p.NumStages", - "p.NumSteps", - "p.DefaultCommanderEnabled AS DefaultOwnerEnabled", - "p.DefaultCommanderID AS DefaultOwnerID", - "COUNT(i.ID) AS NumRuns", - "COUNT(CASE WHEN i.CurrentStatus='InProgress' THEN 1 END) AS ActiveRuns", - "COALESCE(MAX(i.CreateAt), 0) AS LastRunAt", - `( - 1 + -- Channel creation is hard-coded - CASE WHEN p.InviteUsersEnabled THEN 1 ELSE 0 END + - CASE WHEN p.DefaultCommanderEnabled THEN 1 ELSE 0 END + - CASE WHEN p.BroadcastEnabled THEN 1 ELSE 0 END + - CASE WHEN p.WebhookOnCreationEnabled THEN 1 ELSE 0 END + - CASE WHEN p.MessageOnJoinEnabled THEN 1 ELSE 0 END + - CASE WHEN p.WebhookOnStatusUpdateEnabled THEN 1 ELSE 0 END + - CASE WHEN p.SignalAnyKeywordsEnabled THEN 1 ELSE 0 END + - CASE WHEN p.CategorizeChannelEnabled THEN 1 ELSE 0 END + - CASE WHEN p.CreateChannelMemberOnNewParticipant THEN 1 ELSE 0 END + - CASE WHEN p.RemoveChannelMemberOnRemovedParticipant THEN 1 ELSE 0 END - ) AS NumActions`, - "COALESCE(ChannelNameTemplate, '') ChannelNameTemplate", - "COALESCE(s.DefaultPlaybookAdminRole, 'playbook_admin') DefaultPlaybookAdminRole", - "COALESCE(s.DefaultPlaybookMemberRole, 'playbook_member') DefaultPlaybookMemberRole", - "COALESCE(s.DefaultRunAdminRole, 'run_admin') DefaultRunAdminRole", - "COALESCE(s.DefaultRunMemberRole, 'run_member') DefaultRunMemberRole", - ). - From("IR_Playbook AS p"). - LeftJoin("IR_Incident AS i ON p.ID = i.PlaybookID"). - LeftJoin("Teams t ON t.Id = p.TeamID"). - LeftJoin("Schemes s ON t.SchemeId = s.Id"). - GroupBy("p.ID"). - GroupBy("s.Id"). - Where(permissionsAndFilter). - Where(teamLimitExpr) - - if len(opts.PlaybookIDs) > 0 { - queryForResults = queryForResults.Where(sq.Eq{"p.ID": opts.PlaybookIDs}) - } - queryForResults, err := applyPlaybookFilterOptionsSort(queryForResults, opts) - if err != nil { - return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to apply sort options") - } - - queryForTotal := p.store.builder. - Select("COUNT(*)"). - From("IR_Playbook AS p"). - Where(permissionsAndFilter). - Where(teamLimitExpr) - - if opts.SearchTerm != "" { - column := "p.Title" - searchString := opts.SearchTerm - - // Postgres performs a case-sensitive search, so we need to lowercase - // both the column contents and the search string - if p.store.db.DriverName() == model.DatabaseDriverPostgres { - column = "LOWER(p.Title)" - searchString = strings.ToLower(opts.SearchTerm) - } - - queryForResults = queryForResults.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")}) - queryForTotal = queryForTotal.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")}) - } - - if !opts.WithArchived { - queryForResults = queryForResults.Where(sq.Eq{"p.DeleteAt": 0}) - queryForTotal = queryForTotal.Where(sq.Eq{"DeleteAt": 0}) - } - - var playbooks []app.Playbook - err = p.store.selectBuilder(p.store.db, &playbooks, queryForResults) - if err == sql.ErrNoRows { - return app.GetPlaybooksResults{}, errors.Wrap(app.ErrNotFound, "no playbooks found") - } else if err != nil { - return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get playbooks") - } - - var total int - if err = p.store.getBuilder(p.store.db, &total, queryForTotal); err != nil { - return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get total count") - } - - ids := make([]string, 0, len(playbooks)) - for _, pb := range playbooks { - ids = append(ids, pb.ID) - } - var members []playbookMember - err = p.store.selectBuilder(p.store.db, &members, p.membersSelect.Where(sq.Eq{"PlaybookID": ids})) - if err != nil { - return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get playbook members") - } - var metrics []app.PlaybookMetricConfig - err = p.store.selectBuilder(p.store.db, &metrics, p.metricsSelect.Where(sq.Eq{"PlaybookID": ids})) - if err != nil { - return app.GetPlaybooksResults{}, errors.Wrap(err, "failed to get playbooks metrics") - } - - addMembersToPlaybooks(members, playbooks) - addMetricsToPlaybooks(metrics, playbooks) - - pageCount := 0 - if opts.PerPage > 0 { - pageCount = int(math.Ceil(float64(total) / float64(opts.PerPage))) - } - hasMore := opts.Page+1 < pageCount - - return app.GetPlaybooksResults{ - TotalCount: total, - PageCount: pageCount, - HasMore: hasMore, - Items: playbooks, - }, nil -} - -// GetPlaybooksWithKeywords retrieves all playbooks with keywords enabled -func (p *playbookStore) GetPlaybooksWithKeywords(opts app.PlaybookFilterOptions) ([]app.Playbook, error) { - queryForResults := p.store.builder. - Select("ID", "Title", "UpdateAt", "TeamID", "ConcatenatedSignalAnyKeywords"). - From("IR_Playbook AS p"). - Where(sq.Eq{"SignalAnyKeywordsEnabled": true}). - Offset(uint64(opts.Page * opts.PerPage)). - Limit(uint64(opts.PerPage)) - - var rawPlaybooks []sqlPlaybook - err := p.store.selectBuilder(p.store.db, &rawPlaybooks, queryForResults) - if err == sql.ErrNoRows { - return []app.Playbook{}, nil - } else if err != nil { - return []app.Playbook{}, errors.Wrap(err, "failed to get playbooks") - } - - playbooks := make([]app.Playbook, 0, len(rawPlaybooks)) - for _, playbook := range rawPlaybooks { - out, err := toPlaybook(playbook) - if err != nil { - return nil, errors.Wrapf(err, "can't convert raw playbook to playbook type") - } - playbooks = append(playbooks, out) - } - return playbooks, nil -} - -// GetTimeLastUpdated retrieves time last playbook was updated at. -// Passed argument determines whether to include playbooks with -// SignalAnyKeywordsEnabled flag or not. -func (p *playbookStore) GetTimeLastUpdated(onlyPlaybooksWithKeywordsEnabled bool) (int64, error) { - queryForResults := p.store.builder. - Select("COALESCE(MAX(UpdateAt), 0)"). - From("IR_Playbook AS p"). - Where(sq.Eq{"DeleteAt": 0}) - if onlyPlaybooksWithKeywordsEnabled { - queryForResults = queryForResults.Where(sq.Eq{"SignalAnyKeywordsEnabled": true}) - } - - var updateAt []int64 - err := p.store.selectBuilder(p.store.db, &updateAt, queryForResults) - if err == sql.ErrNoRows { - return 0, nil - } else if err != nil { - return 0, errors.Wrap(err, "failed to get playbooks") - } - return updateAt[0], nil -} - -// GetPlaybookIDsForUser retrieves playbooks user can access -// Notice that method is not checking weather or not user is member of a team -func (p *playbookStore) GetPlaybookIDsForUser(userID string, teamID string) ([]string, error) { - // Check that you are a playbook member or there are no restrictions. - permissionsAndFilter := sq.Expr(`( - EXISTS(SELECT 1 - FROM IR_PlaybookMember as pm - WHERE pm.PlaybookID = p.ID - AND pm.MemberID = ?) - OR NOT EXISTS(SELECT 1 - FROM IR_PlaybookMember as pm - WHERE pm.PlaybookID = p.ID) - )`, userID) - - queryForResults := p.store.builder. - Select("ID"). - From("IR_Playbook AS p"). - Where(sq.Eq{"DeleteAt": 0}). - Where(sq.Eq{"TeamID": teamID}). - Where(permissionsAndFilter) - - var playbookIDs []string - - err := p.store.selectBuilder(p.store.db, &playbookIDs, queryForResults) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrapf(err, "failed to get playbookIDs for a user - %v", userID) - } - return playbookIDs, nil -} - -func (p *playbookStore) GraphqlUpdate(id string, setmap map[string]interface{}) error { - if id == "" { - return errors.New("id should not be empty") - } - - // if checklists are passed and len (as string) is bigger than limit -> fails - if _, exists := setmap["ChecklistsJSON"]; exists { - if len(string(setmap["ChecklistsJSON"].([]uint8))) > maxJSONLength { - return fmt.Errorf("failed update playbook with id '%s': json too long (max %d)", id, maxJSONLength) - } - } - - _, err := p.store.execBuilder(p.store.db, sq. - Update("IR_Playbook"). - SetMap(setmap). - Where(sq.Eq{"ID": id})) - - if err != nil { - return errors.Wrapf(err, "failed to update playbook with id '%s'", id) - } - - return nil -} - -// Update updates a playbook -func (p *playbookStore) Update(playbook app.Playbook) (err error) { - if playbook.ID == "" { - return errors.New("id should not be empty") - } - - rawPlaybook, err := toSQLPlaybook(playbook) - if err != nil { - return err - } - - tx, err := p.store.db.Beginx() - if err != nil { - return errors.Wrap(err, "could not begin transaction") - } - defer p.store.finalizeTransaction(tx) - - _, err = p.store.execBuilder(tx, sq. - Update("IR_Playbook"). - SetMap(map[string]interface{}{ - "Title": rawPlaybook.Title, - "Description": rawPlaybook.Description, - "TeamID": rawPlaybook.TeamID, - "Public": rawPlaybook.Public, - "CreatePublicIncident": rawPlaybook.CreatePublicPlaybookRun, - "UpdateAt": rawPlaybook.UpdateAt, - "DeleteAt": rawPlaybook.DeleteAt, - "ChecklistsJSON": rawPlaybook.ChecklistsJSON, - "NumStages": len(rawPlaybook.Checklists), - "NumSteps": getSteps(rawPlaybook.Playbook), - "ReminderMessageTemplate": rawPlaybook.ReminderMessageTemplate, - "ReminderTimerDefaultSeconds": rawPlaybook.ReminderTimerDefaultSeconds, - "StatusUpdateEnabled": rawPlaybook.StatusUpdateEnabled, - "ConcatenatedInvitedUserIDs": rawPlaybook.ConcatenatedInvitedUserIDs, - "ConcatenatedInvitedGroupIDs": rawPlaybook.ConcatenatedInvitedGroupIDs, - "InviteUsersEnabled": rawPlaybook.InviteUsersEnabled, - "DefaultCommanderID": rawPlaybook.DefaultOwnerID, - "DefaultCommanderEnabled": rawPlaybook.DefaultOwnerEnabled, - "ConcatenatedBroadcastChannelIDs": rawPlaybook.ConcatenatedBroadcastChannelIDs, - "BroadcastEnabled": rawPlaybook.BroadcastEnabled, //nolint - "ConcatenatedWebhookOnCreationURLs": rawPlaybook.ConcatenatedWebhookOnCreationURLs, - "WebhookOnCreationEnabled": rawPlaybook.WebhookOnCreationEnabled, - "MessageOnJoin": rawPlaybook.MessageOnJoin, - "MessageOnJoinEnabled": rawPlaybook.MessageOnJoinEnabled, - "RetrospectiveReminderIntervalSeconds": rawPlaybook.RetrospectiveReminderIntervalSeconds, - "RetrospectiveTemplate": rawPlaybook.RetrospectiveTemplate, - "RetrospectiveEnabled": rawPlaybook.RetrospectiveEnabled, - "ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs, - "WebhookOnStatusUpdateEnabled": rawPlaybook.WebhookOnStatusUpdateEnabled, - "ConcatenatedSignalAnyKeywords": rawPlaybook.ConcatenatedSignalAnyKeywords, - "SignalAnyKeywordsEnabled": rawPlaybook.SignalAnyKeywordsEnabled, - "CategorizeChannelEnabled": rawPlaybook.CategorizeChannelEnabled, - "CategoryName": rawPlaybook.CategoryName, - "RunSummaryTemplateEnabled": rawPlaybook.RunSummaryTemplateEnabled, - "RunSummaryTemplate": rawPlaybook.RunSummaryTemplate, - "ChannelNameTemplate": rawPlaybook.ChannelNameTemplate, - "CreateChannelMemberOnNewParticipant": rawPlaybook.CreateChannelMemberOnNewParticipant, - "RemoveChannelMemberOnRemovedParticipant": rawPlaybook.RemoveChannelMemberOnRemovedParticipant, - "ChannelID": rawPlaybook.ChannelID, - "ChannelMode": rawPlaybook.ChannelMode, - }). - Where(sq.Eq{"ID": rawPlaybook.ID})) - - if err != nil { - return errors.Wrapf(err, "failed to update playbook with id '%s'", rawPlaybook.ID) - } - - if err = p.replacePlaybookMembers(tx, rawPlaybook.Playbook); err != nil { - return errors.Wrapf(err, "failed to replace playbook members for playbook with id '%s'", rawPlaybook.ID) - } - - if err = p.replacePlaybookMetrics(tx, rawPlaybook.Playbook); err != nil { - return errors.Wrapf(err, "failed to replace playbook metrics configs for playbook with id '%s'", rawPlaybook.ID) - } - - if err = tx.Commit(); err != nil { - return errors.Wrap(err, "could not commit transaction") - } - - return nil -} - -// Archive archives a playbook. -func (p *playbookStore) Archive(id string) error { - if id == "" { - return errors.New("ID cannot be empty") - } - - _, err := p.store.execBuilder(p.store.db, sq. - Update("IR_Playbook"). - Set("DeleteAt", model.GetMillis()). - Where(sq.Eq{"ID": id})) - - if err != nil { - return errors.Wrapf(err, "failed to delete playbook with id '%s'", id) - } - - return nil -} - -// Restore restores a deleted playbook. -func (p *playbookStore) Restore(id string) error { - if id == "" { - return errors.New("ID cannot be empty") - } - - _, err := p.store.execBuilder(p.store.db, sq. - Update("IR_Playbook"). - Set("DeleteAt", 0). - Where(sq.Eq{"ID": id})) - - if err != nil { - return errors.Wrapf(err, "failed to restore playbook with id '%s'", id) - } - - return nil -} - -// Get number of active playbooks. -func (p *playbookStore) GetPlaybooksActiveTotal() (int64, error) { - var count int64 - - query := p.store.builder. - Select("COUNT(*)"). - From("IR_Playbook"). - Where(sq.Eq{"DeleteAt": 0}) - - if err := p.store.getBuilder(p.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count active playbooks'") - } - - return count, nil -} - -// Get number of active playbooks. -func (p *playbookStore) GetNumMetrics(playbookID string) (int64, error) { - var count int64 - - query := p.store.builder. - Select("COUNT(*)"). - From("IR_MetricConfig"). - Where(sq.Eq{"PlaybookID": playbookID}) - - if err := p.store.getBuilder(p.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count metrics") - } - - return count, nil -} - -func (p *playbookStore) AddPlaybookMember(id string, memberID string) error { - if id == "" || memberID == "" { - return errors.New("ids should not be empty") - } - - _, err := p.store.execBuilder(p.store.db, sq. - Insert("IR_PlaybookMember"). - Columns("PlaybookID", "MemberID", "Roles"). - Values(id, memberID, app.PlaybookRoleMember)) - - if err != nil { - return errors.Wrapf(err, "failed to update playbook with id '%s'", id) - } - - return nil -} - -func (p *playbookStore) RemovePlaybookMember(id string, memberID string) error { - if id == "" || memberID == "" { - return errors.New("ids should not be empty") - } - - _, err := p.store.execBuilder(p.store.db, sq. - Delete("IR_PlaybookMember"). - Where(sq.Eq{"PlaybookID": id}). - Where(sq.Eq{"MemberID": memberID})) - - if err != nil { - return errors.Wrapf(err, "failed to update playbook with id '%s'", id) - } - - return nil -} - -// replacePlaybookMembers replaces the members of a playbook -func (p *playbookStore) replacePlaybookMembers(q queryExecer, playbook app.Playbook) error { - // Delete existing members who are not in the new playbook.MemberIDs list - delBuilder := sq.Delete("IR_PlaybookMember"). - Where(sq.Eq{"PlaybookID": playbook.ID}) - if _, err := p.store.execBuilder(q, delBuilder); err != nil { - return err - } - - if len(playbook.Members) == 0 { - return nil - } - - insert := sq. - Insert("IR_PlaybookMember"). - Columns("PlaybookID", "MemberID", "Roles") - - for _, m := range playbook.Members { - insert = insert.Values(playbook.ID, m.UserID, strings.Join(m.Roles, " ")) - } - - if _, err := p.store.execBuilder(q, insert); err != nil { - return err - } - - return nil -} - -// replacePlaybookMetrics replaces the metric configs of a playbook -func (p *playbookStore) replacePlaybookMetrics(q queryExecer, playbook app.Playbook) error { - // First, we mark as deleted all existing metrics for this playbook, then restore those which are in the playbook object. - updateBuilder := sq.Update("IR_MetricConfig"). - Set("DeleteAt", model.GetMillis()). - Where(sq.Eq{"PlaybookID": playbook.ID}). - Where(sq.Eq{"DeleteAt": 0}) - - if _, err := p.store.execBuilder(q, updateBuilder); err != nil { - return err - } - - // Restore and update existing metric configs. Insert a new ones. - var err error - for i, m := range playbook.Metrics { - if m.ID == "" { - _, err = p.store.execBuilder(q, sq. - Insert("IR_MetricConfig"). - Columns("ID", "PlaybookID", "Title", "Description", "Type", "Target", "Ordering"). - Values(model.NewId(), playbook.ID, m.Title, m.Description, m.Type, m.Target, i)) - } else { - _, err = p.store.execBuilder(q, sq. - Update("IR_MetricConfig"). - SetMap(map[string]interface{}{ - "Title": m.Title, - "Description": m.Description, - "Target": m.Target, - "Ordering": i, - "DeleteAt": 0, - }). - Where(sq.Eq{"ID": m.ID}), - ) - } - if err != nil { - return err - } - } - - return nil -} - -func (p *playbookStore) AutoFollow(playbookID, userID string) error { - var err error - if p.store.db.DriverName() == model.DatabaseDriverMysql { - _, err = p.store.execBuilder(p.store.db, sq. - Insert("IR_PlaybookAutoFollow"). - Columns("PlaybookID", "UserID"). - Values(playbookID, userID). - Suffix("ON DUPLICATE KEY UPDATE playbookID = playbookID")) - } else { - _, err = p.store.execBuilder(p.store.db, sq. - Insert("IR_PlaybookAutoFollow"). - Columns("PlaybookID", "UserID"). - Values(playbookID, userID). - Suffix("ON CONFLICT (PlaybookID,UserID) DO NOTHING")) - } - return errors.Wrapf(err, "failed to insert autofollowing '%s' for playbook '%s'", userID, playbookID) -} - -func (p *playbookStore) AutoUnfollow(playbookID, userID string) error { - if _, err := p.store.execBuilder(p.store.db, sq. - Delete("IR_PlaybookAutoFollow"). - Where(sq.And{sq.Eq{"UserID": userID}, sq.Eq{"PlaybookID": playbookID}})); err != nil { - return errors.Wrapf(err, "failed to delete autofollow '%s' for playbook '%s'", userID, playbookID) - } - return nil -} - -func (p *playbookStore) GetAutoFollows(playbookID string) ([]string, error) { - query := p.queryBuilder. - Select("UserID"). - From("IR_PlaybookAutoFollow"). - Where(sq.Eq{"PlaybookID": playbookID}) - - autoFollows := make([]string, 0) - err := p.store.selectBuilder(p.store.db, &autoFollows, query) - if err == sql.ErrNoRows { - return []string{}, nil - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get autoFollows for playbook '%s'", playbookID) - } - - return autoFollows, nil -} - -func (p *playbookStore) GetMetric(id string) (*app.PlaybookMetricConfig, error) { - metricSelect := p.queryBuilder. - Select( - "c.ID", - "c.PlaybookID", - "c.Title", - "c.Description", - "c.Type", - "c.Target", - ). - From("IR_MetricConfig c"). - Where(sq.Eq{"c.ID": id}) - - var metric app.PlaybookMetricConfig - err := p.store.getBuilder(p.store.db, &metric, metricSelect) - if err != nil { - return nil, err - } - - return &metric, nil -} - -func (p *playbookStore) AddMetric(playbookID string, config app.PlaybookMetricConfig) error { - numExistingMetrics, err := p.GetNumMetrics(playbookID) - if err != nil { - return err - } - - if numExistingMetrics >= app.MaxMetricsPerPlaybook { - return errors.Errorf("playbook cannot have more than %d key metrics", app.MaxMetricsPerPlaybook) - } - - _, err = p.store.execBuilder(p.store.db, sq. - Insert("IR_MetricConfig"). - Columns("ID", "PlaybookID", "Title", "Description", "Type", "Target", "Ordering"). - Values(model.NewId(), playbookID, config.Title, config.Description, config.Type, config.Target, numExistingMetrics)) - - if err != nil { - return errors.Wrapf(err, "failed to add metric") - } - - return nil -} - -func (p *playbookStore) DeleteMetric(id string) error { - if id == "" { - return errors.New("id should not be empty") - } - - _, err := p.store.execBuilder(p.store.db, sq. - Update("IR_MetricConfig"). - Set("DeleteAt", model.GetMillis()). - Where(sq.Eq{"ID": id})) - - if err != nil { - return errors.Wrapf(err, "failed to delete metric with id %q", id) - } - - return nil -} - -func (p *playbookStore) UpdateMetric(id string, setmap map[string]interface{}) error { - if id == "" { - return errors.New("id should not be empty") - } - - _, err := p.store.execBuilder(p.store.db, sq. - Update("IR_MetricConfig"). - SetMap(setmap). - Where(sq.Eq{"ID": id})) - - if err != nil { - return errors.Wrapf(err, "failed to update metric with id %q", id) - } - - return nil -} - -func generatePlaybookSchemeRoles(member playbookMember, playbook *app.Playbook) []string { - schemeRoles := []string{} - for _, role := range strings.Fields(member.Roles) { - if role == app.PlaybookRoleAdmin { - if playbook.DefaultPlaybookAdminRole == "" { - schemeRoles = append(schemeRoles, app.PlaybookRoleAdmin) - } else { - schemeRoles = append(schemeRoles, playbook.DefaultPlaybookAdminRole) - } - } else if role == app.PlaybookRoleMember { - if playbook.DefaultPlaybookMemberRole == "" { - schemeRoles = append(schemeRoles, app.PlaybookRoleMember) - } else { - schemeRoles = append(schemeRoles, playbook.DefaultPlaybookMemberRole) - } - } - } - - return schemeRoles -} - -func addMembersToPlaybooks(members []playbookMember, playbooks []app.Playbook) { - playbookToMembers := make(map[string][]playbookMember) - for _, member := range members { - playbookToMembers[member.PlaybookID] = append(playbookToMembers[member.PlaybookID], member) - } - - for i, playbook := range playbooks { - addMembersToPlaybook(playbookToMembers[playbook.ID], &(playbooks[i])) - } -} - -func addMembersToPlaybook(members []playbookMember, playbook *app.Playbook) { - for _, m := range members { - playbook.Members = append(playbook.Members, app.PlaybookMember{ - UserID: m.MemberID, - Roles: strings.Fields(m.Roles), - SchemeRoles: generatePlaybookSchemeRoles(m, playbook), - }) - } -} - -func addMetricsToPlaybooks(metrics []app.PlaybookMetricConfig, playbooks []app.Playbook) { - playbookToMetrics := make(map[string][]app.PlaybookMetricConfig) - for _, metric := range metrics { - playbookToMetrics[metric.PlaybookID] = append(playbookToMetrics[metric.PlaybookID], metric) - } - - for i, playbook := range playbooks { - playbooks[i].Metrics = playbookToMetrics[playbook.ID] - } -} - -func getSteps(playbook app.Playbook) int { - steps := 0 - for _, p := range playbook.Checklists { - steps += len(p.Items) - } - - return steps -} - -func toSQLPlaybook(playbook app.Playbook) (*sqlPlaybook, error) { - checklistsJSON, err := json.Marshal(playbook.Checklists) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal checklist json for playbook id: '%s'", playbook.ID) - } - - if len(checklistsJSON) > maxJSONLength { - return nil, errors.Wrapf(errors.New("invalid data"), "checklist json for playbook id '%s' is too long (max %d)", playbook.ID, maxJSONLength) - } - - return &sqlPlaybook{ - Playbook: playbook, - ChecklistsJSON: checklistsJSON, - ConcatenatedInvitedUserIDs: strings.Join(playbook.InvitedUserIDs, ","), - ConcatenatedInvitedGroupIDs: strings.Join(playbook.InvitedGroupIDs, ","), - ConcatenatedSignalAnyKeywords: strings.Join(playbook.SignalAnyKeywords, ","), - ConcatenatedBroadcastChannelIDs: strings.Join(playbook.BroadcastChannelIDs, ","), - ConcatenatedWebhookOnCreationURLs: strings.Join(playbook.WebhookOnCreationURLs, ","), - ConcatenatedWebhookOnStatusUpdateURLs: strings.Join(playbook.WebhookOnStatusUpdateURLs, ","), - }, nil -} - -func toPlaybook(rawPlaybook sqlPlaybook) (app.Playbook, error) { - p := rawPlaybook.Playbook - if len(rawPlaybook.ChecklistsJSON) > 0 { - if err := json.Unmarshal(rawPlaybook.ChecklistsJSON, &p.Checklists); err != nil { - return app.Playbook{}, errors.Wrapf(err, "failed to unmarshal checklists json for playbook id: '%s'", p.ID) - } - } - - p.InvitedUserIDs = []string(nil) - if rawPlaybook.ConcatenatedInvitedUserIDs != "" { - p.InvitedUserIDs = strings.Split(rawPlaybook.ConcatenatedInvitedUserIDs, ",") - } - - p.InvitedGroupIDs = []string(nil) - if rawPlaybook.ConcatenatedInvitedGroupIDs != "" { - p.InvitedGroupIDs = strings.Split(rawPlaybook.ConcatenatedInvitedGroupIDs, ",") - } - - p.SignalAnyKeywords = []string(nil) - if rawPlaybook.ConcatenatedSignalAnyKeywords != "" { - p.SignalAnyKeywords = strings.Split(rawPlaybook.ConcatenatedSignalAnyKeywords, ",") - } - - p.BroadcastChannelIDs = []string(nil) - if rawPlaybook.ConcatenatedBroadcastChannelIDs != "" { - p.BroadcastChannelIDs = strings.Split(rawPlaybook.ConcatenatedBroadcastChannelIDs, ",") - } - - p.WebhookOnCreationURLs = []string(nil) - if rawPlaybook.ConcatenatedWebhookOnCreationURLs != "" { - p.WebhookOnCreationURLs = strings.Split(rawPlaybook.ConcatenatedWebhookOnCreationURLs, ",") - } - - p.WebhookOnStatusUpdateURLs = []string(nil) - if rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs != "" { - p.WebhookOnStatusUpdateURLs = strings.Split(rawPlaybook.ConcatenatedWebhookOnStatusUpdateURLs, ",") - } - return p, nil -} - -// insights - store manager functions - -func (p *playbookStore) GetTopPlaybooksForTeam(teamID, userID string, opts *model.InsightsOpts) (*app.PlaybooksInsightsList, error) { - - query := insightsQueryBuilder(p, teamID, userID, opts, insightsQueryTypeTeam) - - topPlaybooksList := make([]*app.PlaybookInsight, 0) - err := p.store.selectBuilder(p.store.db, &topPlaybooksList, query) - if err != nil { - return nil, errors.Wrapf(err, "failed to get top team playbooks for for user: %s", userID) - } - - topPlaybooks := GetTopPlaybooksInsightsListWithPagination(topPlaybooksList, opts.PerPage) - - return topPlaybooks, nil -} - -func (p *playbookStore) GetTopPlaybooksForUser(teamID, userID string, opts *model.InsightsOpts) (*app.PlaybooksInsightsList, error) { - - query := insightsQueryBuilder(p, teamID, userID, opts, insightsQueryTypeUser) - - topPlaybooksList := make([]*app.PlaybookInsight, 0) - err := p.store.selectBuilder(p.store.db, &topPlaybooksList, query) - if err != nil { - return nil, errors.Wrapf(err, "failed to get top user playbooks for for user: %s", userID) - } - - topPlaybooks := GetTopPlaybooksInsightsListWithPagination(topPlaybooksList, opts.PerPage) - - return topPlaybooks, nil -} - -func insightsQueryBuilder(p *playbookStore, teamID, userID string, opts *model.InsightsOpts, queryType string) sq.SelectBuilder { - permissionsAndFilter := sq.Expr(`( - EXISTS(SELECT 1 - FROM IR_PlaybookMember as pm - WHERE pm.PlaybookID = p.ID - AND pm.MemberID = ?) - )`, userID) - - var whereCondition sq.And - if queryType == insightsQueryTypeUser { - whereCondition = sq.And{ - permissionsAndFilter, - sq.Eq{"p.TeamID": teamID}, - sq.GtOrEq{"i.CreateAt": opts.StartUnixMilli}, - } - } else if queryType == insightsQueryTypeTeam { - whereCondition = sq.And{ - sq.GtOrEq{"i.CreateAt": opts.StartUnixMilli}, - sq.Or{ - permissionsAndFilter, - sq.Eq{"p.Public": true}, - }, - sq.Eq{"p.TeamID": teamID}, - } - } else { - whereCondition = sq.And{} - } - offset := opts.Page * opts.PerPage - limit := opts.PerPage - query := p.queryBuilder. - Select( - "p.ID as PlaybookID", - "p.Title", - "COUNT(i.ID) AS NumRuns", - "COALESCE(MAX(i.CreateAt), 0) AS LastRunAt", - ). - From("IR_Playbook as p"). - LeftJoin("IR_Incident AS i ON p.ID = i.PlaybookID"). - Where(whereCondition). - GroupBy("p.ID"). - OrderBy("NumRuns desc"). - Offset(uint64(offset)). - Limit(uint64(limit + 1)) - - return query -} - -// GetTopPlaybooksInsightsListWithPagination returns a page given a list of PlaybooksInsight assumed to be -// sorted by Runs(score). Returns a PlaybooksInsightsList. -func GetTopPlaybooksInsightsListWithPagination(playbooks []*app.PlaybookInsight, limit int) *app.PlaybooksInsightsList { - // Add pagination support - var hasNext bool - if (limit != 0) && (len(playbooks) == limit+1) { - hasNext = true - playbooks = playbooks[:len(playbooks)-1] - } - - return &app.PlaybooksInsightsList{HasNext: hasNext, Items: playbooks} -} diff --git a/server/playbooks/server/sqlstore/playbook_run.go b/server/playbooks/server/sqlstore/playbook_run.go deleted file mode 100644 index e749b4ae5a8..00000000000 --- a/server/playbooks/server/sqlstore/playbook_run.go +++ /dev/null @@ -1,1751 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - "fmt" - "math" - "strings" - "time" - - "gopkg.in/guregu/null.v4" - - "github.com/jmoiron/sqlx" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -const ( - legacyEventTypeCommanderChanged = "commander_changed" -) - -type sqlPlaybookRun struct { - app.PlaybookRun - ChecklistsJSON json.RawMessage - ConcatenatedInvitedUserIDs string - ConcatenatedInvitedGroupIDs string - ConcatenatedParticipantIDs string - ConcatenatedBroadcastChannelIDs string - ConcatenatedWebhookOnCreationURLs string - ConcatenatedWebhookOnStatusUpdateURLs string - Metric null.Int -} - -type sqlRunMetricData struct { - IncidentID string - MetricConfigID string - Value null.Int -} - -// playbookRunStore holds the information needed to fulfill the methods in the store interface. -type playbookRunStore struct { - pluginAPI PluginAPIClient - store *SQLStore - queryBuilder sq.StatementBuilderType - playbookRunSelect sq.SelectBuilder - statusPostsSelect sq.SelectBuilder - timelineEventsSelect sq.SelectBuilder - metricsDataSelectSingleRun sq.SelectBuilder - sqlMetricsDataSelectMultipleRuns sq.SelectBuilder -} - -// Ensure playbookRunStore implements the app.PlaybookRunStore interface. -var _ app.PlaybookRunStore = (*playbookRunStore)(nil) - -type playbookRunStatusPosts []struct { - PlaybookRunID string - app.StatusPost -} - -func applyPlaybookRunFilterOptionsSort(builder sq.SelectBuilder, options app.PlaybookRunFilterOptions) (sq.SelectBuilder, error) { - var sort string - switch options.Sort { - case app.SortByCreateAt: - sort = "CreateAt" - case app.SortByID: - sort = "ID" - case app.SortByName: - sort = "Name" - case app.SortByOwnerUserID: - sort = "OwnerUserID" - case app.SortByTeamID: - sort = "TeamID" - case app.SortByEndAt: - sort = "EndAt" - case app.SortByStatus: - sort = "CurrentStatus" - case app.SortByLastStatusUpdateAt: - sort = "LastStatusUpdateAt" - case "": - // Default to a stable sort if none explicitly provided. - sort = "ID" - case app.SortByMetric0, app.SortByMetric1, app.SortByMetric2, app.SortByMetric3: - // Will handle below - default: - return sq.SelectBuilder{}, errors.Errorf("unsupported sort parameter '%s'", options.Sort) - } - - var direction string - switch options.Direction { - case app.DirectionAsc: - direction = "ASC" - case app.DirectionDesc: - direction = "DESC" - case "": - // Default to an ascending sort if none explicitly provided. - direction = "ASC" - default: - return sq.SelectBuilder{}, errors.Errorf("unsupported direction parameter '%s'", options.Direction) - } - - page := options.Page - perPage := options.PerPage - if page < 0 { - page = 0 - } - if perPage < 0 { - perPage = 0 - } - - builder = builder. - Offset(uint64(page * perPage)). - Limit(uint64(perPage)) - - switch options.Sort { - case app.SortByMetric0, app.SortByMetric1, app.SortByMetric2, app.SortByMetric3: - if options.PlaybookID == "" { - return sq.SelectBuilder{}, errors.New("sorting by metric requires a playbook_id") - } - - ordering := 0 - switch options.Sort { - case app.SortByMetric1: - ordering = 1 - case app.SortByMetric2: - ordering = 2 - case app.SortByMetric3: - ordering = 3 - } - - // Since we're sorting by metric, we need to create the correct metric column to sort by - builder = builder.Column( - sq.Alias( - sq.Select("m.Value"). - From("IR_Metric AS m"). - InnerJoin("IR_MetricConfig AS mc ON (mc.ID = m.MetricConfigID)"). - Where("mc.DeleteAt = 0"). - Where(sq.Eq{"mc.PlaybookID": options.PlaybookID}). - Where("m.IncidentID = i.ID"). - Where(sq.Eq{"mc.Ordering": ordering}), - "Metric", - )). - OrderByClause("Metric " + direction) - default: - builder = builder.OrderByClause(fmt.Sprintf("%s %s", sort, direction)) - } - - return builder, nil -} - -// NewPlaybookRunStore creates a new store for playbook run ServiceImpl. -func NewPlaybookRunStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) app.PlaybookRunStore { - // construct the participants list so that the frontend doesn't have to query the server, bc if - // the user is not a member of the channel they won't have permissions to get the user list - participantsCol := ` - COALESCE( - (SELECT string_agg(rp.UserId, ',') - FROM IR_Incident as i2 - JOIN IR_Run_Participants as rp on rp.IncidentID = i2.ID - WHERE i2.Id = i.Id - AND rp.IsParticipant = true - AND rp.UserId NOT IN (SELECT UserId FROM Bots) - ), '' - ) AS ConcatenatedParticipantIDs` - if sqlStore.db.DriverName() == model.DatabaseDriverMysql { - participantsCol = ` - COALESCE( - (SELECT group_concat(rp.UserId separator ',') - FROM IR_Incident as i2 - JOIN IR_Run_Participants as rp on rp.IncidentID = i2.ID - WHERE i2.Id = i.Id - AND rp.IsParticipant = true - AND rp.UserId NOT IN (SELECT UserId FROM Bots) - ), '' - ) AS ConcatenatedParticipantIDs` - } - - // When adding a PlaybookRun column #1: add to this select - playbookRunSelect := sqlStore.builder. - Select("i.ID", "i.Name AS Name", "i.Description AS Summary", "i.CommanderUserID AS OwnerUserID", "i.TeamID", "i.ChannelID", - "i.CreateAt", "i.EndAt", "i.DeleteAt", "i.PostID", "i.PlaybookID", "i.ReporterUserID", "i.CurrentStatus", "i.LastStatusUpdateAt", - "i.ChecklistsJSON", "COALESCE(i.ReminderPostID, '') ReminderPostID", "i.PreviousReminder", - "COALESCE(ReminderMessageTemplate, '') ReminderMessageTemplate", "ReminderTimerDefaultSeconds", "StatusUpdateEnabled", - "ConcatenatedInvitedUserIDs", "ConcatenatedInvitedGroupIDs", "DefaultCommanderID AS DefaultOwnerID", - "ConcatenatedBroadcastChannelIDs", "ConcatenatedWebhookOnCreationURLs", "Retrospective", "RetrospectiveEnabled", "MessageOnJoin", "RetrospectivePublishedAt", "RetrospectiveReminderIntervalSeconds", - "RetrospectiveWasCanceled", "ConcatenatedWebhookOnStatusUpdateURLs", "StatusUpdateBroadcastChannelsEnabled", "StatusUpdateBroadcastWebhooksEnabled", - "CreateChannelMemberOnNewParticipant", "RemoveChannelMemberOnRemovedParticipant", - "COALESCE(CategoryName, '') CategoryName", "SummaryModifiedAt", "i.RunType AS Type"). - Column(participantsCol). - From("IR_Incident AS i") - - statusPostsSelect := sqlStore.builder. - Select("sp.IncidentID AS PlaybookRunID", "p.ID", "p.CreateAt", "p.DeleteAt"). - From("IR_StatusPosts as sp"). - Join("Posts as p ON sp.PostID = p.Id") - - timelineEventsSelect := sqlStore.builder. - Select( - "te.ID", - "te.IncidentID AS PlaybookRunID", - "te.CreateAt", - "te.DeleteAt", - "te.EventAt", - ). - // Map "commander_changed" to "owner_changed", preserving database compatibility - // without complicating the code. - Column( - sq.Alias( - sq.Case(). - When(sq.Eq{"te.EventType": legacyEventTypeCommanderChanged}, sq.Expr("?", app.OwnerChanged)). - Else("te.EventType"), - "EventType", - ), - ). - Columns( - "te.Summary", - "te.Details", - "te.PostID", - "te.SubjectUserID", - "te.CreatorUserID", - ). - From("IR_TimelineEvent as te") - - metricsDataSelectSingleRun := sqlStore.builder. - Select("MetricConfigID", "Value"). - From("IR_Metric AS m"). - Join("IR_MetricConfig AS mc ON (mc.ID = m.MetricConfigID)"). - Where("mc.DeleteAt = 0") - - sqlMetricsDataSelectMultipleRuns := sqlStore.builder. - Select("IncidentID", "MetricConfigID", "Value"). - From("IR_Metric AS m"). - Join("IR_MetricConfig AS mc ON (mc.ID = m.MetricConfigID)"). - Where("mc.DeleteAt = 0"). - OrderBy("mc.Ordering ASC") - - return &playbookRunStore{ - pluginAPI: pluginAPI, - store: sqlStore, - queryBuilder: sqlStore.builder, - playbookRunSelect: playbookRunSelect, - statusPostsSelect: statusPostsSelect, - timelineEventsSelect: timelineEventsSelect, - metricsDataSelectSingleRun: metricsDataSelectSingleRun, - sqlMetricsDataSelectMultipleRuns: sqlMetricsDataSelectMultipleRuns, - } -} - -// GetPlaybookRuns returns filtered playbook runs and the total count before paging. -func (s *playbookRunStore) GetPlaybookRuns(requesterInfo app.RequesterInfo, options app.PlaybookRunFilterOptions) (*app.GetPlaybookRunsResults, error) { - permissionsExpr := s.buildPermissionsExpr(requesterInfo) - teamLimitExpr := buildTeamLimitExpr(requesterInfo, options.TeamID, "i") - - queryForResults := s.playbookRunSelect. - Where(permissionsExpr). - Where(teamLimitExpr) - - queryForTotal := s.store.builder. - Select("COUNT(*)"). - From("IR_Incident AS i"). - Where(permissionsExpr). - Where(teamLimitExpr) - - if len(options.Statuses) != 0 { - queryForResults = queryForResults.Where(sq.Eq{"i.CurrentStatus": options.Statuses}) - queryForTotal = queryForTotal.Where(sq.Eq{"i.CurrentStatus": options.Statuses}) - } - - if len(options.Types) != 0 { - queryForResults = queryForResults.Where(sq.Eq{"i.RunType": options.Types}) - queryForTotal = queryForTotal.Where(sq.Eq{"i.RunType": options.Types}) - } - - if options.OwnerID != "" { - queryForResults = queryForResults.Where(sq.Eq{"i.CommanderUserID": options.OwnerID}) - queryForTotal = queryForTotal.Where(sq.Eq{"i.CommanderUserID": options.OwnerID}) - } - - if options.ParticipantID != "" { - membershipClause := s.queryBuilder. - Select("1"). - Prefix("EXISTS("). - From("IR_Run_Participants AS p"). - Where("p.IncidentID = i.ID"). - Where("p.IsParticipant = true"). - Where(sq.Eq{"p.UserID": strings.ToLower(options.ParticipantID)}). - Suffix(")") - - queryForResults = queryForResults.Where(membershipClause) - queryForTotal = queryForTotal.Where(membershipClause) - } - - if options.ParticipantOrFollowerID != "" { - userIDFilter := strings.ToLower(options.ParticipantOrFollowerID) - followerFilterExpr := sq.Expr(`EXISTS(SELECT 1 - FROM IR_Run_Participants as rp - WHERE rp.IncidentID = i.ID - AND rp.UserID = ? - AND rp.IsFollower = TRUE)`, userIDFilter) - participantFilterExpr := sq.Expr(`EXISTS(SELECT 1 - FROM IR_Run_Participants as rp - WHERE rp.IncidentID = i.ID - AND rp.UserID = ? - AND rp.IsParticipant = TRUE)`, userIDFilter) - myRunsClause := sq.Or{followerFilterExpr, participantFilterExpr} - - if options.IncludeFavorites { - favoriteFilterExpr := sq.Expr(`EXISTS(SELECT 1 - FROM IR_Category AS cat - INNER JOIN IR_Category_Item it ON cat.ID = it.CategoryID - WHERE cat.Name = 'Favorite' - AND it.Type = 'r' - AND it.ItemID = i.ID - AND cat.UserID = ?)`, userIDFilter) - myRunsClause = append(myRunsClause, favoriteFilterExpr) - } - - queryForResults = queryForResults.Where(myRunsClause) - queryForTotal = queryForTotal.Where(myRunsClause) - } - - if options.PlaybookID != "" { - queryForResults = queryForResults.Where(sq.Eq{"i.PlaybookID": options.PlaybookID}) - queryForTotal = queryForTotal.Where(sq.Eq{"i.PlaybookID": options.PlaybookID}) - } - - // TODO: do we need to sanitize (replace any '%'s in the search term)? - if options.SearchTerm != "" { - column := "i.Name" - searchString := options.SearchTerm - - // Postgres performs a case-sensitive search, so we need to lowercase - // both the column contents and the search string - if s.store.db.DriverName() == model.DatabaseDriverPostgres { - column = "LOWER(i.Name)" - searchString = strings.ToLower(options.SearchTerm) - } - - queryForResults = queryForResults.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")}) - queryForTotal = queryForTotal.Where(sq.Like{column: fmt.Sprint("%", searchString, "%")}) - } - - if options.ChannelID != "" { - queryForResults = queryForResults.Where(sq.Eq{"i.ChannelId": options.ChannelID}) - queryForTotal = queryForTotal.Where(sq.Eq{"i.ChannelId": options.ChannelID}) - } - - queryForResults = queryActiveBetweenTimes(queryForResults, options.ActiveGTE, options.ActiveLT) - queryForTotal = queryActiveBetweenTimes(queryForTotal, options.ActiveGTE, options.ActiveLT) - - queryForResults = queryStartedBetweenTimes(queryForResults, options.StartedGTE, options.StartedLT) - queryForTotal = queryStartedBetweenTimes(queryForTotal, options.StartedGTE, options.StartedLT) - - queryForResults, err := applyPlaybookRunFilterOptionsSort(queryForResults, options) - if err != nil { - return nil, errors.Wrap(err, "failed to apply sort options") - } - - tx, err := s.store.db.Beginx() - if err != nil { - return nil, errors.Wrap(err, "could not begin transaction") - } - defer s.store.finalizeTransaction(tx) - - var rawPlaybookRuns []sqlPlaybookRun - if err = s.store.selectBuilder(tx, &rawPlaybookRuns, queryForResults); err != nil { - return nil, errors.Wrap(err, "failed to query for playbook runs") - } - - var total int - if err = s.store.getBuilder(tx, &total, queryForTotal); err != nil { - return nil, errors.Wrap(err, "failed to get total count") - } - pageCount := 0 - if options.PerPage > 0 { - pageCount = int(math.Ceil(float64(total) / float64(options.PerPage))) - } - hasMore := options.Page+1 < pageCount - - playbookRuns := make([]app.PlaybookRun, 0, len(rawPlaybookRuns)) - playbookRunIDs := make([]string, 0, len(rawPlaybookRuns)) - for _, rawPlaybookRun := range rawPlaybookRuns { - var playbookRun *app.PlaybookRun - playbookRun, err = s.toPlaybookRun(rawPlaybookRun) - if err != nil { - return nil, err - } - playbookRuns = append(playbookRuns, *playbookRun) - playbookRunIDs = append(playbookRunIDs, playbookRun.ID) - } - - var statusPosts playbookRunStatusPosts - - postInfoSelect := s.statusPostsSelect. - OrderBy("p.CreateAt"). - Where(sq.Eq{"sp.IncidentID": playbookRunIDs}) - - err = s.store.selectBuilder(tx, &statusPosts, postInfoSelect) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrap(err, "failed to get playbook run status posts") - } - - timelineEvents, err := s.getTimelineEventsForPlaybookRun(tx, playbookRunIDs) - if err != nil { - return nil, err - } - - metricsData, err := s.getMetricsForPlaybookRun(tx, playbookRunIDs) - if err != nil { - return nil, err - } - - if err = tx.Commit(); err != nil { - return nil, errors.Wrap(err, "could not commit transaction") - } - - addStatusPostsToPlaybookRuns(statusPosts, playbookRuns) - addTimelineEventsToPlaybookRuns(timelineEvents, playbookRuns) - addMetricsToPlaybookRuns(metricsData, playbookRuns) - - return &app.GetPlaybookRunsResults{ - TotalCount: total, - PageCount: pageCount, - PerPage: options.PerPage, - HasMore: hasMore, - Items: playbookRuns, - }, nil -} - -// CreatePlaybookRun creates a new playbook run. If playbook run has an ID, that ID will be used. -func (s *playbookRunStore) CreatePlaybookRun(playbookRun *app.PlaybookRun) (*app.PlaybookRun, error) { - if playbookRun == nil { - return nil, errors.New("playbook run is nil") - } - - playbookRun = playbookRun.Clone() - - if playbookRun.ID == "" { - playbookRun.ID = model.NewId() - } - - playbookRun.Checklists = populateChecklistIDs(playbookRun.Checklists) - - rawPlaybookRun, err := toSQLPlaybookRun(*playbookRun) - if err != nil { - return nil, err - } - - if rawPlaybookRun.Type != app.RunTypeChannelChecklist && rawPlaybookRun.Type != app.RunTypePlaybook { - rawPlaybookRun.Type = app.RunTypePlaybook - } - - // When adding a PlaybookRun column #2: add to the SetMap - _, err = s.store.execBuilder(s.store.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": rawPlaybookRun.ID, - "Name": rawPlaybookRun.Name, - "Description": rawPlaybookRun.Summary, - "SummaryModifiedAt": rawPlaybookRun.SummaryModifiedAt, - "CommanderUserID": rawPlaybookRun.OwnerUserID, - "ReporterUserID": rawPlaybookRun.ReporterUserID, - "TeamID": rawPlaybookRun.TeamID, - "ChannelID": rawPlaybookRun.ChannelID, - "CreateAt": rawPlaybookRun.CreateAt, - "EndAt": rawPlaybookRun.EndAt, - "PostID": rawPlaybookRun.PostID, - "PlaybookID": rawPlaybookRun.PlaybookID, - "ChecklistsJSON": rawPlaybookRun.ChecklistsJSON, - "ReminderPostID": rawPlaybookRun.ReminderPostID, - "PreviousReminder": rawPlaybookRun.PreviousReminder, - "ReminderMessageTemplate": rawPlaybookRun.ReminderMessageTemplate, - "StatusUpdateEnabled": rawPlaybookRun.StatusUpdateEnabled, - "ReminderTimerDefaultSeconds": rawPlaybookRun.ReminderTimerDefaultSeconds, - "CurrentStatus": rawPlaybookRun.CurrentStatus, - "LastStatusUpdateAt": rawPlaybookRun.LastStatusUpdateAt, - "ConcatenatedInvitedUserIDs": rawPlaybookRun.ConcatenatedInvitedUserIDs, - "ConcatenatedInvitedGroupIDs": rawPlaybookRun.ConcatenatedInvitedGroupIDs, - "DefaultCommanderID": rawPlaybookRun.DefaultOwnerID, - "ConcatenatedBroadcastChannelIDs": rawPlaybookRun.ConcatenatedBroadcastChannelIDs, - "ConcatenatedWebhookOnCreationURLs": rawPlaybookRun.ConcatenatedWebhookOnCreationURLs, - "Retrospective": rawPlaybookRun.Retrospective, - "RetrospectivePublishedAt": rawPlaybookRun.RetrospectivePublishedAt, - "RetrospectiveEnabled": rawPlaybookRun.RetrospectiveEnabled, - "MessageOnJoin": rawPlaybookRun.MessageOnJoin, - "RetrospectiveReminderIntervalSeconds": rawPlaybookRun.RetrospectiveReminderIntervalSeconds, - "RetrospectiveWasCanceled": rawPlaybookRun.RetrospectiveWasCanceled, - "ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs, - "CategoryName": rawPlaybookRun.CategoryName, - "StatusUpdateBroadcastChannelsEnabled": rawPlaybookRun.StatusUpdateBroadcastChannelsEnabled, - "StatusUpdateBroadcastWebhooksEnabled": rawPlaybookRun.StatusUpdateBroadcastWebhooksEnabled, - "CreateChannelMemberOnNewParticipant": rawPlaybookRun.CreateChannelMemberOnNewParticipant, - "RemoveChannelMemberOnRemovedParticipant": rawPlaybookRun.RemoveChannelMemberOnRemovedParticipant, - "RunType": rawPlaybookRun.Type, - // Preserved for backwards compatibility with v1.2 - "ActiveStage": 0, - "ActiveStageTitle": "", - "IsActive": true, - "DeleteAt": 0, - })) - - if err != nil { - return nil, errors.Wrapf(err, "failed to store new playbook run") - } - - return playbookRun, nil -} - -// UpdatePlaybookRun updates a playbook run. -func (s *playbookRunStore) UpdatePlaybookRun(playbookRun *app.PlaybookRun) (*app.PlaybookRun, error) { - if playbookRun == nil { - return nil, errors.New("playbook run is nil") - } - if playbookRun.ID == "" { - return nil, errors.New("ID should not be empty") - } - - playbookRun = playbookRun.Clone() - playbookRun.Checklists = populateChecklistIDs(playbookRun.Checklists) - - rawPlaybookRun, err := toSQLPlaybookRun(*playbookRun) - if err != nil { - return nil, err - } - tx, err := s.store.db.Beginx() - if err != nil { - return nil, errors.Wrap(err, "could not begin transaction") - } - defer s.store.finalizeTransaction(tx) - - // When adding a PlaybookRun column #3: add to this SetMap (if it is a column that can be updated) - _, err = s.store.execBuilder(tx, sq. - Update("IR_Incident"). - SetMap(map[string]interface{}{ - "Name": rawPlaybookRun.Name, - "Description": rawPlaybookRun.Summary, - "SummaryModifiedAt": rawPlaybookRun.SummaryModifiedAt, - "CommanderUserID": rawPlaybookRun.OwnerUserID, - "LastStatusUpdateAt": rawPlaybookRun.LastStatusUpdateAt, - "ChecklistsJSON": rawPlaybookRun.ChecklistsJSON, - "ReminderPostID": rawPlaybookRun.ReminderPostID, - "PreviousReminder": rawPlaybookRun.PreviousReminder, - "ConcatenatedInvitedUserIDs": rawPlaybookRun.ConcatenatedInvitedUserIDs, - "ConcatenatedInvitedGroupIDs": rawPlaybookRun.ConcatenatedInvitedGroupIDs, - "DefaultCommanderID": rawPlaybookRun.DefaultOwnerID, - "ConcatenatedBroadcastChannelIDs": rawPlaybookRun.ConcatenatedBroadcastChannelIDs, - "ConcatenatedWebhookOnCreationURLs": rawPlaybookRun.ConcatenatedWebhookOnCreationURLs, - "Retrospective": rawPlaybookRun.Retrospective, - "RetrospectivePublishedAt": rawPlaybookRun.RetrospectivePublishedAt, - "MessageOnJoin": rawPlaybookRun.MessageOnJoin, - "RetrospectiveReminderIntervalSeconds": rawPlaybookRun.RetrospectiveReminderIntervalSeconds, - "RetrospectiveWasCanceled": rawPlaybookRun.RetrospectiveWasCanceled, - "ConcatenatedWebhookOnStatusUpdateURLs": rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs, - "StatusUpdateBroadcastChannelsEnabled": rawPlaybookRun.StatusUpdateBroadcastChannelsEnabled, - "StatusUpdateBroadcastWebhooksEnabled": rawPlaybookRun.StatusUpdateBroadcastWebhooksEnabled, - "StatusUpdateEnabled": rawPlaybookRun.StatusUpdateEnabled, - "CreateChannelMemberOnNewParticipant": rawPlaybookRun.CreateChannelMemberOnNewParticipant, - "RemoveChannelMemberOnRemovedParticipant": rawPlaybookRun.RemoveChannelMemberOnRemovedParticipant, - "RunType": rawPlaybookRun.Type, - }). - Where(sq.Eq{"ID": rawPlaybookRun.ID})) - - if err != nil { - return nil, errors.Wrapf(err, "failed to update playbook run with id '%s'", rawPlaybookRun.ID) - } - - if err = s.updateRunMetrics(tx, rawPlaybookRun.PlaybookRun); err != nil { - return nil, errors.Wrapf(err, "failed to update playbook run metrics for run with id '%s'", rawPlaybookRun.PlaybookRun.ID) - } - - if err = tx.Commit(); err != nil { - return nil, errors.Wrap(err, "could not commit transaction") - } - - return playbookRun, nil -} - -func (s *playbookRunStore) UpdateStatus(statusPost *app.SQLStatusPost) error { - if statusPost == nil { - return errors.New("status post is nil") - } - if statusPost.PlaybookRunID == "" { - return errors.New("needs playbook run ID") - } - if statusPost.PostID == "" { - return errors.New("needs post ID") - } - - if _, err := s.store.execBuilder(s.store.db, sq. - Insert("IR_StatusPosts"). - SetMap(map[string]interface{}{ - "IncidentID": statusPost.PlaybookRunID, - "PostID": statusPost.PostID, - })); err != nil { - return errors.Wrap(err, "failed to add new status post") - } - - return nil -} - -func (s *playbookRunStore) FinishPlaybookRun(playbookRunID string, endAt int64) error { - if _, err := s.store.execBuilder(s.store.db, sq. - Update("IR_Incident"). - SetMap(map[string]interface{}{ - "CurrentStatus": app.StatusFinished, - "EndAt": endAt, - }). - Where(sq.Eq{"ID": playbookRunID}), - ); err != nil { - return errors.Wrapf(err, "failed to finish run for id '%s'", playbookRunID) - } - - return nil -} - -func (s *playbookRunStore) RestorePlaybookRun(playbookRunID string, restoredAt int64) error { - if _, err := s.store.execBuilder(s.store.db, sq. - Update("IR_Incident"). - SetMap(map[string]interface{}{ - "CurrentStatus": app.StatusInProgress, - "EndAt": 0, - "LastStatusUpdateAt": restoredAt, - }). - Where(sq.Eq{"ID": playbookRunID})); err != nil { - return errors.Wrapf(err, "failed to restore run for id '%s'", playbookRunID) - } - - return nil -} - -// CreateTimelineEvent creates the timeline event -func (s *playbookRunStore) CreateTimelineEvent(event *app.TimelineEvent) (*app.TimelineEvent, error) { - if event.PlaybookRunID == "" { - return nil, errors.New("needs playbook run ID") - } - if event.EventType == "" { - return nil, errors.New("needs event type") - } - if event.CreateAt == 0 { - event.CreateAt = model.GetMillis() - } - event.ID = model.NewId() - - eventType := string(event.EventType) - if event.EventType == app.OwnerChanged { - eventType = legacyEventTypeCommanderChanged - } - - _, err := s.store.execBuilder(s.store.db, sq. - Insert("IR_TimelineEvent"). - SetMap(map[string]interface{}{ - "ID": event.ID, - "IncidentID": event.PlaybookRunID, - "CreateAt": event.CreateAt, - "DeleteAt": event.DeleteAt, - "EventAt": event.EventAt, - "EventType": eventType, - "Summary": event.Summary, - "Details": event.Details, - "PostID": event.PostID, - "SubjectUserID": event.SubjectUserID, - "CreatorUserID": event.CreatorUserID, - })) - - if err != nil { - return nil, errors.Wrap(err, "failed to insert timeline event") - } - - return event, nil -} - -// UpdateTimelineEvent updates (or inserts) the timeline event -func (s *playbookRunStore) UpdateTimelineEvent(event *app.TimelineEvent) error { - if event.ID == "" { - return errors.New("needs event ID") - } - if event.PlaybookRunID == "" { - return errors.New("needs playbook run ID") - } - if event.EventType == "" { - return errors.New("needs event type") - } - - eventType := string(event.EventType) - if event.EventType == app.OwnerChanged { - eventType = legacyEventTypeCommanderChanged - } - - _, err := s.store.execBuilder(s.store.db, sq. - Update("IR_TimelineEvent"). - SetMap(map[string]interface{}{ - "IncidentID": event.PlaybookRunID, - "CreateAt": event.CreateAt, - "DeleteAt": event.DeleteAt, - "EventAt": event.EventAt, - "EventType": eventType, - "Summary": event.Summary, - "Details": event.Details, - "PostID": event.PostID, - "SubjectUserID": event.SubjectUserID, - "CreatorUserID": event.CreatorUserID, - }). - Where(sq.Eq{"ID": event.ID})) - - if err != nil { - return errors.Wrap(err, "failed to update timeline event") - } - - return nil -} - -// GetPlaybookRun gets a playbook run by ID. -func (s *playbookRunStore) GetPlaybookRun(playbookRunID string) (*app.PlaybookRun, error) { - if playbookRunID == "" { - return nil, errors.New("ID cannot be empty") - } - - tx, err := s.store.db.Beginx() - if err != nil { - return nil, errors.Wrap(err, "could not begin transaction") - } - defer s.store.finalizeTransaction(tx) - - var rawPlaybookRun sqlPlaybookRun - err = s.store.getBuilder(tx, &rawPlaybookRun, s.playbookRunSelect.Where(sq.Eq{"i.ID": playbookRunID})) - if err == sql.ErrNoRows { - return nil, errors.Wrapf(app.ErrNotFound, "playbook run with id '%s' does not exist", playbookRunID) - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get playbook run by id '%s'", playbookRunID) - } - - playbookRun, err := s.toPlaybookRun(rawPlaybookRun) - if err != nil { - return nil, err - } - - var statusPosts playbookRunStatusPosts - - postInfoSelect := s.statusPostsSelect. - Where(sq.Eq{"sp.IncidentID": playbookRunID}). - OrderBy("p.CreateAt") - - err = s.store.selectBuilder(tx, &statusPosts, postInfoSelect) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrapf(err, "failed to get playbook run status posts for playbook run with id '%s'", playbookRunID) - } - - timelineEvents, err := s.getTimelineEventsForPlaybookRun(tx, []string{playbookRunID}) - if err != nil { - return nil, err - } - - var metricsData []app.RunMetricData - - err = s.store.selectBuilder(tx, &metricsData, s.metricsDataSelectSingleRun. - Where(sq.Eq{"IncidentID": playbookRunID}). - OrderBy("MetricConfigID")) // Entirely for consistency for the tests) - - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrapf(err, "failed to get metrics data for run with id `%s`", playbookRunID) - } - - if err = tx.Commit(); err != nil { - return nil, errors.Wrap(err, "could not commit transaction") - } - - for _, p := range statusPosts { - playbookRun.StatusPosts = append(playbookRun.StatusPosts, p.StatusPost) - } - - playbookRun.TimelineEvents = append(playbookRun.TimelineEvents, timelineEvents...) - playbookRun.MetricsData = metricsData - - return playbookRun, nil -} - -func (s *playbookRunStore) getTimelineEventsForPlaybookRun(q sqlx.Queryer, playbookRunIDs []string) ([]app.TimelineEvent, error) { - var timelineEvents []app.TimelineEvent - - timelineEventsSelect := s.timelineEventsSelect. - OrderBy("te.EventAt ASC"). - Where(sq.And{sq.Eq{"te.IncidentID": playbookRunIDs}, sq.Eq{"te.DeleteAt": 0}}) - - err := s.store.selectBuilder(q, &timelineEvents, timelineEventsSelect) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrap(err, "failed to get timelineEvents") - } - - return timelineEvents, nil -} - -func (s *playbookRunStore) getMetricsForPlaybookRun(q sqlx.Queryer, playbookRunIDs []string) ([]sqlRunMetricData, error) { - var metricsData []sqlRunMetricData - - sqlMetricsDataSelect := s.sqlMetricsDataSelectMultipleRuns. - Where(sq.Eq{"IncidentID": playbookRunIDs}) - - err := s.store.selectBuilder(q, &metricsData, sqlMetricsDataSelect) - if err != nil && err != sql.ErrNoRows { - return nil, errors.Wrap(err, "failed to get metricsData") - } - - return metricsData, nil -} - -// GetTimelineEvent returns the timeline event by id for the given playbook run. -func (s *playbookRunStore) GetTimelineEvent(playbookRunID, eventID string) (*app.TimelineEvent, error) { - var event app.TimelineEvent - - timelineEventSelect := s.timelineEventsSelect. - Where(sq.And{sq.Eq{"te.IncidentID": playbookRunID}, sq.Eq{"te.ID": eventID}}) - - err := s.store.getBuilder(s.store.db, &event, timelineEventSelect) - if err == sql.ErrNoRows { - return nil, errors.Wrapf(app.ErrNotFound, "timeline event with id (%s) does not exist for playbook run with id (%s)", eventID, playbookRunID) - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get timeline event with id (%s) for playbook run with id (%s)", eventID, playbookRunID) - } - - return &event, nil -} - -// GetPlaybookRunIDsForChannel gets the playbook run IDs list associated with the given channel ID. -func (s *playbookRunStore) GetPlaybookRunIDsForChannel(channelID string) ([]string, error) { - query := s.queryBuilder. - Select("i.ID"). - From("IR_Incident i"). - Where(sq.Eq{"i.ChannelID": channelID}). - Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}). - OrderBy("i.CreateAt DESC"). - OrderBy("i.ID") - - var ids []string - err := s.store.selectBuilder(s.store.db, &ids, query) - if err == sql.ErrNoRows || len(ids) == 0 { - return nil, errors.Wrapf(app.ErrNotFound, "channel with id (%s) does not have a playbook run", channelID) - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get playbook run by channelID '%s'", channelID) - } - - return ids, nil -} - -// GetHistoricalPlaybookRunParticipantsCount returns the count of all members of a playbook run's channel -// since the beginning of the playbook run, excluding bots. -func (s *playbookRunStore) GetHistoricalPlaybookRunParticipantsCount(channelID string) (int64, error) { - query := s.queryBuilder. - Select("COUNT(DISTINCT cmh.UserId)"). - From("ChannelMemberHistory AS cmh"). - Where(sq.Eq{"cmh.ChannelId": channelID}). - Where(sq.Expr("cmh.UserId NOT IN (SELECT UserId FROM Bots)")) - - var numParticipants int64 - err := s.store.getBuilder(s.store.db, &numParticipants, query) - if err != nil { - return 0, errors.Wrap(err, "failed to query database") - } - - return numParticipants, nil -} - -// GetOwners returns the owners of the playbook runs selected by options -func (s *playbookRunStore) GetOwners(requesterInfo app.RequesterInfo, options app.PlaybookRunFilterOptions) ([]app.OwnerInfo, error) { - permissionsExpr := s.buildPermissionsExpr(requesterInfo) - teamLimitExpr := buildTeamLimitExpr(requesterInfo, options.TeamID, "i") - - // At the moment, the options only includes teamID - query := s.queryBuilder. - Select("DISTINCT u.Id AS UserID", "u.Username", "u.FirstName", "u.LastName", "u.Nickname"). - From("IR_Incident AS i"). - Join("Users AS u ON i.CommanderUserID = u.Id"). - Where(teamLimitExpr). - Where(permissionsExpr) - - var owners []app.OwnerInfo - err := s.store.selectBuilder(s.store.db, &owners, query) - if err != nil { - return nil, errors.Wrap(err, "failed to query database") - } - - return owners, nil -} - -// NukeDB removes all playbook run related data. -func (s *playbookRunStore) NukeDB() (err error) { - tx, err := s.store.db.Beginx() - if err != nil { - return errors.Wrap(err, "could not begin transaction") - } - defer s.store.finalizeTransaction(tx) - - if _, err := tx.Exec("DROP TABLE IF EXISTS IR_Metric, IR_MetricConfig, IR_PlaybookMember, IR_Run_Participants, IR_PlaybookAutoFollow, IR_StatusPosts, IR_TimelineEvent, IR_Incident, IR_Playbook, IR_System"); err != nil { - return errors.Wrap(err, "could not delete all IR tables") - } - - if err := tx.Commit(); err != nil { - return errors.Wrap(err, "could not commit") - } - - return s.store.RunMigrations() -} - -func (s *playbookRunStore) ChangeCreationDate(playbookRunID string, creationTimestamp time.Time) error { - updateQuery := s.queryBuilder.Update("IR_Incident"). - Where(sq.Eq{"ID": playbookRunID}). - Set("CreateAt", model.GetMillisForTime(creationTimestamp)) - - sqlResult, err := s.store.execBuilder(s.store.db, updateQuery) - if err != nil { - return errors.Wrapf(err, "unable to execute the update query") - } - - numRows, err := sqlResult.RowsAffected() - if err != nil { - return errors.Wrapf(err, "unable to check how many rows were updated") - } - - if numRows == 0 { - return app.ErrNotFound - } - - return nil -} - -func (s *playbookRunStore) GetBroadcastChannelIDsToRootIDs(playbookRunID string) (map[string]string, error) { - var retAsJSON string - query := s.store.builder.Select("COALESCE(ChannelIDToRootID, '')"). - From("IR_Incident"). - Where(sq.Eq{"ID": playbookRunID}) - - err := s.store.getBuilder(s.store.db, &retAsJSON, query) - if err == sql.ErrNoRows { - return nil, errors.Wrapf(app.ErrNotFound, "could not find playbook with id '%s'", playbookRunID) - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get channelID to rootID map for playbookRunID '%s'", playbookRunID) - } - - ret := make(map[string]string) - if retAsJSON == "" { - return ret, nil - } - - if err := json.Unmarshal([]byte(retAsJSON), &ret); err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal channelID to rootID map for playbookRunID: '%s'", playbookRunID) - } - - return ret, nil -} - -func (s *playbookRunStore) SetBroadcastChannelIDsToRootID(playbookRunID string, channelIDsToRootIDs map[string]string) error { - data, err := json.Marshal(channelIDsToRootIDs) - if err != nil { - return errors.Wrap(err, "failed to marshal channelIDsToRootIDs map") - } - - _, err = s.store.execBuilder(s.store.db, - sq.Update("IR_Incident"). - Set("ChannelIDToRootID", data). - Where(sq.Eq{"ID": playbookRunID})) - if err != nil { - return errors.Wrapf(err, "failed to set ChannelIDsToRootID column for playbookRunID '%s'", playbookRunID) - } - - return nil -} - -func (s *playbookRunStore) buildPermissionsExpr(info app.RequesterInfo) sq.Sqlizer { - if info.IsAdmin { - return nil - } - - // Guests must be participants - if info.IsGuest { - return sq.Expr(` - EXISTS(SELECT 1 - FROM IR_Run_Participants as rp - WHERE rp.IncidentID = i.ID - AND rp.UserId = ? - AND rp.IsParticipant = true - ) - `, info.UserID) - } - - // 1. Is the user a participant of the run? - // 2. Is the playbook open to everyone on the team, or is the user a member of the playbook? - // If so, they have permission to view the run. - return sq.Expr(` - (( - EXISTS ( - SELECT 1 - FROM IR_Run_Participants as rp - WHERE rp.IncidentID = i.ID - AND rp.UserId = ? - AND rp.IsParticipant = true - ) - ) OR ( - (SELECT Public - FROM IR_Playbook - WHERE ID = i.PlaybookID) - OR EXISTS( - SELECT 1 - FROM IR_PlaybookMember - WHERE PlaybookID = i.PlaybookID - AND MemberID = ?) - ))`, info.UserID, info.UserID) -} - -func buildTeamLimitExpr(info app.RequesterInfo, teamID, tableName string) sq.Sqlizer { - filterToSelectedTeam := sq.Eq{fmt.Sprintf("%s.TeamID", tableName): teamID} - onlyTeamsUserIsAMember := sq.Expr(fmt.Sprintf(` - EXISTS(SELECT 1 - FROM TeamMembers as tm - WHERE tm.TeamId = %s.TeamID - AND tm.DeleteAt = 0 - AND tm.UserId = ?) - `, tableName), info.UserID) - - if info.IsAdmin { - if teamID != "" { - return filterToSelectedTeam - } - return nil - } - - if teamID != "" { - return sq.And{ - filterToSelectedTeam, - onlyTeamsUserIsAMember, - } - } - - return onlyTeamsUserIsAMember - -} - -func (s *playbookRunStore) toPlaybookRun(rawPlaybookRun sqlPlaybookRun) (*app.PlaybookRun, error) { - playbookRun := rawPlaybookRun.PlaybookRun - if err := json.Unmarshal(rawPlaybookRun.ChecklistsJSON, &playbookRun.Checklists); err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: %s", rawPlaybookRun.ID) - } - - playbookRun.InvitedUserIDs = []string(nil) - if rawPlaybookRun.ConcatenatedInvitedUserIDs != "" { - playbookRun.InvitedUserIDs = strings.Split(rawPlaybookRun.ConcatenatedInvitedUserIDs, ",") - } - - playbookRun.InvitedGroupIDs = []string(nil) - if rawPlaybookRun.ConcatenatedInvitedGroupIDs != "" { - playbookRun.InvitedGroupIDs = strings.Split(rawPlaybookRun.ConcatenatedInvitedGroupIDs, ",") - } - - playbookRun.ParticipantIDs = []string(nil) - if rawPlaybookRun.ConcatenatedParticipantIDs != "" { - playbookRun.ParticipantIDs = strings.Split(rawPlaybookRun.ConcatenatedParticipantIDs, ",") - } - - playbookRun.BroadcastChannelIDs = []string(nil) - if rawPlaybookRun.ConcatenatedBroadcastChannelIDs != "" { - playbookRun.BroadcastChannelIDs = strings.Split(rawPlaybookRun.ConcatenatedBroadcastChannelIDs, ",") - } - - playbookRun.WebhookOnCreationURLs = []string(nil) - if rawPlaybookRun.ConcatenatedWebhookOnCreationURLs != "" { - playbookRun.WebhookOnCreationURLs = strings.Split(rawPlaybookRun.ConcatenatedWebhookOnCreationURLs, ",") - } - - playbookRun.WebhookOnStatusUpdateURLs = []string(nil) - if rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs != "" { - playbookRun.WebhookOnStatusUpdateURLs = strings.Split(rawPlaybookRun.ConcatenatedWebhookOnStatusUpdateURLs, ",") - } - - // force false broadcast-on-status-update flags if they have no destinations - if len(playbookRun.WebhookOnStatusUpdateURLs) == 0 { - playbookRun.StatusUpdateBroadcastWebhooksEnabled = false - } - if len(playbookRun.BroadcastChannelIDs) == 0 { - playbookRun.StatusUpdateBroadcastChannelsEnabled = false - } - - return &playbookRun, nil -} - -// GetRunsWithAssignedTasks returns the list of runs that have tasks assigned to userID -func (s *playbookRunStore) GetRunsWithAssignedTasks(userID string) ([]app.AssignedRun, error) { - var raw []struct { - app.AssignedRun - ChecklistsJSON json.RawMessage - } - - query := s.store.builder.Select("i.ID AS PlaybookRunID", "i.Name", "i.ChecklistsJSON AS ChecklistsJSON"). - From("IR_Incident AS i"). - Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}). - OrderBy("i.Name") - - if s.store.db.DriverName() == model.DatabaseDriverMysql { - query = query.Where(sq.Like{"i.ChecklistsJSON": fmt.Sprintf("%%\"%s\"%%", userID)}) - } else { - query = query.Where(sq.Like{"i.ChecklistsJSON::text": fmt.Sprintf("%%\"%s\"%%", userID)}) - } - - if err := s.store.selectBuilder(s.store.db, &raw, query); err != nil { - return nil, errors.Wrap(err, "failed to query for assigned tasks") - } - - var ret []app.AssignedRun - for _, rawItem := range raw { - run := rawItem.AssignedRun - - var checklists []app.Checklist - err := json.Unmarshal(rawItem.ChecklistsJSON, &checklists) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: %s", rawItem.PlaybookRunID) - } - - // Check which item(s) have this user as an assignee and add them to the list - for _, checklist := range checklists { - for _, item := range checklist.Items { - if item.AssigneeID == userID && item.State == "" { - task := app.AssignedTask{ - ChecklistID: checklist.ID, - ChecklistTitle: checklist.Title, - ChecklistItem: item, - } - run.Tasks = append(run.Tasks, task) - } - } - } - - if len(run.Tasks) > 0 { - ret = append(ret, run) - } - } - - return ret, nil -} - -// GetParticipatingRuns returns the list of active runs with userID as a participant -func (s *playbookRunStore) GetParticipatingRuns(userID string) ([]app.RunLink, error) { - membershipClause := s.queryBuilder. - Select("1"). - Prefix("EXISTS("). - From("IR_Run_Participants AS rp"). - Where("rp.IncidentID = i.ID"). - Where(sq.Eq{"rp.UserId": userID}). - Where(sq.Eq{"rp.IsParticipant": true}). - Suffix(")") - - query := s.store.builder. - Select("i.ID AS PlaybookRunID", "i.Name"). - From("IR_Incident AS i"). - Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}). - Where(membershipClause). - OrderBy("i.Name") - - var ret []app.RunLink - if err := s.store.selectBuilder(s.store.db, &ret, query); err != nil { - return nil, errors.Wrap(err, "failed to query for active runs") - } - - return ret, nil -} - -// GetOverdueUpdateRuns returns runs owned by userID and that have overdue status updates. -func (s *playbookRunStore) GetOverdueUpdateRuns(userID string) ([]app.RunLink, error) { - // only notify if the user is still a participant - // in other words: don't notify the commander of an overdue run if they have left the run - membershipClause := s.queryBuilder. - Select("1"). - Prefix("EXISTS("). - From("IR_Run_Participants AS rp"). - Where("rp.IncidentID = i.ID"). - Where(sq.Eq{"rp.UserId": userID}). - Where(sq.Eq{"rp.IsParticipant": true}). - Suffix(")") - - query := s.store.builder. - Select("i.ID AS PlaybookRunID", "i.Name"). - From("IR_Incident AS i"). - Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}). - Where(sq.NotEq{"i.PreviousReminder": 0}). - Where(sq.Eq{"i.CommanderUserId": userID}). - Where(sq.Eq{"i.StatusUpdateEnabled": true}). - Where(membershipClause). - OrderBy("i.Name") - - if s.store.db.DriverName() == model.DatabaseDriverMysql { - query = query.Where(sq.Expr("(i.PreviousReminder / 1e6 + i.LastStatusUpdateAt) <= FLOOR(UNIX_TIMESTAMP() * 1000)")) - } else { - query = query.Where(sq.Expr("(i.PreviousReminder / 1e6 + i.LastStatusUpdateAt) <= FLOOR(EXTRACT (EPOCH FROM now())::float*1000)")) - } - - var ret []app.RunLink - if err := s.store.selectBuilder(s.store.db, &ret, query); err != nil { - return nil, errors.Wrap(err, "failed to query for active runs") - } - - return ret, nil -} - -func (s *playbookRunStore) Follow(playbookRunID, userID string) error { - return s.updateFollowing(playbookRunID, userID, true) -} - -func (s *playbookRunStore) Unfollow(playbookRunID, userID string) error { - return s.updateFollowing(playbookRunID, userID, false) -} - -func (s *playbookRunStore) updateFollowing(playbookRunID, userID string, isFollowing bool) error { - var err error - if s.store.db.DriverName() == model.DatabaseDriverMysql { - _, err = s.store.execBuilder(s.store.db, sq. - Insert("IR_Run_Participants"). - Columns("IncidentID", "UserID", "IsFollower"). - Values(playbookRunID, userID, isFollowing). - Suffix("ON DUPLICATE KEY UPDATE IsFollower = ?", isFollowing)) - } else { - _, err = s.store.execBuilder(s.store.db, sq. - Insert("IR_Run_Participants"). - Columns("IncidentID", "UserID", "IsFollower"). - Values(playbookRunID, userID, isFollowing). - Suffix("ON CONFLICT (IncidentID,UserID) DO UPDATE SET IsFollower = ?", isFollowing)) - } - - if err != nil { - return errors.Wrapf(err, "failed to upsert follower '%s' for run '%s'", userID, playbookRunID) - } - - return nil -} - -func (s *playbookRunStore) GetFollowers(playbookRunID string) ([]string, error) { - query := s.queryBuilder. - Select("UserID"). - From("IR_Run_Participants"). - Where(sq.And{sq.Eq{"IsFollower": true}, sq.Eq{"IncidentID": playbookRunID}}) - - var followers []string - err := s.store.selectBuilder(s.store.db, &followers, query) - if err == sql.ErrNoRows { - return []string{}, nil - } else if err != nil { - return nil, errors.Wrapf(err, "failed to get followers for run '%s'", playbookRunID) - } - - return followers, nil -} - -// Get number of active runs. -func (s *playbookRunStore) GetRunsActiveTotal() (int64, error) { - var count int64 - - query := s.store.builder. - Select("COUNT(*)"). - From("IR_Incident"). - Where(sq.Eq{"CurrentStatus": app.StatusInProgress}) - - if err := s.store.getBuilder(s.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count active runs'") - } - - return count, nil -} - -// GetOverdueUpdateRunsTotal returns number of runs that have overdue status updates. -func (s *playbookRunStore) GetOverdueUpdateRunsTotal() (int64, error) { - query := s.store.builder. - Select("COUNT(*)"). - From("IR_Incident"). - Where(sq.Eq{"CurrentStatus": app.StatusInProgress}). - Where(sq.Eq{"StatusUpdateEnabled": true}). - Where(sq.NotEq{"PreviousReminder": 0}) - - if s.store.db.DriverName() == model.DatabaseDriverMysql { - query = query.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(UNIX_TIMESTAMP() * 1000)")) - } else { - query = query.Where(sq.Expr("(PreviousReminder / 1e6 + LastStatusUpdateAt) <= FLOOR(EXTRACT (EPOCH FROM now())::float*1000)")) - } - - var count int64 - if err := s.store.getBuilder(s.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count active runs that have overdue status updates") - } - - return count, nil -} - -// GetOverdueRetroRunsTotal returns the number of completed runs without retro and with reminder -func (s *playbookRunStore) GetOverdueRetroRunsTotal() (int64, error) { - query := s.store.builder. - Select("COUNT(*)"). - From("IR_Incident"). - Where(sq.Eq{"CurrentStatus": app.StatusFinished}). - Where(sq.Eq{"RetrospectiveEnabled": true}). - Where(sq.Eq{"RetrospectivePublishedAt": 0}). - Where(sq.NotEq{"RetrospectiveReminderIntervalSeconds": 0}) - - var count int64 - if err := s.store.getBuilder(s.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count finished runs without retro") - } - - return count, nil -} - -// GetFollowersActiveTotal returns total number of active followers, including duplicates -// if a user is following more than one run, it will be counted multiple times -func (s *playbookRunStore) GetFollowersActiveTotal() (int64, error) { - var count int64 - - query := s.store.builder. - Select("COUNT(*)"). - From("IR_Run_Participants as rp"). - Join("IR_Incident AS i ON (i.ID = rp.IncidentID)"). - Where(sq.Eq{"rp.IsFollower": true}). - Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}) - - if err := s.store.getBuilder(s.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count active followers'") - } - - return count, nil -} - -// GetParticipantsActiveTotal returns number of active participants -// if a user is a participant in more than one run they will be counted multiple times -func (s *playbookRunStore) GetParticipantsActiveTotal() (int64, error) { - var count int64 - - query := s.store.builder. - Select("COUNT(*)"). - From("IR_Run_Participants as rp"). - Join("IR_Incident AS i ON i.ID = rp.IncidentID"). - Where(sq.Eq{"i.CurrentStatus": app.StatusInProgress}). - Where(sq.Eq{"rp.IsParticipant": true}). - Where(sq.Expr("rp.UserId NOT IN (SELECT UserId FROM Bots)")) - - if err := s.store.getBuilder(s.store.db, &count, query); err != nil { - return 0, errors.Wrap(err, "failed to count active participants") - } - - return count, nil -} - -// GetSchemeRolesForChannel scheme role ids for the channel -func (s *playbookRunStore) GetSchemeRolesForChannel(channelID string) (string, string, string, error) { - query := s.queryBuilder. - Select("COALESCE(s.DefaultChannelGuestRole, 'channel_guest') DefaultChannelGuestRole", - "COALESCE(s.DefaultChannelUserRole, 'channel_user') DefaultChannelUserRole", - "COALESCE(s.DefaultChannelAdminRole, 'channel_admin') DefaultChannelAdminRole"). - From("Schemes as s"). - Join("Channels AS c ON (c.SchemeId = s.Id)"). - Where(sq.Eq{"c.Id": channelID}) - - var scheme model.Scheme - err := s.store.getBuilder(s.store.db, &scheme, query) - - return scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, err -} - -// GetSchemeRolesForTeam scheme role ids for the team -func (s *playbookRunStore) GetSchemeRolesForTeam(teamID string) (string, string, string, error) { - query := s.queryBuilder. - Select("COALESCE(s.DefaultChannelGuestRole, 'channel_guest') DefaultChannelGuestRole", - "COALESCE(s.DefaultChannelUserRole, 'channel_user') DefaultChannelUserRole", - "COALESCE(s.DefaultChannelAdminRole, 'channel_admin') DefaultChannelAdminRole"). - From("Schemes as s"). - Join("Teams AS t ON (t.SchemeId = s.Id)"). - Where(sq.Eq{"t.Id": teamID}) - - var scheme model.Scheme - err := s.store.getBuilder(s.store.db, &scheme, query) - - return scheme.DefaultChannelGuestRole, scheme.DefaultChannelUserRole, scheme.DefaultChannelAdminRole, err -} - -// updateRunMetrics updates run metrics values. -func (s *playbookRunStore) updateRunMetrics(q queryExecer, playbookRun app.PlaybookRun) error { - if len(playbookRun.MetricsData) == 0 { - return nil - } - - //retrieve metrics configurations ids for this run to validate received data - query := s.queryBuilder. - Select("ID"). - From("IR_MetricConfig"). - Where(sq.Eq{"PlaybookID": playbookRun.PlaybookID}). - Where(sq.Eq{"DeleteAt": 0}) - - var metricsConfigsIDs []string - err := s.store.selectBuilder(q, &metricsConfigsIDs, query) - if err != nil { - return errors.Wrapf(err, "failed to get metric configs ids for playbook '%s'", playbookRun.PlaybookID) - } - validIDs := make(map[string]bool) - for _, id := range metricsConfigsIDs { - validIDs[id] = true - } - - retrospectivePublished := !playbookRun.RetrospectiveWasCanceled && playbookRun.RetrospectivePublishedAt > 0 - - for _, m := range playbookRun.MetricsData { - //do not store if id is not in run's playbook configuration - if !validIDs[m.MetricConfigID] { - continue - } - if s.store.db.DriverName() == model.DatabaseDriverMysql { - _, err = s.store.execBuilder(q, sq. - Insert("IR_Metric"). - Columns("IncidentID", "MetricConfigID", "Value", "Published"). - Values(playbookRun.ID, m.MetricConfigID, m.Value, retrospectivePublished). - Suffix("ON DUPLICATE KEY UPDATE Value = ?, Published = ?", m.Value, retrospectivePublished)) - } else { - _, err = s.store.execBuilder(q, sq. - Insert("IR_Metric"). - Columns("IncidentID", "MetricConfigID", "Value", "Published"). - Values(playbookRun.ID, m.MetricConfigID, m.Value, retrospectivePublished). - Suffix("ON CONFLICT (IncidentID,MetricConfigID) DO UPDATE SET Value = ?, Published = ?", m.Value, retrospectivePublished)) - } - if err != nil { - return errors.Wrapf(err, "failed to upsert metric value '%s'", m.MetricConfigID) - } - } - return nil -} - -func (s *playbookRunStore) AddParticipants(playbookRunID string, userIDs []string) error { - return s.updateParticipating(playbookRunID, userIDs, true) -} - -func (s *playbookRunStore) RemoveParticipants(playbookRunID string, userIDs []string) error { - return s.updateParticipating(playbookRunID, userIDs, false) -} - -func (s *playbookRunStore) updateParticipating(playbookRunID string, userIDs []string, isParticipating bool) error { - if len(userIDs) == 0 { - return nil - } - - query := sq. - Insert("IR_Run_Participants"). - Columns("IncidentID", "UserID", "IsParticipant") - - for _, userID := range userIDs { - query = query.Values(playbookRunID, userID, isParticipating) - } - - var err error - if s.store.db.DriverName() == model.DatabaseDriverMysql { - _, err = s.store.execBuilder( - s.store.db, - query.Suffix("ON DUPLICATE KEY UPDATE IsParticipant = ?", isParticipating), - ) - } else { - _, err = s.store.execBuilder( - s.store.db, - query.Suffix("ON CONFLICT (IncidentID,UserID) DO UPDATE SET IsParticipant = ?", isParticipating), - ) - } - - if err != nil { - return errors.Wrapf(err, "failed to upsert participants '%+v' for run '%s'", userIDs, playbookRunID) - } - - return nil -} - -// GetPlaybookRunIDsForUser returns run ids where user is a participant or is following -func (s *playbookRunStore) GetPlaybookRunIDsForUser(userID string) ([]string, error) { - requesterInfo := app.RequesterInfo{UserID: userID} - permissionsExpr := s.buildPermissionsExpr(requesterInfo) - teamLimitExpr := buildTeamLimitExpr(requesterInfo, "", "i") - - query := s.store.builder. - Select("i.ID"). - From("IR_Incident AS i"). - Join("IR_Run_Participants AS p ON p.IncidentID = i.ID"). - Where(sq.Or{sq.Eq{"p.IsParticipant": true}, sq.Eq{"p.IsFollower": true}}). - Where(sq.Eq{"p.UserID": strings.ToLower(userID)}). - Where(teamLimitExpr). - Where(permissionsExpr) - - var ids []string - if err := s.store.selectBuilder(s.store.db, &ids, query); err != nil { - return nil, errors.Wrap(err, "failed to query for playbook runs") - } - return ids, nil -} - -// GetRunMetadataByIDs returns playbook runs metadata by passed run IDs. -func (s *playbookRunStore) GetRunMetadataByIDs(runIDs []string) ([]app.RunMetadata, error) { - var runs []app.RunMetadata - query := s.store.builder. - Select("ID", "TeamID", "Name"). - From("IR_Incident"). - Where(sq.Eq{"ID": runIDs}) - if err := s.store.selectBuilder(s.store.db, &runs, query); err != nil { - return nil, errors.Wrap(err, "failed to query playbook run by runIDs") - } - - runsMap := make(map[string]app.RunMetadata, len(runs)) - for _, run := range runs { - runsMap[run.ID] = run - } - orderedRuns := make([]app.RunMetadata, len(runIDs)) - for i, runID := range runIDs { - orderedRuns[i] = runsMap[runID] - } - return orderedRuns, nil -} - -// GetTaskAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by taskIDs -func (s *playbookRunStore) GetTaskAsTopicMetadataByIDs(taskIDs []string) ([]app.TopicMetadata, error) { - tasksMap := make(map[string]app.TopicMetadata, len(taskIDs)) - for _, taskID := range taskIDs { - var runsInDB []struct { - app.TopicMetadata - ChecklistsJSON json.RawMessage - } - query := s.store.builder. - Select("ID AS RunID", "TeamID", "ChecklistsJSON"). - From("IR_Incident") - - if s.store.db.DriverName() == model.DatabaseDriverMysql { - query = query.Where(sq.Like{"ChecklistsJSON": fmt.Sprintf("%%\"%s\"%%", taskID)}) - } else { - query = query.Where(sq.Like{"ChecklistsJSON::text": fmt.Sprintf("%%\"%s\"%%", taskID)}) - } - - if err := s.store.selectBuilder(s.store.db, &runsInDB, query); err != nil { - return nil, errors.Wrapf(err, "failed to query playbook run by taskID - %s", taskID) - } - - for _, run := range runsInDB { - var checklists []app.Checklist - err := json.Unmarshal(run.ChecklistsJSON, &checklists) - if err != nil { - return nil, errors.Wrapf(err, "failed to unmarshal checklists json for playbook run id: %s", run.RunID) - } - - if isTaskInChecklists(checklists, taskID) { - tasksMap[taskID] = app.TopicMetadata{ - ID: taskID, - RunID: run.RunID, - TeamID: run.TeamID, - } - } - } - } - tasks := make([]app.TopicMetadata, len(taskIDs)) - for i, taskID := range taskIDs { - tasks[i] = tasksMap[taskID] - } - - return tasks, nil -} - -func isTaskInChecklists(checklists []app.Checklist, taskID string) bool { - for _, checklist := range checklists { - for _, item := range checklist.Items { - if item.ID == taskID { - return true - } - } - } - return false -} - -// GetStatusAsTopicMetadataByIDs gets PlaybookRunIDs and TeamIDs from runs by statusIDs -func (s *playbookRunStore) GetStatusAsTopicMetadataByIDs(statusIDs []string) ([]app.TopicMetadata, error) { - var statuses []app.TopicMetadata - query := s.store.builder. - Select("sp.PostID AS ID", "sp.IncidentID AS RunID", "i.TeamID AS TeamID"). - From("IR_StatusPosts as sp"). - Join("IR_Incident as i ON sp.IncidentID = i.ID"). - Where(sq.Eq{"sp.PostID": statusIDs}) - if err := s.store.selectBuilder(s.store.db, &statuses, query); err != nil { - return nil, errors.Wrap(err, "failed to query playbook runs by statusIDs") - } - statusesMap := make(map[string]app.TopicMetadata, len(statuses)) - for _, status := range statuses { - statusesMap[status.ID] = status - } - orderedStatuses := make([]app.TopicMetadata, len(statusIDs)) - for i, statusID := range statusIDs { - orderedStatuses[i] = statusesMap[statusID] - } - return orderedStatuses, nil -} - -func (s *playbookRunStore) GraphqlUpdate(id string, setmap map[string]interface{}) error { - if id == "" { - return errors.New("id should not be empty") - } - - _, err := s.store.execBuilder(s.store.db, sq. - Update("IR_Incident"). - SetMap(setmap). - Where(sq.Eq{"ID": id})) - - if err != nil { - return errors.Wrapf(err, "failed to update playbook run with id '%s'", id) - } - - return nil -} - -func toSQLPlaybookRun(playbookRun app.PlaybookRun) (*sqlPlaybookRun, error) { - checklistsJSON, err := checklistsToJSON(playbookRun.Checklists) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal checklist json for playbook run id '%s'", playbookRun.ID) - } - - if len(checklistsJSON) > maxJSONLength { - return nil, errors.Wrapf(errors.New("invalid data"), "checklist json for playbook run id '%s' is too long (max %d)", playbookRun.ID, maxJSONLength) - } - - return &sqlPlaybookRun{ - PlaybookRun: playbookRun, - ChecklistsJSON: checklistsJSON, - ConcatenatedInvitedUserIDs: strings.Join(playbookRun.InvitedUserIDs, ","), - ConcatenatedInvitedGroupIDs: strings.Join(playbookRun.InvitedGroupIDs, ","), - ConcatenatedBroadcastChannelIDs: strings.Join(playbookRun.BroadcastChannelIDs, ","), - ConcatenatedWebhookOnCreationURLs: strings.Join(playbookRun.WebhookOnCreationURLs, ","), - ConcatenatedWebhookOnStatusUpdateURLs: strings.Join(playbookRun.WebhookOnStatusUpdateURLs, ","), - }, nil -} - -// populateChecklistIDs returns a cloned slice with ids entered for checklists and checklist items. -func populateChecklistIDs(checklists []app.Checklist) []app.Checklist { - if len(checklists) == 0 { - return nil - } - - newChecklists := make([]app.Checklist, len(checklists)) - for i, c := range checklists { - newChecklists[i] = c.Clone() - if newChecklists[i].ID == "" { - newChecklists[i].ID = model.NewId() - } - for j, item := range newChecklists[i].Items { - if item.ID == "" { - newChecklists[i].Items[j].ID = model.NewId() - } - } - } - - return newChecklists -} - -// A playbook run needs to assign unique ids to its checklist items -func checklistsToJSON(checklists []app.Checklist) (json.RawMessage, error) { - checklistsJSON, err := json.Marshal(checklists) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal checklist json") - } - - return checklistsJSON, nil -} - -func addStatusPostsToPlaybookRuns(statusIDs playbookRunStatusPosts, playbookRuns []app.PlaybookRun) { - iToPosts := make(map[string][]app.StatusPost) - for _, p := range statusIDs { - iToPosts[p.PlaybookRunID] = append(iToPosts[p.PlaybookRunID], p.StatusPost) - } - for i, playbookRun := range playbookRuns { - playbookRuns[i].StatusPosts = iToPosts[playbookRun.ID] - } -} - -func addTimelineEventsToPlaybookRuns(timelineEvents []app.TimelineEvent, playbookRuns []app.PlaybookRun) { - iToTe := make(map[string][]app.TimelineEvent) - for _, te := range timelineEvents { - iToTe[te.PlaybookRunID] = append(iToTe[te.PlaybookRunID], te) - } - for i, playbookRun := range playbookRuns { - playbookRuns[i].TimelineEvents = iToTe[playbookRun.ID] - } -} - -func addMetricsToPlaybookRuns(metrics []sqlRunMetricData, playbookRuns []app.PlaybookRun) { - playbookRunToMetrics := make(map[string][]app.RunMetricData) - for _, metric := range metrics { - playbookRunToMetrics[metric.IncidentID] = append(playbookRunToMetrics[metric.IncidentID], - app.RunMetricData{ - MetricConfigID: metric.MetricConfigID, - Value: metric.Value, - }) - } - - for i, run := range playbookRuns { - playbookRuns[i].MetricsData = playbookRunToMetrics[run.ID] - } -} - -// queryActiveBetweenTimes will modify the query only if one (or both) of start and end are non-zero. -// If both are non-zero, return the playbook runs active between those two times. -// If start is zero, return the playbook run active before the end (not active after the end). -// If end is zero, return the playbook run active after start. -func queryActiveBetweenTimes(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder { - if start > 0 && end > 0 { - return queryActive(query, start, end) - } else if start > 0 { - return queryActive(query, start, model.GetMillis()) - } else if end > 0 { - return queryActive(query, 0, end) - } - - // both were zero, don't apply a filter: - return query -} - -func queryActive(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder { - return query.Where( - sq.And{ - sq.Or{ - sq.GtOrEq{"i.EndAt": start}, - sq.Eq{"i.EndAt": 0}, - }, - sq.Lt{"i.CreateAt": end}, - }) -} - -// queryStartedBetweenTimes will modify the query only if one (or both) of start and end are non-zero. -// If both are non-zero, return the playbook runs started between those two times. -// If start is zero, return the playbook run started before the end -// If end is zero, return the playbook run started after start. -func queryStartedBetweenTimes(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder { - if start > 0 && end > 0 { - return queryStarted(query, start, end) - } else if start > 0 { - return queryStarted(query, start, model.GetMillis()) - } else if end > 0 { - return queryStarted(query, 0, end) - } - - // both were zero, don't apply a filter: - return query -} - -func queryStarted(query sq.SelectBuilder, start int64, end int64) sq.SelectBuilder { - return query.Where( - sq.And{ - sq.GtOrEq{"i.CreateAt": start}, - sq.Lt{"i.CreateAt": end}, - }) -} diff --git a/server/playbooks/server/sqlstore/playbook_run_test.go b/server/playbooks/server/sqlstore/playbook_run_test.go deleted file mode 100644 index a22b52e7a4a..00000000000 --- a/server/playbooks/server/sqlstore/playbook_run_test.go +++ /dev/null @@ -1,1682 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "fmt" - "math/rand" - "sort" - "strings" - "testing" - "time" - - "gopkg.in/guregu/null.v4" - - sq "github.com/Masterminds/squirrel" - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - mock_sqlstore "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore/mocks" -) - -func TestCreateAndGetPlaybookRun(t *testing.T) { - db := setupTestDB(t) - store := setupSQLStore(t, db) - playbookRunStore := setupPlaybookRunStore(t, db) - setupChannelsTable(t, db) - setupPostsTable(t, db) - - validPlaybookRuns := []struct { - Name string - PlaybookRun *app.PlaybookRun - ExpectedErr error - }{ - { - Name: "Empty values", - PlaybookRun: &app.PlaybookRun{}, - ExpectedErr: nil, - }, - { - Name: "Base playbook run", - PlaybookRun: NewBuilder(t).ToPlaybookRun(), - ExpectedErr: nil, - }, - { - Name: "Name with unicode characters", - PlaybookRun: NewBuilder(t).WithName("valid unicode: ñäåö").ToPlaybookRun(), - ExpectedErr: nil, - }, - { - Name: "Created at 0", - PlaybookRun: NewBuilder(t).WithCreateAt(0).ToPlaybookRun(), - ExpectedErr: nil, - }, - { - Name: "PlaybookRun with one checklist and 10 items", - PlaybookRun: NewBuilder(t).WithChecklists([]int{10}).ToPlaybookRun(), - ExpectedErr: nil, - }, - { - Name: "PlaybookRun with five checklists with different number of items", - PlaybookRun: NewBuilder(t).WithChecklists([]int{1, 2, 3, 4, 5}).ToPlaybookRun(), - ExpectedErr: nil, - }, - { - Name: "PlaybookRun should not be nil", - PlaybookRun: nil, - ExpectedErr: errors.New("playbook run is nil"), - }, - { - Name: "PlaybookRun /can/ contain checklists with no items", - PlaybookRun: NewBuilder(t).WithChecklists([]int{0}).ToPlaybookRun(), - ExpectedErr: nil, - }, - } - - for _, testCase := range validPlaybookRuns { - t.Run(testCase.Name, func(t *testing.T) { - var expectedPlaybookRun app.PlaybookRun - if testCase.PlaybookRun != nil { - expectedPlaybookRun = *testCase.PlaybookRun - } - - returned, err := playbookRunStore.CreatePlaybookRun(testCase.PlaybookRun) - - if testCase.ExpectedErr != nil { - require.Error(t, err) - require.Equal(t, testCase.ExpectedErr.Error(), err.Error()) - require.Nil(t, returned) - return - } - - require.NoError(t, err) - require.True(t, model.IsValidId(returned.ID)) - expectedPlaybookRun.ID = returned.ID - - createPlaybookRunChannel(t, store, testCase.PlaybookRun) - - _, err = playbookRunStore.GetPlaybookRun(expectedPlaybookRun.ID) - require.NoError(t, err) - }) - } -} - -// TestGetPlaybookRun only tests getting a non-existent playbook run, since getting existing playbook runs -// is tested in TestCreateAndGetPlaybookRun above. -func TestGetPlaybookRun(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - setupChannelsTable(t, db) - - validPlaybookRuns := []struct { - Name string - ID string - ExpectedErr error - }{ - { - Name: "Get a non-existing playbook run", - ID: "nonexisting", - ExpectedErr: errors.New("playbook run with id 'nonexisting' does not exist: not found"), - }, - { - Name: "Get without ID", - ID: "", - ExpectedErr: errors.New("ID cannot be empty"), - }, - } - - for _, testCase := range validPlaybookRuns { - t.Run(testCase.Name, func(t *testing.T) { - returned, err := playbookRunStore.GetPlaybookRun(testCase.ID) - - require.Error(t, err) - require.Equal(t, testCase.ExpectedErr.Error(), err.Error()) - require.Nil(t, returned) - }) - } -} - -func TestUpdatePlaybookRun(t *testing.T) { - pbWithMetrics := NewPBBuilder(). - WithTitle("playbook"). - WithMetrics([]string{"name3", "name1", "name2"}). - ToPlaybook() - - post1 := &model.Post{ - Id: model.NewId(), - CreateAt: 10000000, - DeleteAt: 0, - } - post2 := &model.Post{ - Id: model.NewId(), - CreateAt: 20000000, - DeleteAt: 0, - } - post3 := &model.Post{ - Id: model.NewId(), - CreateAt: 30000000, - DeleteAt: 0, - } - post4 := &model.Post{ - Id: model.NewId(), - CreateAt: 40000000, - DeleteAt: 40300000, - } - post5 := &model.Post{ - Id: model.NewId(), - CreateAt: 40000001, - DeleteAt: 0, - } - post6 := &model.Post{ - Id: model.NewId(), - CreateAt: 40000002, - DeleteAt: 0, - } - allPosts := []*model.Post{post1, post2, post3, post4, post5, post6} - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - setupChannelsTable(t, db) - setupPostsTable(t, db) - savePosts(t, store, allPosts) - - playbookStore := setupPlaybookStore(t, db) - id, err := playbookStore.Create(pbWithMetrics) - require.NoError(t, err) - pbWithMetrics, err = playbookStore.Get(id) - require.NoError(t, err) - - validPlaybookRuns := []struct { - Name string - PlaybookRun *app.PlaybookRun - Update func(app.PlaybookRun) *app.PlaybookRun - ExpectedErr error - }{ - { - Name: "nil playbook run", - PlaybookRun: NewBuilder(t).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - return nil - }, - ExpectedErr: errors.New("playbook run is nil"), - }, - { - Name: "id should not be empty", - PlaybookRun: NewBuilder(t).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.ID = "" - return &old - }, - ExpectedErr: errors.New("ID should not be empty"), - }, - { - Name: "PlaybookRun /can/ contain checklists with no items", - PlaybookRun: NewBuilder(t).WithChecklists([]int{1}).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.Checklists[0].Items = nil - return &old - }, - ExpectedErr: nil, - }, - { - Name: "new description", - PlaybookRun: NewBuilder(t).WithDescription("old description").ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.Summary = "new description" - return &old - }, - ExpectedErr: nil, - }, - { - Name: "PlaybookRun with 2 checklists, update the checklists a bit", - PlaybookRun: NewBuilder(t).WithChecklists([]int{1, 1}).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.Checklists[0].Items[0].State = app.ChecklistItemStateClosed - old.Checklists[1].Items[0].Title = "new title" - return &old - }, - ExpectedErr: nil, - }, - { - Name: "PlaybookRun with metrics, update retrospective text and metrics data", - PlaybookRun: NewBuilder(t).WithPlaybookID(pbWithMetrics.ID).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.MetricsData = generateMetricData(pbWithMetrics) - old.Retrospective = "Retro1" - return &old - }, - ExpectedErr: nil, - }, - { - Name: "PlaybookRun with metrics, update metrics data partially", - PlaybookRun: NewBuilder(t).WithPlaybookID(pbWithMetrics.ID).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.MetricsData = generateMetricData(pbWithMetrics)[1:] - return &old - }, - ExpectedErr: nil, - }, - { - Name: "PlaybookRun with metrics, update metrics data twice. First one will test insert in the table, second will test update", - PlaybookRun: NewBuilder(t).WithPlaybookID(pbWithMetrics.ID).ToPlaybookRun(), - Update: func(old app.PlaybookRun) *app.PlaybookRun { - old.MetricsData = generateMetricData(pbWithMetrics) - - //first update will insert rows - _, err = playbookRunStore.UpdatePlaybookRun(&old) - require.NoError(t, err) - - //second update will update values - for i := range old.MetricsData { - old.MetricsData[i].Value = null.IntFrom(old.MetricsData[i].Value.ValueOrZero() * 10) - } - old.Retrospective = "Retro3" - return &old - }, - ExpectedErr: nil, - }, - } - - for _, testCase := range validPlaybookRuns { - t.Run(testCase.Name, func(t *testing.T) { - returned, err := playbookRunStore.CreatePlaybookRun(testCase.PlaybookRun) - require.NoError(t, err) - createPlaybookRunChannel(t, store, returned) - - expected := testCase.Update(*returned) - - _, err = playbookRunStore.UpdatePlaybookRun(expected) - - if testCase.ExpectedErr != nil { - require.Error(t, err) - require.Equal(t, testCase.ExpectedErr.Error(), err.Error()) - return - } - - require.NoError(t, err) - - actual, err := playbookRunStore.GetPlaybookRun(expected.ID) - require.NoError(t, err) - require.Equal(t, expected, actual) - }) - } -} - -func TestIfDeletedMetricsAreOmitted(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - setupChannelsTable(t, db) - setupPostsTable(t, db) - - //create playbook with metrics - playbookStore := setupPlaybookStore(t, db) - playbook := NewPBBuilder(). - WithTitle("playbook"). - WithMetrics([]string{"name3", "name1"}). - ToPlaybook() - id, err := playbookStore.Create(playbook) - require.NoError(t, err) - playbook, err = playbookStore.Get(id) - require.NoError(t, err) - - // create run based on playbook - playbookRun := NewBuilder(t).WithPlaybookID(playbook.ID).ToPlaybookRun() - playbookRun, err = playbookRunStore.CreatePlaybookRun(playbookRun) - require.NoError(t, err) - createPlaybookRunChannel(t, store, playbookRun) - - // store metrics values - playbookRun.MetricsData = generateMetricData(playbook) - _, err = playbookRunStore.UpdatePlaybookRun(playbookRun) - require.NoError(t, err) - - // delete one metric config from playbook - playbook.Metrics = playbook.Metrics[1:] - err = playbookStore.Update(playbook) - require.NoError(t, err) - - // should return single metric - actual, err := playbookRunStore.GetPlaybookRun(playbookRun.ID) - require.NoError(t, err) - require.Len(t, actual.MetricsData, 1) - require.Equal(t, actual.MetricsData[0].MetricConfigID, playbook.Metrics[0].ID) -} - -func TestRestorePlaybookRun(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - now := model.GetMillis() - initialPlaybookRun := NewBuilder(t). - WithCreateAt(now - 1000). - WithCurrentStatus(app.StatusFinished). - ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(initialPlaybookRun) - require.NoError(t, err) - createPlaybookRunChannel(t, store, returned) - - err = playbookRunStore.RestorePlaybookRun(returned.ID, now) - require.NoError(t, err) - - finalPlaybookRun := *returned - finalPlaybookRun.CurrentStatus = app.StatusInProgress - finalPlaybookRun.EndAt = 0 - finalPlaybookRun.LastStatusUpdateAt = now - - actual, err := playbookRunStore.GetPlaybookRun(returned.ID) - require.NoError(t, err) - require.Equal(t, &finalPlaybookRun, actual) -} - -// intended to catch problems with the code assembling StatusPosts -func TestStressTestGetPlaybookRuns(t *testing.T) { - rand.Seed(time.Now().UTC().UnixNano()) - - // Change these to larger numbers to stress test. Keep them low for CI. - numPlaybookRuns := 100 - postsPerPlaybookRun := 3 - perPage := 10 - verifyPages := []int{0, 2, 4, 6, 8} - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - setupChannelsTable(t, db) - setupPostsTable(t, db) - teamID := model.NewId() - withPosts := createPlaybookRunsAndPosts(t, store, playbookRunStore, numPlaybookRuns, postsPerPlaybookRun, teamID) - - t.Run("stress test status posts retrieval", func(t *testing.T) { - for _, p := range verifyPages { - returned, err := playbookRunStore.GetPlaybookRuns(app.RequesterInfo{ - UserID: "testID", - IsAdmin: true, - }, app.PlaybookRunFilterOptions{ - TeamID: teamID, - Sort: app.SortByCreateAt, - Direction: app.DirectionAsc, - Page: p, - PerPage: perPage, - }) - require.NoError(t, err) - numRet := min(perPage, len(withPosts)) - require.Equal(t, numRet, len(returned.Items)) - for i := 0; i < numRet; i++ { - idx := p*perPage + i - assert.ElementsMatch(t, withPosts[idx].StatusPosts, returned.Items[i].StatusPosts) - expWithoutStatusPosts := withPosts[idx] - expWithoutStatusPosts.StatusPosts = nil - actWithoutStatusPosts := returned.Items[i] - actWithoutStatusPosts.StatusPosts = nil - assert.Equal(t, expWithoutStatusPosts, actWithoutStatusPosts) - } - } - }) -} - -func TestStressTestGetPlaybookRunsStats(t *testing.T) { - // don't need to assemble stats in CI - t.SkipNow() - - rand.Seed(time.Now().UTC().UnixNano()) - - // Change these to larger numbers to stress test. - numPlaybookRuns := 1000 - postsPerPlaybookRun := 3 - perPage := 10 - - // For stats: - numReps := 30 - - // so we don't start returning pages with 0 playbook runs: - require.LessOrEqual(t, numReps*perPage, numPlaybookRuns) - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - setupChannelsTable(t, db) - setupPostsTable(t, db) - teamID := model.NewId() - _ = createPlaybookRunsAndPosts(t, store, playbookRunStore, numPlaybookRuns, postsPerPlaybookRun, teamID) - - t.Run("stress test status posts retrieval", func(t *testing.T) { - intervals := make([]int64, 0, numReps) - for i := 0; i < numReps; i++ { - start := time.Now() - _, err := playbookRunStore.GetPlaybookRuns(app.RequesterInfo{ - UserID: "testID", - IsAdmin: true, - }, app.PlaybookRunFilterOptions{ - TeamID: teamID, - Sort: app.SortByCreateAt, - Direction: app.DirectionAsc, - Page: i, - PerPage: perPage, - }) - intervals = append(intervals, time.Since(start).Milliseconds()) - require.NoError(t, err) - } - cil, ciu := ciForN30(intervals) - fmt.Printf("Mean: %.2f\tStdErr: %.2f\t95%% CI: (%.2f, %.2f)\n", - mean(intervals), stdErr(intervals), cil, ciu) - }) -} - -func createPlaybookRunsAndPosts(t testing.TB, store *SQLStore, playbookRunStore app.PlaybookRunStore, numPlaybookRuns, maxPostsPerPlaybookRun int, teamID string) []app.PlaybookRun { - playbookRunsSorted := make([]app.PlaybookRun, 0, numPlaybookRuns) - for i := 0; i < numPlaybookRuns; i++ { - numPosts := maxPostsPerPlaybookRun - posts := make([]*model.Post, 0, numPosts) - for j := 0; j < numPosts; j++ { - post := newPost(rand.Intn(2) == 0) - posts = append(posts, post) - } - savePosts(t, store, posts) - - inc := NewBuilder(t). - WithTeamID(teamID). - WithCreateAt(int64(100000 + i)). - WithName(fmt.Sprintf("playbook run %d", i)). - WithChecklists([]int{1}). - ToPlaybookRun() - ret, err := playbookRunStore.CreatePlaybookRun(inc) - require.NoError(t, err) - createPlaybookRunChannel(t, store, ret) - playbookRunsSorted = append(playbookRunsSorted, *ret) - } - - return playbookRunsSorted -} - -func newPost(deleted bool) *model.Post { - createAt := rand.Int63() - deleteAt := int64(0) - if deleted { - deleteAt = createAt + 100 - } - return &model.Post{ - Id: model.NewId(), - CreateAt: createAt, - DeleteAt: deleteAt, - } -} - -func TestGetPlaybookRunIDForChannel(t *testing.T) { - db := setupTestDB(t) - store := setupSQLStore(t, db) - playbookRunStore := setupPlaybookRunStore(t, db) - setupChannelsTable(t, db) - - t.Run("retrieve existing playbookRunID", func(t *testing.T) { - playbookRun1 := NewBuilder(t).ToPlaybookRun() - playbookRun2 := NewBuilder(t).ToPlaybookRun() - - returned1, err := playbookRunStore.CreatePlaybookRun(playbookRun1) - require.NoError(t, err) - createPlaybookRunChannel(t, store, playbookRun1) - - returned2, err := playbookRunStore.CreatePlaybookRun(playbookRun2) - require.NoError(t, err) - createPlaybookRunChannel(t, store, playbookRun2) - - ids1, err := playbookRunStore.GetPlaybookRunIDsForChannel(playbookRun1.ChannelID) - require.NoError(t, err) - require.Len(t, ids1, 1) - require.Equal(t, returned1.ID, ids1[0]) - ids2, err := playbookRunStore.GetPlaybookRunIDsForChannel(playbookRun2.ChannelID) - require.NoError(t, err) - require.Len(t, ids2, 1) - require.Equal(t, returned2.ID, ids2[0]) - }) - t.Run("fail to retrieve non-existing playbookRunID", func(t *testing.T) { - ids1, err := playbookRunStore.GetPlaybookRunIDsForChannel("nonexistingid") - require.Error(t, err) - require.Len(t, ids1, 0) - require.True(t, strings.HasPrefix(err.Error(), - "channel with id (nonexistingid) does not have a playbook run")) - }) -} - -func TestNukeDB(t *testing.T) { - team1id := model.NewId() - - alice := userInfo{ - ID: model.NewId(), - Name: "alice", - } - - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - - db := setupTestDB(t) - store := setupSQLStore(t, db) - - setupChannelsTable(t, db) - setupTeamMembersTable(t, db) - - playbookRunStore := setupPlaybookRunStore(t, db) - playbookStore := setupPlaybookStore(t, db) - - t.Run("nuke db with a few playbook runs in it", func(t *testing.T) { - for i := 0; i < 10; i++ { - newPlaybookRun := NewBuilder(t).ToPlaybookRun() - _, err := playbookRunStore.CreatePlaybookRun(newPlaybookRun) - require.NoError(t, err) - createPlaybookRunChannel(t, store, newPlaybookRun) - } - - var rows int64 - err := db.Get(&rows, "SELECT COUNT(*) FROM IR_Incident") - require.NoError(t, err) - require.Equal(t, 10, int(rows)) - - err = playbookRunStore.NukeDB() - require.NoError(t, err) - - err = db.Get(&rows, "SELECT COUNT(*) FROM IR_Incident") - require.NoError(t, err) - require.Equal(t, 0, int(rows)) - }) - - t.Run("nuke db with playbooks", func(t *testing.T) { - members := []userInfo{alice, bob} - addUsers(t, store, members) - addUsersToTeam(t, store, members, team1id) - - for i := 0; i < 10; i++ { - newPlaybook := NewPBBuilder().WithMembers(members).ToPlaybook() - _, err := playbookStore.Create(newPlaybook) - require.NoError(t, err) - } - - var rows int64 - - err := db.Get(&rows, "SELECT COUNT(*) FROM IR_Playbook") - require.NoError(t, err) - require.Equal(t, 10, int(rows)) - - err = db.Get(&rows, "SELECT COUNT(*) FROM IR_PlaybookMember") - require.NoError(t, err) - require.Equal(t, 20, int(rows)) - - err = playbookRunStore.NukeDB() - require.NoError(t, err) - - err = db.Get(&rows, "SELECT COUNT(*) FROM IR_Playbook") - require.NoError(t, err) - require.Equal(t, 0, int(rows)) - - err = db.Get(&rows, "SELECT COUNT(*) FROM IR_PlaybookMember") - require.NoError(t, err) - require.Equal(t, 0, int(rows)) - }) -} - -func TestTasksAndRunsDigest(t *testing.T) { - db := setupTestDB(t) - store := setupSQLStore(t, db) - playbookRunStore := setupPlaybookRunStore(t, db) - setupTeamsTable(t, db) - - userID := "testUserID" - testUser := userInfo{ID: userID, Name: "test.user"} - otherCommanderUserID := model.NewId() - otherCommander := userInfo{ID: otherCommanderUserID, Name: "other.commander"} - addUsers(t, store, []userInfo{testUser, otherCommander}) - - team1 := model.Team{ - Id: model.NewId(), - Name: "Team1", - } - team2 := model.Team{ - Id: model.NewId(), - Name: "Team2", - } - createTeams(t, store, []model.Team{team1, team2}) - - channel01 := model.Channel{Id: model.NewId(), Type: "O", Name: "channel-01"} - channel02 := model.Channel{Id: model.NewId(), Type: "O", Name: "channel-02"} - channel03 := model.Channel{Id: model.NewId(), Type: "O", Name: "channel-03"} - channel04 := model.Channel{Id: model.NewId(), Type: "O", Name: "channel-04"} - channel05 := model.Channel{Id: model.NewId(), Type: "O", Name: "channel-05"} - channel06 := model.Channel{Id: model.NewId(), Type: "O", Name: "channel-06"} - channels := []model.Channel{channel01, channel02, channel03, channel04, channel05, channel06} - - // three assigned tasks for inc01, and an overdue update - inc01 := *NewBuilder(nil). - WithName("inc01 - this is the playbook name for channel 01"). - WithChannel(&channel01). - WithTeamID(team1.Id). - WithChecklists([]int{1, 2, 3, 4}). - WithStatusUpdateEnabled(true). - WithUpdateOverdueBy(2 * time.Minute). - WithOwnerUserID(userID). - ToPlaybookRun() - inc01.Checklists[0].Items[0].AssigneeID = userID - inc01.Checklists[1].Items[1].AssigneeID = userID - inc01.Checklists[2].Items[2].AssigneeID = userID - inc01TaskTitles := []string{ - inc01.Checklists[0].Items[0].Title, - inc01.Checklists[1].Items[1].Title, - inc01.Checklists[2].Items[2].Title, - } - // This should not trigger an assigned task: - inc01.Checklists[3].Items[0].Title = userID - - // one assigned task for inc02, works cross team, with overdue update - inc02 := *NewBuilder(nil). - WithName("inc02 - this is the playbook name for channel 02"). - WithChannel(&channel02). - WithTeamID(team2.Id). - WithStatusUpdateEnabled(true). - WithUpdateOverdueBy(1 * time.Minute). - WithOwnerUserID(userID). - WithChecklists([]int{1, 2, 3, 4}). - ToPlaybookRun() - inc02.Checklists[3].Items[2].AssigneeID = userID - inc02TaskTitles := []string{inc02.Checklists[3].Items[2].Title} - - // no assigned task for inc03, with non-overdue update - inc03 := *NewBuilder(nil). - WithName("inc03 - this is the playbook name for channel 03"). - WithChannel(&channel03). - WithTeamID(team1.Id). - WithStatusUpdateEnabled(true). - WithUpdateOverdueBy(-2 * time.Minute). - WithOwnerUserID(userID). - WithChecklists([]int{1, 2, 3, 4}). - ToPlaybookRun() - inc03.Checklists[3].Items[2].AssigneeID = "someotheruserid" - - // one assigned task for inc04, with overdue update, but inc04 is finished - inc04 := *NewBuilder(nil). - WithName("inc04 - this is the playbook name for channel 04"). - WithChannel(&channel04). - WithTeamID(team1.Id). - WithChecklists([]int{1, 2, 3, 4}). - WithStatusUpdateEnabled(true). - WithUpdateOverdueBy(2 * time.Minute). - WithOwnerUserID(userID). - WithCurrentStatus(app.StatusFinished). - ToPlaybookRun() - inc04.Checklists[3].Items[2].AssigneeID = userID - - // no assigned task for inc05, and not participant in inc05 - inc05 := *NewBuilder(nil). - WithName("inc05 - this is the playbook name for channel 05"). - WithChannel(&channel05). - WithTeamID(team1.Id). - WithOwnerUserID(otherCommanderUserID). - WithChecklists([]int{1, 2, 3, 4}). - ToPlaybookRun() - inc05.Checklists[3].Items[2].AssigneeID = "someotheruserid" - - // no assigned task for inc06, with overdue update, not commander but participating - inc06 := *NewBuilder(nil). - WithName("inc06 - this is the playbook name for channel 06"). - WithChannel(&channel06). - WithTeamID(team1.Id). - WithOwnerUserID(otherCommanderUserID). - WithStatusUpdateEnabled(true). - WithUpdateOverdueBy(2 * time.Minute). - WithChecklists([]int{1, 2, 3, 4}). - ToPlaybookRun() - inc03.Checklists[2].Items[2].AssigneeID = "someotheruserid" - - playbookRuns := []app.PlaybookRun{inc01, inc02, inc03, inc04, inc05, inc06} - - for i := range playbookRuns { - created, err := playbookRunStore.CreatePlaybookRun(&playbookRuns[i]) - playbookRuns[i] = *created - require.NoError(t, err) - } - - addUsersToRuns(t, store, []userInfo{testUser}, []string{playbookRuns[0].ID, playbookRuns[1].ID, playbookRuns[2].ID, playbookRuns[3].ID, playbookRuns[5].ID}) - - createChannels(t, store, channels) - - t.Run("gets assigned tasks only", func(t *testing.T) { - runs, err := playbookRunStore.GetRunsWithAssignedTasks(userID) - require.NoError(t, err) - - total := 0 - for _, run := range runs { - total += len(run.Tasks) - } - - require.Equal(t, 4, total) - - // don't make assumptions about ordering until we figure that out PM-side - expected := map[string][]string{ - inc01.Name: inc01TaskTitles, - inc02.Name: inc02TaskTitles, - } - for _, run := range runs { - for _, task := range run.Tasks { - require.Contains(t, expected[run.Name], task.Title) - } - } - }) - - t.Run("gets participating runs only", func(t *testing.T) { - runs, err := playbookRunStore.GetParticipatingRuns(userID) - require.NoError(t, err) - - total := len(runs) - - require.Equal(t, 4, total) - - // don't make assumptions about ordering until we figure that out PM-side - expected := map[string]int{ - inc01.Name: 1, - inc02.Name: 1, - inc03.Name: 1, - inc06.Name: 1, - } - - actual := make(map[string]int) - - for _, run := range runs { - actual[run.Name]++ - } - - require.Equal(t, expected, actual) - }) - - t.Run("gets overdue updates", func(t *testing.T) { - runs, err := playbookRunStore.GetOverdueUpdateRuns(userID) - require.NoError(t, err) - - total := len(runs) - - require.Equal(t, 2, total) - - // don't make assumptions about ordering until we figure that out PM-side - expected := map[string]int{ - inc01.Name: 1, - inc02.Name: 1, - } - - actual := make(map[string]int) - - for _, run := range runs { - actual[run.Name]++ - } - - require.Equal(t, expected, actual) - }) -} - -func TestGetRunsActiveTotal(t *testing.T) { - createRuns := func(store *SQLStore, playbookRunStore app.PlaybookRunStore, num int, status string) { - now := model.GetMillis() - for i := 0; i < num; i++ { - run := NewBuilder(t). - WithCreateAt(now - int64(i*1000)). - WithCurrentStatus(status). - ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - createPlaybookRunChannel(t, store, returned) - } - } - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - t.Run("zero runs", func(t *testing.T) { - actual, err := playbookRunStore.GetRunsActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - // add finished runs - createRuns(store, playbookRunStore, 10, app.StatusFinished) - - t.Run("zero active runs, few finished runs", func(t *testing.T) { - actual, err := playbookRunStore.GetRunsActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - // add active runs - createRuns(store, playbookRunStore, 15, app.StatusInProgress) - t.Run("few active runs, few finished runs", func(t *testing.T) { - actual, err := playbookRunStore.GetRunsActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(15), actual) - }) -} - -func TestGetOverdueUpdateRunsTotal(t *testing.T) { - // overdue: 0 means no reminders at all. -1 means set only due reminders. 1 means set only overdue reminders. - createRuns := func(store *SQLStore, playbookRunStore app.PlaybookRunStore, num int, status string, overdue int) { - now := model.GetMillis() - for i := 0; i < num; i++ { - run := NewBuilder(t). - WithCreateAt(now - int64(i*1000)). - WithCurrentStatus(status). - WithStatusUpdateEnabled(true). - WithUpdateOverdueBy(time.Duration(overdue) * 2 * time.Minute * time.Duration(i+1)). - ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - createPlaybookRunChannel(t, store, returned) - } - } - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - t.Run("zero runs", func(t *testing.T) { - actual, err := playbookRunStore.GetOverdueUpdateRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - t.Run("zero active runs with overdue, few finished runs with overdue", func(t *testing.T) { - // add finished runs with overdue reminders - createRuns(store, playbookRunStore, 7, app.StatusFinished, 1) - // add active runs without reminders - createRuns(store, playbookRunStore, 5, app.StatusInProgress, 0) - - actual, err := playbookRunStore.GetOverdueUpdateRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - t.Run("few active runs with overdue", func(t *testing.T) { - // add active runs with overdue - createRuns(store, playbookRunStore, 9, app.StatusInProgress, 1) - - actual, err := playbookRunStore.GetOverdueUpdateRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(9), actual) - }) - - t.Run("few active runs with due reminder", func(t *testing.T) { - // add active runs with due reminder - createRuns(store, playbookRunStore, 4, app.StatusInProgress, -1) - - actual, err := playbookRunStore.GetOverdueUpdateRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(9), actual) - }) -} - -func TestGetOverdueRetroRunsTotal(t *testing.T) { - createRuns := func( - store *SQLStore, - playbookRunStore app.PlaybookRunStore, - num int, - status string, - retroEnabled bool, - retroInterval int64, - retroPublishedAt int64, - retroCanceled bool, - ) { - - now := model.GetMillis() - - for i := 0; i < num; i++ { - run := NewBuilder(t). - WithCreateAt(now - int64(i*1000)). - WithCurrentStatus(status). - WithRetrospectiveEnabled(retroEnabled). - WithRetrospectivePublishedAt(retroPublishedAt). - WithRetrospectiveCanceled(retroCanceled). - WithRetrospectiveReminderInterval(retroInterval). - ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - createPlaybookRunChannel(t, store, returned) - } - } - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - t.Run("zero runs", func(t *testing.T) { - actual, err := playbookRunStore.GetOverdueRetroRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - t.Run("zero finished runs, few active runs", func(t *testing.T) { - // add active runs with enabled/disabled retro - createRuns(store, playbookRunStore, 5, app.StatusInProgress, true, 60, 0, false) - createRuns(store, playbookRunStore, 2, app.StatusInProgress, false, 0, 0, false) - // add active runs with published retro - createRuns(store, playbookRunStore, 6, app.StatusInProgress, true, 60, 100000000, false) - - actual, err := playbookRunStore.GetOverdueRetroRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - t.Run("few finished runs, few active runs", func(t *testing.T) { - // add finished runs with enabled/disabled retro - createRuns(store, playbookRunStore, 3, app.StatusFinished, true, 60, 0, false) - createRuns(store, playbookRunStore, 4, app.StatusFinished, false, 60, 0, false) - // add finished runs with published/canceled retro - createRuns(store, playbookRunStore, 7, app.StatusFinished, true, 60, 100000000, false) - createRuns(store, playbookRunStore, 8, app.StatusFinished, true, 60, 100000000, true) - // add finished runs without retro and without reminder - createRuns(store, playbookRunStore, 2, app.StatusFinished, true, 60, 100000000, false) - - actual, err := playbookRunStore.GetOverdueRetroRunsTotal() - require.NoError(t, err) - require.Equal(t, int64(3), actual) - }) -} - -func TestGetFollowersActiveTotal(t *testing.T) { - createRuns := func( - playbookRunStore app.PlaybookRunStore, - followers []string, - teamID string, - num int, - status string, - ) { - - for i := 0; i < num; i++ { - run := NewBuilder(t). - WithCurrentStatus(status). - WithTeamID(teamID). - ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - for _, f := range followers { - err = playbookRunStore.Follow(returned.ID, f) - require.NoError(t, err) - } - } - } - - alice := userInfo{ - ID: model.NewId(), - } - bob := userInfo{ - ID: model.NewId(), - } - followers := []string{alice.ID, bob.ID} - teamID := model.NewId() - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - setupChannelsTable(t, db) - setupTeamMembersTable(t, db) - - t.Run("zero active followers", func(t *testing.T) { - // create active runs without followers - createRuns(playbookRunStore, nil, teamID, 2, app.StatusInProgress) - // create finished runs with followers - createRuns(playbookRunStore, followers, teamID, 3, app.StatusFinished) - - actual, err := playbookRunStore.GetFollowersActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - t.Run("runs with active followers", func(t *testing.T) { - // create active runs with followers - createRuns(playbookRunStore, followers, teamID, 3, app.StatusInProgress) - createRuns(playbookRunStore, followers[:1], teamID, 2, app.StatusInProgress) - - expected := 2*3 + 1*2 - actual, err := playbookRunStore.GetFollowersActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(expected), actual) - }) -} - -func TestGetParticipantsActiveTotal(t *testing.T) { - createRuns := func( - store *SQLStore, - playbookRunStore app.PlaybookRunStore, - playbookID string, - participants []userInfo, - teamID string, - num int, - status string, - ) { - - for i := 0; i < num; i++ { - run := NewBuilder(t). - WithCurrentStatus(status). - WithPlaybookID(playbookID). - WithTeamID(teamID). - ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - if len(participants) > 0 { - addUsersToRuns(t, store, participants, []string{returned.ID}) - } - - createPlaybookRunChannel(t, store, returned) - } - } - - alice := userInfo{ - ID: model.NewId(), - Name: "alice", - } - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - tom := userInfo{ - ID: model.NewId(), - Name: "tom", - } - bot1 := userInfo{ - ID: model.NewId(), - Name: "Mr. Bot", - } - - playbook1 := NewPBBuilder(). - WithTitle("playbook 1"). - ToPlaybook() - playbook2 := NewPBBuilder(). - WithTitle("playbook 2"). - ToPlaybook() - - team1ID := model.NewId() - team2ID := model.NewId() - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - playbookStore := setupPlaybookStore(t, db) - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - setupChannelMembersTable(t, db) - setupChannelMemberHistoryTable(t, db) - setupChannelsTable(t, db) - - addUsers(t, store, []userInfo{alice, bob, tom}) - addBots(t, store, []userInfo{bot1}) - - addUsersToTeam(t, store, []userInfo{alice, bob, bot1}, team1ID) - addUsersToTeam(t, store, []userInfo{tom, bob, bot1}, team2ID) - - // create two playbooks - playbook1ID, err := playbookStore.Create(playbook1) - require.NoError(t, err) - playbook2ID, err := playbookStore.Create(playbook2) - require.NoError(t, err) - - t.Run("zero active participants", func(t *testing.T) { - // create active runs without participants - createRuns(store, playbookRunStore, "", nil, team1ID, 2, app.StatusInProgress) - // create finished runs with participants - createRuns(store, playbookRunStore, playbook1ID, []userInfo{alice, bob, bot1}, team1ID, 3, app.StatusFinished) - - actual, err := playbookRunStore.GetParticipantsActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - t.Run("runs with active participants", func(t *testing.T) { - // create active runs with participants - createRuns(store, playbookRunStore, playbook1ID, []userInfo{alice, bob, bot1}, team1ID, 3, app.StatusInProgress) - createRuns(store, playbookRunStore, playbook2ID, []userInfo{tom, bob}, team2ID, 5, app.StatusInProgress) - - expected := 2*3 + 2*5 // ignore bots - actual, err := playbookRunStore.GetParticipantsActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(expected), actual) - }) -} - -func setupPlaybookRunStore(t *testing.T, db *sqlx.DB) app.PlaybookRunStore { - mockCtrl := gomock.NewController(t) - - kvAPI := mock_sqlstore.NewMockKVAPI(mockCtrl) - configAPI := mock_sqlstore.NewMockConfigurationAPI(mockCtrl) - pluginAPIClient := PluginAPIClient{ - KV: kvAPI, - Configuration: configAPI, - } - - sqlStore := setupSQLStore(t, db) - - return NewPlaybookRunStore(pluginAPIClient, sqlStore) -} - -func TestGetSchemeRolesForChannel(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - - t.Run("channel with no scheme", func(t *testing.T) { - _, err := store.execBuilder(store.db, sq. - Insert("Schemes"). - SetMap(map[string]interface{}{ - "ID": "scheme_0", - "DefaultChannelGuestRole": "guest0", - "DefaultChannelUserRole": "user0", - "DefaultChannelAdminRole": "admin0", - })) - require.NoError(t, err) - - _, err = store.execBuilder(store.db, sq. - Insert("Channels"). - SetMap(map[string]interface{}{ - "ID": "channel_0", - })) - require.NoError(t, err) - - _, _, _, err = playbookRunStore.GetSchemeRolesForChannel("channel_0") - require.Error(t, err) - }) - - t.Run("channel with scheme", func(t *testing.T) { - _, err := store.execBuilder(store.db, sq. - Insert("Schemes"). - SetMap(map[string]interface{}{ - "ID": "scheme_1", - "DefaultChannelGuestRole": nil, - "DefaultChannelUserRole": "user1", - "DefaultChannelAdminRole": "admin1", - })) - require.NoError(t, err) - - _, err = store.execBuilder(store.db, sq. - Insert("Channels"). - SetMap(map[string]interface{}{ - "ID": "channel_1", - "SchemeId": "scheme_1", - })) - require.NoError(t, err) - - guest, user, admin, err := playbookRunStore.GetSchemeRolesForChannel("channel_1") - require.NoError(t, err) - require.Equal(t, guest, model.ChannelGuestRoleId) - require.Equal(t, user, "user1") - require.Equal(t, admin, "admin1") - }) -} - -func TestGetPlaybookRunIDsForUser(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - - alice := userInfo{ - ID: model.NewId(), - Name: "alice", - } - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - tom := userInfo{ - ID: model.NewId(), - Name: "tom", - } - allIDs := []string{} - teamID := model.NewId() - addUsersToTeam(t, store, []userInfo{alice, bob, tom}, teamID) - - for i := 0; i < 10; i++ { - run := NewBuilder(t).WithTeamID(teamID).ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - - allIDs = append(allIDs, returned.ID) - } - - t.Run("no runs for user", func(t *testing.T) { - returnedIDs, err := playbookRunStore.GetPlaybookRunIDsForUser(alice.ID) - require.NoError(t, err) - require.Len(t, returnedIDs, 0) - }) - - t.Run("all runs for user", func(t *testing.T) { - for _, id := range allIDs { - addUsersToRuns(t, store, []userInfo{tom}, []string{id}) - } - returnedIDs, err := playbookRunStore.GetPlaybookRunIDsForUser(tom.ID) - require.NoError(t, err) - require.Len(t, returnedIDs, len(allIDs)) - }) - - t.Run("some runs for user", func(t *testing.T) { - for i := 0; i < len(allIDs)/2; i++ { - addUsersToRuns(t, store, []userInfo{bob}, []string{allIDs[i]}) - } - returnedIDs, err := playbookRunStore.GetPlaybookRunIDsForUser(bob.ID) - require.NoError(t, err) - require.Len(t, returnedIDs, len(allIDs)/2) - }) - - t.Run("remove user from team", func(t *testing.T) { - for _, id := range allIDs { - addUsersToRuns(t, store, []userInfo{alice}, []string{id}) - } - updateBuilder := store.builder.Update("TeamMembers"). - Set("DeleteAt", model.GetMillis()). - Where(sq.And{sq.Eq{"TeamID": teamID}, sq.Eq{"UserID": alice.ID}}) - _, err := store.execBuilder(store.db, updateBuilder) - require.NoError(t, err) - - returnedIDs, err := playbookRunStore.GetPlaybookRunIDsForUser(alice.ID) - require.NoError(t, err) - require.Len(t, returnedIDs, 0) - }) -} - -func TestGetRunMetadataByIDs(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - - allIDs := []string{} - teamID := model.NewId() - for i := 0; i < 10; i++ { - run := NewBuilder(t).WithTeamID(teamID).ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - - allIDs = append(allIDs, returned.ID) - } - - t.Run("empty slice of ids", func(t *testing.T) { - runs, err := playbookRunStore.GetRunMetadataByIDs([]string{}) - require.NoError(t, err) - require.Len(t, runs, 0) - }) - - t.Run("single id", func(t *testing.T) { - runs, err := playbookRunStore.GetRunMetadataByIDs([]string{allIDs[0]}) - require.NoError(t, err) - require.Len(t, runs, 1) - require.Equal(t, allIDs[0], runs[0].ID) - require.Equal(t, teamID, runs[0].TeamID) - require.Equal(t, "base playbook run", runs[0].Name) - }) - - t.Run("many ids", func(t *testing.T) { - runs, err := playbookRunStore.GetRunMetadataByIDs(allIDs[0:5]) - require.NoError(t, err) - require.Len(t, runs, 5) - m := map[string]app.RunMetadata{} - for _, run := range runs { - m[run.ID] = run - } - for _, id := range allIDs[0:5] { - run := m[id] - require.Equal(t, id, run.ID) - require.Equal(t, teamID, run.TeamID) - require.Equal(t, "base playbook run", run.Name) - } - }) -} - -func TestGetTaskAsTopicMetadataByIDs(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - - teamID := model.NewId() - allRuns := []*app.PlaybookRun{} - for i := 0; i < 10; i++ { - run := NewBuilder(t).WithTeamID(teamID).WithChecklists([]int{1, 2, 3, 4, 5}).ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - - allRuns = append(allRuns, returned) - } - - t.Run("empty slice of ids", func(t *testing.T) { - tasks, err := playbookRunStore.GetTaskAsTopicMetadataByIDs([]string{}) - require.NoError(t, err) - require.Len(t, tasks, 0) - }) - - t.Run("single id not in db", func(t *testing.T) { - tasks, err := playbookRunStore.GetTaskAsTopicMetadataByIDs([]string{"random-task-id"}) - - require.NoError(t, err) - require.Len(t, tasks, 1) - require.Equal(t, tasks[0], app.TopicMetadata{}) - - }) - - t.Run("single task id from db", func(t *testing.T) { - tasks, err := playbookRunStore.GetTaskAsTopicMetadataByIDs([]string{allRuns[0].Checklists[0].Items[0].ID}) - require.NoError(t, err) - require.Len(t, tasks, 1) - - require.Equal(t, allRuns[0].Checklists[0].Items[0].ID, tasks[0].ID) - require.Equal(t, teamID, tasks[0].TeamID) - require.Equal(t, allRuns[0].ID, tasks[0].RunID) - }) - - t.Run("multiple task id from db", func(t *testing.T) { - taskIDs := []string{ - allRuns[0].Checklists[0].Items[0].ID, - allRuns[0].Checklists[1].Items[0].ID, - allRuns[1].Checklists[0].Items[0].ID, - allRuns[1].Checklists[1].Items[0].ID, - allRuns[5].Checklists[0].Items[0].ID, - allRuns[5].Checklists[1].Items[0].ID, - } - tasks, err := playbookRunStore.GetTaskAsTopicMetadataByIDs(taskIDs) - require.NoError(t, err) - require.Len(t, tasks, 6) - - for i, taskID := range taskIDs { - require.Equal(t, taskID, tasks[i].ID) - require.Equal(t, teamID, tasks[i].TeamID) - } - }) - - t.Run("taskID in title", func(t *testing.T) { - runWithTaskIDInTitle := NewBuilder(t).WithTeamID(teamID).WithChecklists([]int{1, 2, 3, 4, 5}).ToPlaybookRun() - runWithTaskIDInTitle.Checklists[0].Title = allRuns[5].Checklists[0].Items[0].ID - - returnedRunWithTaskIDInTitle, err := playbookRunStore.CreatePlaybookRun(runWithTaskIDInTitle) - require.NoError(t, err) - taskIDs := []string{ - allRuns[0].Checklists[0].Items[0].ID, - allRuns[0].Checklists[1].Items[0].ID, - allRuns[1].Checklists[0].Items[0].ID, - allRuns[1].Checklists[1].Items[0].ID, - allRuns[5].Checklists[0].Items[0].ID, - allRuns[5].Checklists[1].Items[0].ID, - runWithTaskIDInTitle.Checklists[0].Items[0].ID, - } - playbookRunIDs := []string{ - allRuns[0].ID, - allRuns[0].ID, - allRuns[1].ID, - allRuns[1].ID, - allRuns[5].ID, - allRuns[5].ID, - returnedRunWithTaskIDInTitle.ID, - } - tasks, err := playbookRunStore.GetTaskAsTopicMetadataByIDs(taskIDs) - require.NoError(t, err) - require.Len(t, tasks, 7) - - for i, taskID := range taskIDs { - require.Equal(t, taskID, tasks[i].ID) - require.Equal(t, teamID, tasks[i].TeamID) - require.Equal(t, playbookRunIDs[i], tasks[i].RunID) - } - }) -} - -func TestGetStatusAsTopicMetadataByIDs(t *testing.T) { - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - - teamID := model.NewId() - allStatusIDs := []string{} - runIDs := []string{} - for i := 0; i < 10; i++ { - run := NewBuilder(t).WithTeamID(teamID).WithChecklists([]int{1, 2, 3, 4, 5}).ToPlaybookRun() - - returned, err := playbookRunStore.CreatePlaybookRun(run) - require.NoError(t, err) - - for j := 0; j < 10; j++ { - statusID := model.NewId() - err := playbookRunStore.UpdateStatus(&app.SQLStatusPost{ - PlaybookRunID: returned.ID, - PostID: statusID, - }) - require.NoError(t, err) - allStatusIDs = append(allStatusIDs, statusID) - runIDs = append(runIDs, returned.ID) - } - } - - t.Run("empty slice of ids", func(t *testing.T) { - statuses, err := playbookRunStore.GetStatusAsTopicMetadataByIDs([]string{}) - require.NoError(t, err) - require.Len(t, statuses, 0) - }) - - t.Run("single id not in db", func(t *testing.T) { - statuses, err := playbookRunStore.GetStatusAsTopicMetadataByIDs([]string{"random-status-id"}) - require.NoError(t, err) - require.Len(t, statuses, 1) - require.Equal(t, statuses[0], app.TopicMetadata{}) - }) - - t.Run("single status id from db", func(t *testing.T) { - statuses, err := playbookRunStore.GetStatusAsTopicMetadataByIDs([]string{allStatusIDs[0]}) - require.NoError(t, err) - require.Len(t, statuses, 1) - - require.Equal(t, allStatusIDs[0], statuses[0].ID) - require.Equal(t, teamID, statuses[0].TeamID) - require.Equal(t, runIDs[0], statuses[0].RunID) - }) - - t.Run("all status ids from db", func(t *testing.T) { - statuses, err := playbookRunStore.GetStatusAsTopicMetadataByIDs(allStatusIDs) - require.NoError(t, err) - require.Len(t, statuses, len(allStatusIDs)) - - for i, statusID := range allStatusIDs { - require.Equal(t, statusID, statuses[i].ID) - require.Equal(t, teamID, statuses[i].TeamID) - require.Equal(t, runIDs[i], statuses[i].RunID) - } - }) -} - -// PlaybookRunBuilder is a utility to build playbook runs with a default base. -// Use it as: -// NewBuilder.WithName("name").WithXYZ(xyz)....ToPlaybookRun() -type PlaybookRunBuilder struct { - t testing.TB - playbookRun *app.PlaybookRun -} - -func NewBuilder(t testing.TB) *PlaybookRunBuilder { - return &PlaybookRunBuilder{ - t: t, - playbookRun: &app.PlaybookRun{ - Name: "base playbook run", - OwnerUserID: model.NewId(), - TeamID: model.NewId(), - ChannelID: model.NewId(), - CreateAt: model.GetMillis(), - DeleteAt: 0, - PostID: model.NewId(), - PlaybookID: model.NewId(), - Checklists: nil, - CurrentStatus: "InProgress", - Type: app.RunTypePlaybook, - }, - } -} - -func (ib *PlaybookRunBuilder) WithName(name string) *PlaybookRunBuilder { - ib.playbookRun.Name = name - - return ib -} - -func (ib *PlaybookRunBuilder) WithDescription(desc string) *PlaybookRunBuilder { - ib.playbookRun.Summary = desc - - return ib -} - -func (ib *PlaybookRunBuilder) WithID() *PlaybookRunBuilder { - ib.playbookRun.ID = model.NewId() - - return ib -} - -func (ib *PlaybookRunBuilder) WithParticipant(user userInfo) *PlaybookRunBuilder { - ib.playbookRun.ParticipantIDs = append(ib.playbookRun.ParticipantIDs, user.ID) - sort.Strings(ib.playbookRun.ParticipantIDs) - - return ib -} - -func (ib *PlaybookRunBuilder) ToPlaybookRun() *app.PlaybookRun { - return ib.playbookRun -} - -func (ib *PlaybookRunBuilder) WithCreateAt(createAt int64) *PlaybookRunBuilder { - ib.playbookRun.CreateAt = createAt - - return ib -} - -func (ib *PlaybookRunBuilder) WithChecklists(itemsPerChecklist []int) *PlaybookRunBuilder { - ib.playbookRun.Checklists = make([]app.Checklist, len(itemsPerChecklist)) - - for i, numItems := range itemsPerChecklist { - var items []app.ChecklistItem - for j := 0; j < numItems; j++ { - items = append(items, app.ChecklistItem{ - ID: model.NewId(), - Title: fmt.Sprint("Checklist ", i, " - item ", j), - }) - } - - ib.playbookRun.Checklists[i] = app.Checklist{ - ID: model.NewId(), - Title: fmt.Sprint("Checklist ", i), - Items: items, - } - } - - return ib -} - -func (ib *PlaybookRunBuilder) WithOwnerUserID(id string) *PlaybookRunBuilder { - ib.playbookRun.OwnerUserID = id - - return ib -} - -func (ib *PlaybookRunBuilder) WithTeamID(id string) *PlaybookRunBuilder { - ib.playbookRun.TeamID = id - - return ib -} - -func (ib *PlaybookRunBuilder) WithCurrentStatus(status string) *PlaybookRunBuilder { - ib.playbookRun.CurrentStatus = status - - if status == app.StatusFinished { - ib.playbookRun.EndAt = ib.playbookRun.CreateAt + 100 - } - - return ib -} - -func (ib *PlaybookRunBuilder) WithChannel(channel *model.Channel) *PlaybookRunBuilder { - ib.playbookRun.ChannelID = channel.Id - - // Consider the playbook run name as authoritative. - channel.DisplayName = ib.playbookRun.Name - - return ib -} - -func (ib *PlaybookRunBuilder) WithPlaybookID(id string) *PlaybookRunBuilder { - ib.playbookRun.PlaybookID = id - - return ib -} - -func (ib *PlaybookRunBuilder) WithStatusUpdateEnabled(isEnabled bool) *PlaybookRunBuilder { - ib.playbookRun.StatusUpdateEnabled = isEnabled - - return ib -} - -// WithUpdateOverdueBy sets a PreviousReminder and LastStatusUpdate such that there is an update -// due overdueAmount ago. Set a negative number for an update due in the future. -func (ib *PlaybookRunBuilder) WithUpdateOverdueBy(overdueAmount time.Duration) *PlaybookRunBuilder { - // simplify the math: set previous reminder to be the overdue amount - ib.playbookRun.PreviousReminder = overdueAmount - - // and the lastStatusUpdateAt to be twice as much before that - ib.playbookRun.LastStatusUpdateAt = time.Now().Add(-2*overdueAmount).Unix() * 1000 - - return ib -} - -func (ib *PlaybookRunBuilder) WithRetrospectiveEnabled(enabled bool) *PlaybookRunBuilder { - ib.playbookRun.RetrospectiveEnabled = enabled - - return ib -} - -func (ib *PlaybookRunBuilder) WithRetrospectivePublishedAt(publishedAt int64) *PlaybookRunBuilder { - ib.playbookRun.RetrospectivePublishedAt = publishedAt - - return ib -} - -func (ib *PlaybookRunBuilder) WithRetrospectiveCanceled(canceled bool) *PlaybookRunBuilder { - ib.playbookRun.RetrospectiveWasCanceled = canceled - - return ib -} - -func (ib *PlaybookRunBuilder) WithRetrospectiveReminderInterval(interval int64) *PlaybookRunBuilder { - ib.playbookRun.RetrospectiveReminderIntervalSeconds = interval - - return ib -} - -func generateMetricData(playbook app.Playbook) []app.RunMetricData { - metrics := make([]app.RunMetricData, 0) - for i, mc := range playbook.Metrics { - metrics = append(metrics, - app.RunMetricData{ - MetricConfigID: mc.ID, - Value: null.IntFrom(int64(i + 10)), - }, - ) - } - // Entirely for consistency for the tests - sort.Slice(metrics, func(i, j int) bool { return metrics[i].MetricConfigID < metrics[j].MetricConfigID }) - - return metrics -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/server/playbooks/server/sqlstore/playbook_test.go b/server/playbooks/server/sqlstore/playbook_test.go deleted file mode 100644 index 55161ef6e5b..00000000000 --- a/server/playbooks/server/sqlstore/playbook_test.go +++ /dev/null @@ -1,2041 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "fmt" - "sort" - "strconv" - "testing" - - "gopkg.in/guregu/null.v4" - - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - mock_sqlstore "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore/mocks" -) - -func membersFromIds(ids []string) []app.PlaybookMember { - members := []app.PlaybookMember{} - for _, id := range ids { - members = append(members, app.PlaybookMember{ - UserID: id, - Roles: []string{}, - SchemeRoles: []string{}, - }) - } - - return members -} - -func newUserInfo() userInfo { - id := model.NewId() - return userInfo{ - ID: id, - Name: id, - } -} - -func multipleUserInfo(n int) []userInfo { - list := make([]userInfo, 0, n) - for i := 0; i < n; i++ { - list = append(list, newUserInfo()) - } - return list -} - -func cleanMetricsIDs(metrics []app.PlaybookMetricConfig) { - for i := range metrics { - metrics[i].ID = "" - metrics[i].PlaybookID = "" - } -} - -func metricsFromNames(names []string) []app.PlaybookMetricConfig { - metrics := make([]app.PlaybookMetricConfig, len(names)) - types := []string{app.MetricTypeCurrency, app.MetricTypeDuration, app.MetricTypeInteger} - - for i := range names { - metrics[i] = app.PlaybookMetricConfig{ - Title: names[i], - Description: "description: " + strconv.Itoa(i), - Type: types[i%len(types)], - Target: null.IntFrom(int64(i)), - } - } - return metrics -} - -func TestGetPlaybook(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - team3id := model.NewId() - - jon := userInfo{ - ID: model.NewId(), - Name: "jon", - } - - andrew := userInfo{ - ID: model.NewId(), - Name: "Andrew", - } - - matt := userInfo{ - ID: model.NewId(), - Name: "Matt", - } - - lucia := userInfo{ - ID: model.NewId(), - Name: "Lucía", - } - - desmond := userInfo{ - ID: model.NewId(), - Name: "Desmond", - } - - pb01 := NewPBBuilder(). - WithTitle("playbook 1"). - WithDescription("this is a description, not very long, but it can be up to 4096 bytes"). - WithTeamID(team1id). - WithCreateAt(500). - WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, andrew, matt}). - ToPlaybook() - - pb02 := NewPBBuilder(). - WithTitle("playbook 2"). - WithTeamID(team1id). - WithCreateAt(600). - WithCreatePublic(true). - WithChecklists([]int{1, 4, 6, 7, 1}). // 19 - WithMembers([]userInfo{andrew, matt}). - WithMetrics([]string{"name11", "name12"}). - ToPlaybook() - - pb03 := NewPBBuilder(). - WithTitle("playbook 3"). - WithTeamID(team1id). - WithChecklists([]int{1, 2, 3}). - WithCreateAt(700). - WithMembers([]userInfo{jon, matt, lucia}). - WithMetrics([]string{"name14", "name12", "name13", "name11"}). - ToPlaybook() - - pb04 := NewPBBuilder(). - WithTitle("playbook 4"). - WithDescription("this is a description, not very long, but it can be up to 2048 bytes"). - WithTeamID(team1id). - WithCreateAt(800). - WithChecklists([]int{20}). - WithMembers([]userInfo{matt}). - WithMetrics([]string{"name17"}). - ToPlaybook() - - pb05 := NewPBBuilder(). - WithTitle("playbook 5"). - WithTeamID(team2id). - WithCreateAt(1000). - WithChecklists([]int{1}). - WithMembers([]userInfo{jon, andrew}). - WithMetrics([]string{"name21", "name22", "name20", "name24"}). - ToPlaybook() - - pb06 := NewPBBuilder(). - WithTitle("playbook 6"). - WithTeamID(team2id). - WithCreateAt(1100). - WithChecklists([]int{1, 2, 3}). - WithMembers([]userInfo{matt}). - WithMetrics([]string{"name27", "name29"}). - ToPlaybook() - - pb07 := NewPBBuilder(). - WithTitle("playbook 7"). - WithTeamID(team3id). - WithCreateAt(1200). - WithChecklists([]int{1}). - WithMembers([]userInfo{andrew}). - WithChannelNameTemplate("playbook XX"). - ToPlaybook() - - pb08 := NewPBBuilder(). - WithTitle("playbook 8 -- so many members, but should have Desmond and Lucy"). - WithTeamID(team3id). - WithCreateAt(1300). - WithChecklists([]int{1}). - WithMembers(append(multipleUserInfo(100), desmond, lucia)). - WithKeywords([]string{"keyword"}). - WithChannelNameTemplate("playbook YY"). - ToPlaybook() - - playbooks := []app.Playbook{pb01, pb02, pb03, pb04, pb05, pb06, pb07, pb08} - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run(driverName+" - id empty", func(t *testing.T) { - actual, err := playbookStore.Get("") - require.Error(t, err) - require.EqualError(t, err, "ID cannot be empty") - require.Equal(t, app.Playbook{}, actual) - }) - - t.Run(driverName+" - create and retrieve playbook", func(t *testing.T) { - id, err := playbookStore.Create(pb02) - require.NoError(t, err) - expected := pb02.Clone() - expected.ID = id - - actual, err := playbookStore.Get(id) - require.NoError(t, err) - - //check if playbookID was set correctly - for _, m := range actual.Metrics { - require.Equal(t, m.PlaybookID, id) - } - cleanMetricsIDs(actual.Metrics) - require.Equal(t, expected, actual) - }) - - t.Run(driverName+" - create and retrieve all playbooks", func(t *testing.T) { - var inserted []app.Playbook - for _, p := range playbooks { - id, err := playbookStore.Create(p) - require.NoError(t, err) - - tmp := p.Clone() - tmp.ID = id - inserted = append(inserted, tmp) - } - - for _, p := range inserted { - got, err := playbookStore.Get(p.ID) - cleanMetricsIDs(got.Metrics) - require.NoError(t, err) - require.Equal(t, p, got) - } - require.Equal(t, len(playbooks), len(inserted)) - }) - - t.Run(driverName+" - create but retrieve non-existing playbook", func(t *testing.T) { - _, err := playbookStore.Create(pb02) - require.NoError(t, err) - - actual, err := playbookStore.Get("nonexisting") - cleanMetricsIDs(actual.Metrics) - require.Error(t, err) - require.EqualError(t, err, "playbook does not exist for id 'nonexisting': not found") - require.Equal(t, app.Playbook{}, actual) - }) - - t.Run(driverName+" - set and retrieve playbook with no members and no checklists", func(t *testing.T) { - pb10 := NewPBBuilder(). - WithTitle("playbook 10"). - WithTeamID(team1id). - WithCreateAt(800). - ToPlaybook() - id, err := playbookStore.Create(pb10) - require.NoError(t, err) - expected := pb10.Clone() - expected.ID = id - - actual, err := playbookStore.Get(id) - require.NoError(t, err) - require.Equal(t, expected, actual) - }) -} - -func TestGetPlaybooksForTeam(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - team3id := model.NewId() - - lucy := userInfo{ - ID: model.NewId(), - Name: "Lucy", - } - - jon := userInfo{ - ID: model.NewId(), - Name: "jon", - } - - andrew := userInfo{ - ID: model.NewId(), - Name: "Andrew", - } - - matt := userInfo{ - ID: model.NewId(), - Name: "Matt", - } - - lucia := userInfo{ - ID: model.NewId(), - Name: "Lucía", - } - - bill := userInfo{ - ID: model.NewId(), - Name: "Bill", - } - - jen := userInfo{ - ID: model.NewId(), - Name: "Jen", - } - - desmond := userInfo{ - ID: model.NewId(), - Name: "Desmond", - } - - jack := userInfo{ - ID: model.NewId(), - Name: "Jack", - } - - users := []userInfo{jon, andrew, matt, lucia, bill, jen, desmond, jack} - - pb01 := NewPBBuilder(). - WithTitle("playbook 01"). - WithDescription("this is a description, not very long, but it can be up to 4096 bytes"). - WithTeamID(team1id). - WithCreateAt(500). - WithUpdateAt(0). - WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, andrew, matt}). - WithMetrics([]string{"name4", "name1", "name3"}). - ToPlaybook() - - pb02 := NewPBBuilder(). - WithTitle("playbook 02"). - WithTeamID(team1id). - WithCreateAt(600). - WithUpdateAt(0). - WithCreatePublic(true). - WithChecklists([]int{1, 4, 6, 7, 1}). // 19 - WithMembers([]userInfo{andrew, matt}). - WithMetrics([]string{"name4", "name1", "name3", "name5"}). - ToPlaybook() - - pb03 := NewPBBuilder(). - WithTitle("playbook 03"). - WithTeamID(team1id). - WithChecklists([]int{1, 2, 3}). - WithCreateAt(700). - WithUpdateAt(0). - WithMembers([]userInfo{jon, matt, lucia}). - WithMetrics([]string{"name3"}). - ToPlaybook() - - pb04 := NewPBBuilder(). - WithTitle("playbook 04"). - WithDescription("this is a description, not very long, but it can be up to 2048 bytes"). - WithTeamID(team1id). - WithCreateAt(800). - WithUpdateAt(0). - WithChecklists([]int{20}). - WithMembers([]userInfo{matt}). - ToPlaybook() - - pb05 := NewPBBuilder(). - WithTitle("playbook 05"). - WithTeamID(team2id). - WithCreateAt(1000). - WithUpdateAt(0). - WithChecklists([]int{1}). - WithMembers([]userInfo{jon, andrew}). - ToPlaybook() - - pb06 := NewPBBuilder(). - WithTitle("playbook 06"). - WithTeamID(team2id). - WithCreateAt(1100). - WithUpdateAt(0). - WithChecklists([]int{1, 2, 3}). - WithMembers([]userInfo{matt}). - ToPlaybook() - - pb07 := NewPBBuilder(). - WithTitle("playbook 07"). - WithTeamID(team3id). - WithCreateAt(1200). - WithUpdateAt(0). - WithChecklists([]int{1}). - WithMembers([]userInfo{andrew}). - ToPlaybook() - - pb08 := NewPBBuilder(). - WithTitle("playbook 008 -- so many members, but should have Desmond and Lucy"). - WithTeamID(team3id). - WithCreateAt(1300). - WithUpdateAt(0). - WithChecklists([]int{1}). - WithMembers(append(multipleUserInfo(100), desmond, lucia)). - WithChannelNameTemplate("playbook YY"). - ToPlaybook() - - pb09 := NewPBBuilder(). - WithTitle("playbook 09 -- all access"). - WithTeamID(team3id). - WithCreateAt(1600). - WithUpdateAt(0). - WithChecklists([]int{1}). - WithMembers([]userInfo{}). - WithChannelNameTemplate("playbook XX"). - ToPlaybook() - - pb10 := NewPBBuilder(). - WithTitle("playbook 10"). - WithDescription("I'm archived"). - WithTeamID(team1id). - WithCreateAt(1700). - WithUpdateAt(0). - WithDeleteAt(1701). - WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, andrew, matt}). - ToPlaybook() - - pb11 := NewPBBuilder(). - WithTitle("playbook 11"). - WithTeamID(team1id). - WithCreateAt(1800). - WithUpdateAt(0). - WithCreatePublicPlaybook(true). - WithMembers([]userInfo{jack}). - ToPlaybook() - - playbooks := []app.Playbook{pb01, pb02, pb03, pb04, pb05, pb06, pb07, pb08, pb09, pb10, pb11} - - createPlaybooks := func(store app.PlaybookStore) { - t.Helper() - - for _, p := range playbooks { - _, err := store.Create(p) - require.NoError(t, err) - } - } - - tests := []struct { - name string - teamID string - requesterInfo app.RequesterInfo - options app.PlaybookFilterOptions - expected app.GetPlaybooksResults - expectedErr error - }{ - { - name: "team1 from Andrew", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: andrew.ID, - TeamID: team1id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 3, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb02, pb11}, - }, - expectedErr: nil, - }, - { - name: "team1 from Andrew, with archived", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: andrew.ID, - TeamID: team1id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByCreateAt, - Page: 0, - PerPage: 1000, - WithArchived: true, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb02, pb10, pb11}, - }, - expectedErr: nil, - }, - { - name: "team1 from jon", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: jon.ID, - TeamID: team1id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 3, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb03, pb11}, - }, - expectedErr: nil, - }, - { - name: "team1 from jon title desc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: jon.ID, - TeamID: team1id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Direction: app.DirectionDesc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 3, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb11, pb03, pb01}, - }, - expectedErr: nil, - }, - { - name: "team1 from jon sort by stages desc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: jon.ID, - TeamID: team1id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByStages, - Direction: app.DirectionDesc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 3, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb03, pb01, pb11}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin, no special access", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 2, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb03, pb11}, - }, - expectedErr: nil, - }, - /*{ - name: "team1 from Admin", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb02, pb03, pb04}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin, member only", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 1, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb03}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by steps desc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortBySteps, - Direction: app.DirectionDesc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb04, pb02, pb03, pb01}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by title desc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Direction: app.DirectionDesc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb04, pb03, pb02, pb01}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by steps, default is asc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortBySteps, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb03, pb02, pb04}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by steps, specify asc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortBySteps, - Direction: app.DirectionAsc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb03, pb02, pb04}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by steps, desc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortBySteps, - Direction: app.DirectionDesc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb04, pb02, pb03, pb01}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by stages", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByStages, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb04, pb01, pb03, pb02}, - }, - expectedErr: nil, - }, - { - name: "team1 from Admin sort by stages, desc", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByStages, - Direction: app.DirectionDesc, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb02, pb03, pb01, pb04}, - }, - expectedErr: nil, - },*/ - { - name: "team2 from Matt", - teamID: team2id, - requesterInfo: app.RequesterInfo{ - UserID: matt.ID, - TeamID: team2id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 1, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb06}, - }, - expectedErr: nil, - }, - { - name: "team3 from Andrew (not on team)", - teamID: team3id, - requesterInfo: app.RequesterInfo{ - UserID: andrew.ID, - TeamID: team3id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 0, - PageCount: 0, - HasMore: false, - Items: []app.Playbook{}, - }, - expectedErr: nil, - }, - /*{ - name: "team3 from Admin", - teamID: team3id, - requesterInfo: app.RequesterInfo{ - UserID: lucia.ID, - IsAdmin: true, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 2, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb07, pb08}, - }, - expectedErr: nil, - },*/ - { - name: "team3 from Desmond - testing many members", - teamID: team3id, - requesterInfo: app.RequesterInfo{ - UserID: desmond.ID, - TeamID: team3id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 0, - PageCount: 0, - HasMore: false, - Items: []app.Playbook{}, - }, - expectedErr: nil, - }, - { - name: "none found", - teamID: "not-existing", - expected: app.GetPlaybooksResults{ - TotalCount: 0, - PageCount: 0, - HasMore: false, - Items: nil, - }, - expectedErr: nil, - }, - { - name: "all teams from Andrew", - teamID: "", - requesterInfo: app.RequesterInfo{ - UserID: andrew.ID, - TeamID: "", - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 4, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb02, pb05, pb11}, - }, - expectedErr: nil, - }, - { - name: "playbooks Andrew is member of", - teamID: team1id, - requesterInfo: app.RequesterInfo{ - UserID: andrew.ID, - TeamID: team1id, - }, - options: app.PlaybookFilterOptions{ - Sort: app.SortByTitle, - Page: 0, - PerPage: 1000, - WithMembershipOnly: true, - }, - expected: app.GetPlaybooksResults{ - TotalCount: 2, - PageCount: 1, - HasMore: false, - Items: []app.Playbook{pb01, pb02}, - }, - expectedErr: nil, - }, - } - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run(driverName+" - zero playbooks", func(t *testing.T) { - result, err := playbookStore.GetPlaybooks() - require.NoError(t, err) - require.ElementsMatch(t, []app.Playbook{}, result) - }) - - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - addUsers(t, store, users) - addUsersToTeam(t, store, users, team1id) - addUsersToTeam(t, store, users, team2id) - makeAdmin(t, store, lucy) - - createPlaybooks(playbookStore) - - for _, testCase := range tests { - t.Run(driverName+" - "+testCase.name, func(t *testing.T) { - actual, err := playbookStore.GetPlaybooksForTeam(testCase.requesterInfo, testCase.teamID, testCase.options) - - if testCase.expectedErr != nil { - require.Nil(t, actual) - require.Error(t, err) - require.Equal(t, testCase.expectedErr.Error(), err.Error()) - - return - } - - require.NoError(t, err) - - for i, p := range actual.Items { - require.True(t, model.IsValidId(p.ID)) - actual.Items[i].ID = "" - cleanMetricsIDs(actual.Items[i].Metrics) - require.Equal(t, p.Metrics, testCase.expected.Items[i].Metrics) - } - - // remove the checklists and members from the expected playbooks--we don't return them in getPlaybooks - for i := range testCase.expected.Items { - testCase.expected.Items[i].Checklists = nil - testCase.expected.Items[i].Members = nil - } - - require.ElementsMatch(t, justTitles(testCase.expected.Items), justTitles(actual.Items)) - require.Equal(t, testCase.expected.HasMore, actual.HasMore) - require.Equal(t, testCase.expected.PageCount, actual.PageCount) - require.Equal(t, testCase.expected.TotalCount, actual.TotalCount) - require.Len(t, actual.Items, len(testCase.expected.Items)) - }) - } -} - -func justTitles(playbooks []app.Playbook) []string { - titles := []string{} - for _, pb := range playbooks { - titles = append(titles, pb.Title) - } - - return titles -} - -func TestUpdatePlaybook(t *testing.T) { - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - - alice := userInfo{ - ID: model.NewId(), - Name: "alice", - } - - jon := userInfo{ - ID: model.NewId(), - Name: "jon", - } - - andrew := userInfo{ - ID: model.NewId(), - Name: "Andrew", - } - - matt := userInfo{ - ID: model.NewId(), - Name: "Matt", - } - - bill := userInfo{ - ID: model.NewId(), - Name: "Bill", - } - - jen := userInfo{ - ID: model.NewId(), - Name: "Jen", - } - - db := setupTestDB(t) - playbookStore := setupPlaybookStore(t, db) - - tests := []struct { - name string - playbook app.Playbook - update func(app.Playbook) app.Playbook - expectedErr error - clean func(app.Playbook, app.Playbook) (app.Playbook, app.Playbook) - }{ - { - name: "id should not be empty", - playbook: NewPBBuilder().ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - return app.Playbook{} - }, - expectedErr: errors.New("id should not be empty"), - }, - { - name: "Playbook run /can/ contain checklists with no items", - playbook: NewPBBuilder().WithChecklists([]int{1}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Checklists[0].Items = nil - old.NumSteps = 0 - return old - }, - expectedErr: nil, - }, - { - name: "playbook now public", - playbook: NewPBBuilder().WithChecklists([]int{1}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.CreatePublicPlaybookRun = true - return old - }, - expectedErr: nil, - }, - { - name: "playbook new title", - playbook: NewPBBuilder().WithChecklists([]int{1}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Title = "new title" - return old - }, - expectedErr: nil, - }, - { - name: "playbook new description", - playbook: NewPBBuilder().WithDescription("original description"). - WithChecklists([]int{1}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Description = "new description" - return old - }, - expectedErr: nil, - }, - { - name: "delete playbook", - playbook: NewPBBuilder().WithChecklists([]int{1}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.DeleteAt = model.GetMillis() - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 2 checklists, update the checklists a bit", - playbook: NewPBBuilder().WithChecklists([]int{1, 2}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Checklists[0].Items[0].State = app.ChecklistItemStateClosed - old.Checklists[1].Items[1].Title = "new title" - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 3 checklists, update to 0", - playbook: NewPBBuilder().WithChecklists([]int{1, 2, 5}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Checklists = []app.Checklist{} - old.NumSteps = 0 - old.NumStages = 0 - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 2 members, go to 1", - playbook: NewPBBuilder().WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, andrew}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Members = membersFromIds([]string{andrew.ID}) - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 3 members, go to 4 with different members", - playbook: NewPBBuilder().WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, andrew, bob}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - oldMembers := []string{matt.ID, bill.ID, alice.ID, jen.ID} - sort.Strings(oldMembers) - old.Members = membersFromIds(oldMembers) - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 3 members, go to 4 with one different member", - playbook: NewPBBuilder().WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, andrew, bob}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - oldMembers := []string{jon.ID, andrew.ID, bob.ID, alice.ID} - sort.Strings(oldMembers) - old.Members = membersFromIds(oldMembers) - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 0 members, go to 2", - playbook: NewPBBuilder().WithChecklists([]int{1, 2}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - oldMembers := []string{alice.ID, jen.ID} - sort.Strings(oldMembers) - old.Members = membersFromIds(oldMembers) - return old - }, - expectedErr: nil, - }, - { - name: "Playbook run with 5 members, go to 0", - playbook: NewPBBuilder(). - WithChecklists([]int{1, 2}). - WithMembers([]userInfo{ - jon, - andrew, - {model.NewId(), "j1"}, - {model.NewId(), "j2"}, - {model.NewId(), "j3"}, - }). - ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old.Members = nil - return old - }, - expectedErr: nil, - }, - { - name: "playbook with 0 metrics, go to 3", - playbook: NewPBBuilder().ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old, err := playbookStore.Get(old.ID) - require.NoError(t, err) - old.Title = "new title" - old.Metrics = metricsFromNames([]string{"name3", "name1", "name2"}) - return old - }, - expectedErr: nil, - clean: func(old, updated app.Playbook) (app.Playbook, app.Playbook) { - cleanMetricsIDs(updated.Metrics) - return old, updated - }, - }, - { - name: "playbook with 4 metrics, go to 0", - playbook: NewPBBuilder().WithMetrics([]string{"name3", "name1", "name2", "name4"}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old, err := playbookStore.Get(old.ID) - require.NoError(t, err) - old.Title = "new title" - old.Metrics = nil - return old - }, - expectedErr: nil, - }, - { - name: "playbook with 4 metrics, go to 3 and reorder", - playbook: NewPBBuilder().WithMetrics([]string{"name3", "name1", "name2", "name4"}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old, err := playbookStore.Get(old.ID) - require.NoError(t, err) - - old.Title = "new title" - old.Metrics = old.Metrics[1:] - old.Metrics[0], old.Metrics[1] = old.Metrics[1], old.Metrics[0] - return old - }, - expectedErr: nil, - }, - { - name: "playbook with 4 metrics, go to 3. reorder and replacement", - playbook: NewPBBuilder().WithMetrics([]string{"name3", "name1", "name2", "name4"}).ToPlaybook(), - update: func(old app.Playbook) app.Playbook { - old, err := playbookStore.Get(old.ID) - require.NoError(t, err) - - old.Title = "new title" - old.Metrics = old.Metrics[2:] - old.Metrics[0], old.Metrics[1] = old.Metrics[1], old.Metrics[0] - old.Metrics = append(old.Metrics, metricsFromNames([]string{"name10"})...) - return old - }, - expectedErr: nil, - clean: func(old, updated app.Playbook) (app.Playbook, app.Playbook) { - cleanMetricsIDs(old.Metrics) - cleanMetricsIDs(updated.Metrics) - return old, updated - }, - }, - } - - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - returned, err := playbookStore.Create(testCase.playbook) - testCase.playbook.ID = returned - require.NoError(t, err) - expected := testCase.update(testCase.playbook) - - err = playbookStore.Update(expected) - - if testCase.expectedErr != nil { - require.Error(t, err) - require.EqualError(t, err, testCase.expectedErr.Error()) - return - } - - require.NoError(t, err) - - actual, err := playbookStore.Get(expected.ID) - require.NoError(t, err) - if testCase.clean != nil { - expected, actual = testCase.clean(expected, actual) - } - require.Equal(t, expected, actual) - }) - } -} - -func TestDeletePlaybook(t *testing.T) { - team1id := model.NewId() - - andrew := userInfo{ - ID: model.NewId(), - Name: "Andrew", - } - - matt := userInfo{ - ID: model.NewId(), - Name: "Matt", - } - - pb02 := NewPBBuilder(). - WithTitle("playbook 2"). - WithTeamID(team1id). - WithCreateAt(600). - WithCreatePublic(true). - WithChecklists([]int{1, 4, 6, 7, 1}). // 19 - WithMembers([]userInfo{andrew, matt}). - WithMetrics([]string{"name1", "name2"}). - ToPlaybook() - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run(driverName+" - id empty", func(t *testing.T) { - err := playbookStore.Archive("") - require.Error(t, err) - require.EqualError(t, err, "ID cannot be empty") - }) - - t.Run(driverName+" - create and delete playbook", func(t *testing.T) { - now := model.GetMillis() - - id, err := playbookStore.Create(pb02) - require.NoError(t, err) - expected := pb02.Clone() - expected.ID = id - - err = playbookStore.Archive(id) - require.NoError(t, err) - - actual, err := playbookStore.Get(id) - require.NoError(t, err) - require.GreaterOrEqual(t, actual.DeleteAt, now) - - expected.DeleteAt = actual.DeleteAt - cleanMetricsIDs(actual.Metrics) - require.Equal(t, expected, actual) - }) -} - -func TestGetPlaybooksForKeywords(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - team3id := model.NewId() - - pb01 := NewPBBuilder(). - WithTitle("playbook 1"). - WithTeamID(team1id). - WithKeywords([]string{"one", "two", "three"}). - ToPlaybook() - - pb02 := NewPBBuilder(). - WithTitle("playbook 2"). - WithTeamID(team1id). - WithKeywords([]string{"one", "two"}). - ToPlaybook() - - pb03 := NewPBBuilder(). - WithTitle("playbook 3"). - WithTeamID(team1id). - WithKeywords([]string{"one"}). - ToPlaybook() - - pb04 := NewPBBuilder(). - WithTitle("playbook 4"). - WithTeamID(team1id). - ToPlaybook() - - pb05 := NewPBBuilder(). - WithTitle("playbook 5"). - WithTeamID(team2id). - ToPlaybook() - - pb06 := NewPBBuilder(). - WithTitle("playbook 6"). - WithTeamID(team2id). - ToPlaybook() - - pb07 := NewPBBuilder(). - WithTitle("playbook 7"). - WithTeamID(team3id). - ToPlaybook() - - pb08 := NewPBBuilder(). - WithTitle("playbook 8"). - WithTeamID(team3id). - ToPlaybook() - - pb09 := NewPBBuilder(). - WithTitle("playbook 9"). - WithTeamID(team3id). - WithKeywords([]string{"other", "keywords"}). - ToPlaybook() - - pb := []app.Playbook{pb01, pb02, pb03, pb04, pb05, pb06, pb07, pb08, pb09} - - createPlaybooks := func(store app.PlaybookStore) { - t.Helper() - - for _, p := range pb { - _, err := store.Create(p) - require.NoError(t, err) - } - } - - expected := []app.Playbook{pb01, pb02, pb03, pb09} - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run("zero playbooks", func(t *testing.T) { - result, err := playbookStore.GetPlaybooks() - require.NoError(t, err) - require.ElementsMatch(t, []app.Playbook{}, result) - }) - - createPlaybooks(playbookStore) - - t.Run(driverName+" - get playbooks with keywords", func(t *testing.T) { - actual, err := playbookStore.GetPlaybooksWithKeywords(app.PlaybookFilterOptions{Page: 0, PerPage: 100}) - sort.Slice(actual, func(i, j int) bool { return actual[i].Title < actual[j].Title }) - - require.NoError(t, err) - require.Len(t, actual, len(expected)) - for i := range actual { - require.Equal(t, expected[i].TeamID, actual[i].TeamID) - require.Equal(t, expected[i].Title, actual[i].Title) - require.Equal(t, expected[i].SignalAnyKeywords, actual[i].SignalAnyKeywords) - } - }) -} - -func TestGetTimeLastUpdated(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - team3id := model.NewId() - - pb01 := NewPBBuilder(). - WithTitle("playbook 1"). - WithTeamID(team1id). - WithKeywords([]string{"one", "two", "three"}). - WithUpdateAt(400). - ToPlaybook() - - pb02 := NewPBBuilder(). - WithTitle("playbook 2"). - WithTeamID(team1id). - WithUpdateAt(500). - WithKeywords([]string{"one", "two"}). - ToPlaybook() - - pb03 := NewPBBuilder(). - WithTitle("playbook 3"). - WithTeamID(team2id). - WithUpdateAt(450). - ToPlaybook() - - pb04 := NewPBBuilder(). - WithTitle("playbook 4"). - WithTeamID(team3id). - WithUpdateAt(600). - WithKeywords([]string{"one"}). - ToPlaybook() - - pb05 := NewPBBuilder(). - WithTitle("playbook 5"). - WithTeamID(team3id). - WithUpdateAt(700). - ToPlaybook() - - pb := []app.Playbook{pb01, pb02, pb03, pb04, pb05} - - createPlaybooks := func(store app.PlaybookStore) { - t.Helper() - - for _, p := range pb { - _, err := store.Create(p) - require.NoError(t, err) - } - } - - expected := int64(600) - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run("zero playbooks", func(t *testing.T) { - result, err := playbookStore.GetPlaybooks() - require.NoError(t, err) - require.ElementsMatch(t, []app.Playbook{}, result) - - lastUpdated, err := playbookStore.GetTimeLastUpdated(true) - require.NoError(t, err) - require.Equal(t, int64(0), lastUpdated) - }) - - createPlaybooks(playbookStore) - - t.Run(driverName+" - get time last updated", func(t *testing.T) { - actual, err := playbookStore.GetTimeLastUpdated(true) - - require.NoError(t, err) - require.Equal(t, expected, actual) - }) -} - -func TestGetPlaybookIDsForUser(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - team3id := model.NewId() - - lucy := userInfo{ - ID: model.NewId(), - Name: "Lucy", - } - - jon := userInfo{ - ID: model.NewId(), - Name: "jon", - } - - andrew := userInfo{ - ID: model.NewId(), - Name: "Andrew", - } - - matt := userInfo{ - ID: model.NewId(), - Name: "Matt", - } - - lucia := userInfo{ - ID: model.NewId(), - Name: "Lucía", - } - - bill := userInfo{ - ID: model.NewId(), - Name: "Bill", - } - - jen := userInfo{ - ID: model.NewId(), - Name: "Jen", - } - - desmond := userInfo{ - ID: model.NewId(), - Name: "Desmond", - } - - users := []userInfo{jon, andrew, matt, lucia, bill, jen, desmond, lucy} - - pb01 := NewPBBuilder(). - WithTeamID(team1id). - WithMembers([]userInfo{jon, andrew, matt}). - ToPlaybook() - - pb02 := NewPBBuilder(). - WithTeamID(team1id). - WithMembers([]userInfo{andrew, matt}). - ToPlaybook() - - pb03 := NewPBBuilder(). - WithTeamID(team1id). - WithMembers([]userInfo{jon, matt, lucia}). - ToPlaybook() - - pb04 := NewPBBuilder(). - WithTeamID(team1id). - WithMembers([]userInfo{matt}). - ToPlaybook() - - pb05 := NewPBBuilder(). - WithTeamID(team2id). - WithMembers([]userInfo{jon, andrew}). - ToPlaybook() - - pb06 := NewPBBuilder(). - WithTeamID(team2id). - WithMembers([]userInfo{matt}). - ToPlaybook() - - pb07 := NewPBBuilder(). - WithTeamID(team3id). - WithMembers([]userInfo{andrew}). - ToPlaybook() - - pb08 := NewPBBuilder(). - WithTeamID(team3id). - WithMembers(append(multipleUserInfo(100), desmond, lucia)). - ToPlaybook() - - pb09 := NewPBBuilder(). - WithTeamID(team3id). - WithMembers([]userInfo{}). - ToPlaybook() - - pb := []app.Playbook{pb01, pb02, pb03, pb04, pb05, pb06, pb07, pb08, pb09} - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run("zero playbooks", func(t *testing.T) { - result, err := playbookStore.GetPlaybooks() - require.NoError(t, err) - require.ElementsMatch(t, []app.Playbook{}, result) - }) - - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - addUsers(t, store, users) - - t.Helper() - - for i := range pb { - id, err := playbookStore.Create(pb[i]) - pb[i].ID = id - require.NoError(t, err) - } - - tests := []struct { - name string - teamID string - userID string - expected []string - expectedErr error - }{ - { - name: "team1 from Andrew", - teamID: team1id, - userID: andrew.ID, - expected: []string{pb[0].ID, pb[1].ID}, - expectedErr: nil, - }, - { - name: "team1 from jon", - teamID: team1id, - userID: jon.ID, - expected: []string{pb[0].ID, pb[2].ID}, - expectedErr: nil, - }, - { - name: "team1 from lucia", - teamID: team1id, - userID: lucia.ID, - expected: []string{pb[2].ID}, - expectedErr: nil, - }, - { - name: "team2 from Matt", - teamID: team2id, - userID: matt.ID, - expected: []string{pb[5].ID}, - expectedErr: nil, - }, - { - name: "team3 from Andrew (not on team)", - teamID: team3id, - userID: andrew.ID, - expected: []string{pb[6].ID, pb[8].ID}, - expectedErr: nil, - }, - { - name: "team3 from Lucia", - teamID: team3id, - userID: lucia.ID, - expected: []string{pb[7].ID, pb[8].ID}, - expectedErr: nil, - }, - { - name: "team3 from Desmond - testing many members", - teamID: team3id, - userID: desmond.ID, - expected: []string{pb[7].ID, pb[8].ID}, - expectedErr: nil, - }, - { - name: "none found", - teamID: "not-existing", - userID: matt.ID, - expected: []string{}, - expectedErr: nil, - }, - { - name: "team3 from Matt", - teamID: team3id, - userID: matt.ID, - expected: []string{pb[8].ID}, - expectedErr: nil, - }, - } - - for _, testCase := range tests { - t.Run(driverName+" - "+testCase.name, func(t *testing.T) { - actual, err := playbookStore.GetPlaybookIDsForUser(testCase.userID, testCase.teamID) - - if testCase.expectedErr != nil { - require.Nil(t, actual) - require.Error(t, err) - require.Equal(t, testCase.expectedErr.Error(), err.Error()) - - return - } - - require.NoError(t, err) - require.ElementsMatch(t, testCase.expected, actual) - }) - } - - for i := range pb { - pb[i].ID = "" - } -} - -func TestGetPlaybooksActiveTotal(t *testing.T) { - teamIDs := []string{model.NewId(), model.NewId(), model.NewId()} - - createPlaybooks := func(store app.PlaybookStore, num int) []string { - t.Helper() - playbooksIDs := make([]string, num) - - for i := range playbooksIDs { - pb := NewPBBuilder(). - WithTitle(fmt.Sprintf("playbook %d", i)). - WithTeamID(teamIDs[i%len(teamIDs)]). - WithUpdateAt(int64(i * 100)). - ToPlaybook() - - id, err := store.Create(pb) - require.NoError(t, err) - playbooksIDs[i] = id - } - return playbooksIDs - } - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - - t.Run("zero playbooks", func(t *testing.T) { - actual, err := playbookStore.GetPlaybooksActiveTotal() - require.NoError(t, err) - require.Equal(t, int64(0), actual) - }) - - playbooksNum := 20 - playbooksIDs := createPlaybooks(playbookStore, playbooksNum) - - t.Run(driverName+" - only active playbooks", func(t *testing.T) { - actual, err := playbookStore.GetPlaybooksActiveTotal() - - require.NoError(t, err) - require.Equal(t, int64(playbooksNum), actual) - }) - - // archive 1/3 of playbooks - for i := 0; i < playbooksNum/3; i++ { - err := playbookStore.Archive(playbooksIDs[i]) - require.NoError(t, err) - } - - t.Run(driverName+" - both active and archived playbooks", func(t *testing.T) { - actual, err := playbookStore.GetPlaybooksActiveTotal() - - require.NoError(t, err) - require.Equal(t, int64(playbooksNum-playbooksNum/3), actual) - }) -} - -// PlaybookBuilder is a utility to build playbooks with a default base. -// Use it as: -// NewBuilder.WithName("name").WithXYZ(xyz)....ToPlaybook() -type PlaybookBuilder struct { - *app.Playbook -} - -func NewPBBuilder() *PlaybookBuilder { - timeNow := model.GetMillis() - return &PlaybookBuilder{ - &app.Playbook{ - Title: "base playbook", - TeamID: model.NewId(), - CreatePublicPlaybookRun: false, - CreateAt: model.GetMillis(), - UpdateAt: timeNow, - DeleteAt: 0, - Checklists: []app.Checklist(nil), - Members: []app.PlaybookMember(nil), - InvitedUserIDs: []string(nil), - InvitedGroupIDs: []string(nil), - NumActions: 1, // Channel creation is always on - DefaultPlaybookAdminRole: app.PlaybookRoleAdmin, - DefaultPlaybookMemberRole: app.PlaybookRoleMember, - DefaultRunAdminRole: app.RunRoleAdmin, - DefaultRunMemberRole: app.RunRoleMember, - }, - } -} - -func (p *PlaybookBuilder) WithID() *PlaybookBuilder { - p.ID = model.NewId() - - return p -} - -func (p *PlaybookBuilder) WithTitle(title string) *PlaybookBuilder { - p.Title = title - - return p -} - -func (p *PlaybookBuilder) WithDescription(desc string) *PlaybookBuilder { - p.Description = desc - - return p -} - -func (p *PlaybookBuilder) WithTeamID(id string) *PlaybookBuilder { - p.TeamID = id - - return p -} - -func (p *PlaybookBuilder) WithCreatePublic(public bool) *PlaybookBuilder { - p.CreatePublicPlaybookRun = public - - return p -} - -func (p *PlaybookBuilder) WithCreatePublicPlaybook(public bool) *PlaybookBuilder { - p.Public = public - - return p -} - -func (p *PlaybookBuilder) WithCreateAt(createAt int64) *PlaybookBuilder { - p.CreateAt = createAt - - return p -} - -func (p *PlaybookBuilder) WithDeleteAt(deleteAt int64) *PlaybookBuilder { - p.DeleteAt = deleteAt - - return p -} - -func (p *PlaybookBuilder) WithChecklists(itemsPerChecklist []int) *PlaybookBuilder { - p.Checklists = make([]app.Checklist, len(itemsPerChecklist)) - - for i, numItems := range itemsPerChecklist { - var items []app.ChecklistItem - for j := 0; j < numItems; j++ { - items = append(items, app.ChecklistItem{ - ID: model.NewId(), - Title: fmt.Sprint("Checklist ", i, " - item ", j), - }) - } - - p.Checklists[i] = app.Checklist{ - ID: model.NewId(), - Title: fmt.Sprint("Checklist ", i), - Items: items, - } - } - - p.NumStages = int64(len(itemsPerChecklist)) - p.NumSteps = sum(itemsPerChecklist) - - return p -} - -func (p *PlaybookBuilder) WithChannelNameTemplate(channelNameTemplate string) *PlaybookBuilder { - p.ChannelNameTemplate = channelNameTemplate - - return p -} - -func sum(nums []int) int64 { - ret := 0 - for _, n := range nums { - ret += n - } - return int64(ret) -} - -func (p *PlaybookBuilder) WithMembers(members []userInfo) *PlaybookBuilder { - memberIDs := make([]string, len(members)) - - for i, member := range members { - memberIDs[i] = member.ID - } - sort.Strings(memberIDs) - - p.Members = membersFromIds(memberIDs) - - if len(members) == 0 { - p.Public = true - } - - return p -} - -func (p *PlaybookBuilder) WithKeywords(keywords []string) *PlaybookBuilder { - p.SignalAnyKeywordsEnabled = true - p.SignalAnyKeywords = keywords - p.NumActions++ - - return p -} - -func (p *PlaybookBuilder) WithUpdateAt(updateAt int64) *PlaybookBuilder { - p.UpdateAt = updateAt - - return p -} - -func (p *PlaybookBuilder) WithMetrics(names []string) *PlaybookBuilder { - if len(names) == 0 { - return p - } - p.Metrics = metricsFromNames(names) - - return p -} - -func (p *PlaybookBuilder) ToPlaybook() app.Playbook { - return *p.Playbook -} - -func setupPlaybookStore(t *testing.T, db *sqlx.DB) app.PlaybookStore { - mockCtrl := gomock.NewController(t) - - kvAPI := mock_sqlstore.NewMockKVAPI(mockCtrl) - configAPI := mock_sqlstore.NewMockConfigurationAPI(mockCtrl) - pluginAPIClient := PluginAPIClient{ - KV: kvAPI, - Configuration: configAPI, - } - - sqlStore := setupSQLStore(t, db) - - return NewPlaybookStore(pluginAPIClient, sqlStore) -} - -func TestGetTopPlaybooks(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - - jon := userInfo{ - ID: model.NewId(), - Name: "jon", - } - - matt := userInfo{ - ID: model.NewId(), - Name: "Matt", - } - - desmond := userInfo{ - ID: model.NewId(), - Name: "Desmond", - } - - pb01 := NewPBBuilder(). - WithTitle("playbook 1"). - WithDescription("this is a description, not very long, but it can be up to 4096 bytes"). - WithTeamID(team1id). - WithCreateAt(500). - WithChecklists([]int{1, 2}). - WithMembers([]userInfo{jon, matt, desmond}). - ToPlaybook() - - pb02 := NewPBBuilder(). - WithTitle("playbook 2"). - WithTeamID(team1id). - WithCreateAt(600). - WithChecklists([]int{1, 4, 6, 7, 1}). // 19 - WithMembers([]userInfo{matt}). - WithMetrics([]string{"name11", "name12"}). - ToPlaybook() - - pb03 := NewPBBuilder(). - WithTitle("playbook 3"). - WithTeamID(team2id). - WithChecklists([]int{1, 2, 3}). - WithCreateAt(700). - WithMembers([]userInfo{jon, matt}). - WithMetrics([]string{"name14", "name12", "name13", "name11"}). - ToPlaybook() - - pb04 := NewPBBuilder(). - WithTitle("playbook 4"). - WithDescription("this is a description, not very long, but it can be up to 2048 bytes"). - WithTeamID(team1id). - WithCreateAt(800). - WithChecklists([]int{20}). - WithMembers([]userInfo{matt}). - WithCreatePublicPlaybook(true). - WithMetrics([]string{"name17"}). - ToPlaybook() - - playbooks := []app.Playbook{pb01, pb02, pb03, pb04} - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - playbookRunStore := setupPlaybookRunStore(t, db) - - // create playbooks - createPlaybooks := func(store app.PlaybookStore) { - t.Helper() - - for index, p := range playbooks { - p.ID = "" - playbookCreatedID, err := store.Create(p) - playbooks[index].ID = playbookCreatedID - require.NoError(t, err) - } - } - createPlaybooks(playbookStore) - playbookRuns := []*app.PlaybookRun{ - NewBuilder(t).WithName("pb01-0").WithPlaybookID(playbooks[0].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb01-1").WithPlaybookID(playbooks[0].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb01-2").WithPlaybookID(playbooks[0].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb01-3").WithPlaybookID(playbooks[0].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb02-0").WithPlaybookID(playbooks[1].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb02-1").WithPlaybookID(playbooks[1].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb02-2").WithPlaybookID(playbooks[1].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb02-3").WithPlaybookID(playbooks[1].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb02-4").WithPlaybookID(playbooks[1].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb02-5").WithPlaybookID(playbooks[1].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb03-0").WithPlaybookID(playbooks[2].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb03-1").WithPlaybookID(playbooks[2].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb03-2").WithPlaybookID(playbooks[2].ID).WithCreateAt(200).ToPlaybookRun(), - NewBuilder(t).WithName("pb04-0").WithPlaybookID(playbooks[3].ID).WithCreateAt(400).ToPlaybookRun(), - NewBuilder(t).WithName("pb04-1").WithPlaybookID(playbooks[3].ID).WithCreateAt(400).ToPlaybookRun(), - } - // create playbook runs - for _, playbookRun := range playbookRuns { - _, err := playbookRunStore.CreatePlaybookRun(playbookRun) - require.NoError(t, err) - } - - t.Run(driverName+" - get top team playbooks", func(t *testing.T) { - // for jon - topPlaybooks, err := playbookStore.GetTopPlaybooksForTeam(team1id, jon.ID, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 100}) - require.NoError(t, err) - // should get top playbooks as pb01.ID, and pb04.ID - // implicitly means there's no playbooks from other team - require.Len(t, topPlaybooks.Items, 2) - - require.Equal(t, topPlaybooks.Items[0].NumRuns, 4) - require.Equal(t, topPlaybooks.Items[0].PlaybookID, playbooks[0].ID) - require.Equal(t, topPlaybooks.Items[1].NumRuns, 2) - require.Equal(t, topPlaybooks.Items[1].PlaybookID, playbooks[3].ID) - - // for matt - topPlaybooks, err = playbookStore.GetTopPlaybooksForTeam(team1id, matt.ID, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 100}) - require.NoError(t, err) - // should get top playbooks as pb01.ID, pb02.ID and pb04.ID - // implicitly means there's no playbooks from other team - require.Len(t, topPlaybooks.Items, 3) - require.Equal(t, topPlaybooks.Items[0].NumRuns, 6) - require.Equal(t, topPlaybooks.Items[0].PlaybookID, playbooks[1].ID) - require.Equal(t, topPlaybooks.Items[1].NumRuns, 4) - require.Equal(t, topPlaybooks.Items[1].PlaybookID, playbooks[0].ID) - require.Equal(t, topPlaybooks.Items[2].NumRuns, 2) - require.Equal(t, topPlaybooks.Items[2].PlaybookID, playbooks[3].ID) - }) - - t.Run(driverName+" - get top user playbooks", func(t *testing.T) { - // for jon - topPlaybooks, err := playbookStore.GetTopPlaybooksForUser(team1id, jon.ID, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 100}) - require.NoError(t, err) - // should get top playbooks as pb01.ID, and pb04.ID - // implicitly means there's no playbooks from other team - require.Len(t, topPlaybooks.Items, 1) - // fmt.Println(topPlaybooks.Items) - require.Equal(t, topPlaybooks.Items[0].NumRuns, 4) - require.Equal(t, topPlaybooks.Items[0].PlaybookID, playbooks[0].ID) - - // for team 2 - topPlaybooks, err = playbookStore.GetTopPlaybooksForUser(team2id, jon.ID, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 100}) - require.NoError(t, err) - // should get top playbooks as pb01.ID, and pb04.ID - // implicitly means there's no playbooks from other team - require.Len(t, topPlaybooks.Items, 1) - // fmt.Println(topPlaybooks.Items) - require.Equal(t, topPlaybooks.Items[0].NumRuns, 3) - require.Equal(t, topPlaybooks.Items[0].PlaybookID, playbooks[2].ID) - - // for matt - topPlaybooks, err = playbookStore.GetTopPlaybooksForUser(team1id, matt.ID, &model.InsightsOpts{StartUnixMilli: 0, Page: 0, PerPage: 100}) - require.NoError(t, err) - // should get top playbooks as pb01.ID, and pb04.ID - // implicitly means there's no playbooks from other team - require.Len(t, topPlaybooks.Items, 3) - require.Equal(t, topPlaybooks.Items[0].NumRuns, 6) - require.Equal(t, topPlaybooks.Items[0].PlaybookID, playbooks[1].ID) - require.Equal(t, topPlaybooks.Items[1].NumRuns, 4) - require.Equal(t, topPlaybooks.Items[1].PlaybookID, playbooks[0].ID) - require.Equal(t, topPlaybooks.Items[2].NumRuns, 2) - require.Equal(t, topPlaybooks.Items[2].PlaybookID, playbooks[3].ID) - }) -} diff --git a/server/playbooks/server/sqlstore/pluginapi_client.go b/server/playbooks/server/sqlstore/pluginapi_client.go deleted file mode 100644 index ca17f72fb8b..00000000000 --- a/server/playbooks/server/sqlstore/pluginapi_client.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/playbooks" - - "github.com/mattermost/mattermost/server/public/model" -) - -// StoreAPI is the interface exposing the underlying database, provided by pluginapi -// It is implemented by mattermost-plugin-api/Client.Store, or by the mock StoreAPI. -type StoreAPI interface { - GetMasterDB() (*sql.DB, error) - DriverName() string -} - -// KVAPI is the key value store interface for the pluginkv stores. -// It is implemented by mattermost-plugin-api/Client.KV, or by the mock KVAPI. -type KVAPI interface { - Get(key string, out interface{}) error -} - -type ConfigurationAPI interface { - GetConfig() *model.Config -} - -// PluginAPIClient is the struct combining the interfaces defined above, which is everything -// from pluginapi that the store currently uses. -type PluginAPIClient struct { - Store StoreAPI - KV KVAPI - Configuration ConfigurationAPI -} - -// NewClient receives a pluginapi.Client and returns the PluginAPIClient, which is what the -// store will use to access pluginapi.Client. -func NewClient(serviceAdapter playbooks.ServicesAPI) PluginAPIClient { - return PluginAPIClient{ - Store: serviceAdapter, - KV: serviceAdapter, - Configuration: serviceAdapter, - } -} diff --git a/server/playbooks/server/sqlstore/stats.go b/server/playbooks/server/sqlstore/stats.go deleted file mode 100644 index 4544b4c7c28..00000000000 --- a/server/playbooks/server/sqlstore/stats.go +++ /dev/null @@ -1,614 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "fmt" - "math" - "reflect" - "strconv" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "gopkg.in/guregu/null.v4" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/mattermost/server/public/model" -) - -type StatsStore struct { - pluginAPI PluginAPIClient - store *SQLStore -} - -func NewStatsStore(pluginAPI PluginAPIClient, sqlStore *SQLStore) *StatsStore { - return &StatsStore{ - pluginAPI: pluginAPI, - store: sqlStore, - } -} - -type StatsFilters struct { - TeamID string - PlaybookID string -} - -func applyFilters(query sq.SelectBuilder, filters *StatsFilters) sq.SelectBuilder { - ret := query - - if filters.TeamID != "" { - ret = ret.Where(sq.Eq{"i.TeamID": filters.TeamID}) - } - if filters.PlaybookID != "" { - ret = ret.Where(sq.Eq{"i.PlaybookID": filters.PlaybookID}) - } - - return ret -} - -func (s *StatsStore) TotalInProgressPlaybookRuns(filters *StatsFilters) int { - query := s.store.builder. - Select("COUNT(i.ID)"). - From("IR_Incident as i"). - Where("i.EndAt = 0") - - query = applyFilters(query, filters) - - var total int - if err := s.store.getBuilder(s.store.db, &total, query); err != nil { - logrus.WithError(err).Error("failed to query total in progress playbook runs") - return -1 - } - - return total -} - -// TotalPlaybooks returns the number of playbooks in the server -func (s *StatsStore) TotalPlaybooks() (int, error) { - query := s.store.builder. - Select("COUNT(p.ID)"). - From("IR_Playbook as p") - - var total int - if err := s.store.getBuilder(s.store.db, &total, query); err != nil { - return 0, errors.Wrap(err, "Error retrieving total playbooks stat") - } - - return total, nil -} - -// TotalPlaybookRuns returns the number of playbook runs in the server -func (s *StatsStore) TotalPlaybookRuns() (int, error) { - query := s.store.builder. - Select("COUNT(i.ID)"). - From("IR_Incident as i") - - var total int - if err := s.store.getBuilder(s.store.db, &total, query); err != nil { - return 0, errors.Wrap(err, "Error retrieving total runs stat") - } - - return total, nil -} - -func (s *StatsStore) TotalActiveParticipants(filters *StatsFilters) int { - query := s.store.builder. - Select("COUNT(DISTINCT rp.UserId)"). - From("IR_Run_Participants as rp"). - Join("IR_Incident AS i ON i.ID = rp.IncidentID"). - Where("i.EndAt = 0"). - Where("rp.IsParticipant = true"). - Where(sq.Expr("rp.UserId NOT IN (SELECT UserId FROM Bots)")) - - query = applyFilters(query, filters) - - var total int - if err := s.store.getBuilder(s.store.db, &total, query); err != nil { - logrus.WithError(err).Error("failed to query total active participants") - return -1 - } - - return total -} - -// RunsFinishedBetweenDays are calculated from startDay to endDay (inclusive), where "days" -// are "number of days ago". E.g., for the last 30 days, begin day would be 30 (days ago), end day -// would be 0 (days ago) (up until now). -func (s *StatsStore) RunsFinishedBetweenDays(filters *StatsFilters, startDay, endDay int) int { - dayInMS := int64(86400000) - startInMS := beginningOfTodayMillis() - int64(startDay)*dayInMS - endInMS := endOfTodayMillis() - int64(endDay)*dayInMS - - query := s.store.builder. - Select("COUNT(i.Id) as Count"). - From("IR_Incident as i"). - Where(sq.And{ - sq.Expr("i.EndAt > ?", startInMS), - sq.Expr("i.EndAt <= ?", endInMS), - }) - query = applyFilters(query, filters) - - var total int - if err := s.store.getBuilder(s.store.db, &total, query); err != nil { - logrus.WithError(err).Error("failed to query runs finished between days") - return -1 - } - - return total -} - -// Not efficient. One query per day. -func (s *StatsStore) MovingWindowQueryActive(query sq.SelectBuilder, numDays int) ([]int, error) { - now := model.GetMillis() - dayInMS := int64(86400000) - - results := []int{} - for i := 0; i < numDays; i++ { - modifiedQuery := query.Where( - sq.Expr( - `i.CreateAt < ? AND (i.EndAt > ? OR i.EndAt = 0)`, - now-(int64(i)*dayInMS), - now-(int64(i+1)*dayInMS), - ), - ) - - var value int - if err := s.store.getBuilder(s.store.db, &value, modifiedQuery); err != nil { - return nil, err - } - - results = append(results, value) - } - - return results, nil -} - -// RunsStartedPerWeekLastXWeeks returns the number of runs started each week for the last X weeks. -// Returns data in order of oldest week to most recent week. -func (s *StatsStore) RunsStartedPerWeekLastXWeeks(x int, filters *StatsFilters) ([]int, [][]int64) { - day := int64(86400000) - week := day * 7 - startOfWeek := beginningOfLastSundayMillis() - endOfWeek := startOfWeek + week - 1 - var weeksStartAndEnd [][]int64 - - q := s.store.builder.Select() - for i := 0; i < x; i++ { - if s.store.db.DriverName() == model.DatabaseDriverMysql { - q = q.Column(` - CAST( - COALESCE( - SUM( - CASE - WHEN i.CreateAt >= ? AND i.CreateAt < ? - THEN 1 - ELSE 0 - END) - , 0) - AS UNSIGNED) - `, startOfWeek, endOfWeek) - } else { - q = q.Column(` - COALESCE( - SUM(CASE - WHEN i.CreateAt >= ? AND i.CreateAt < ? - THEN 1 - ELSE 0 - END) - , 0) - `, startOfWeek, endOfWeek) - } - - weeksStartAndEnd = append(weeksStartAndEnd, []int64{startOfWeek, endOfWeek}) - - endOfWeek -= week - startOfWeek -= week - } - - q = q.From("IR_Incident as i") - q = applyFilters(q, filters) - - counts, err := s.performQueryForXCols(q, x) - if err != nil { - logrus.WithError(err).WithField("x", x).Error("failed to query runs started per week last x weeks") - return []int{}, [][]int64{} - } - - reverseSlice(counts) - reverseSlice(weeksStartAndEnd) - - return counts, weeksStartAndEnd -} - -// ActiveRunsPerDayLastXDays returns the number of active runs per day for the last X days. -// Returns data in order of oldest day to most recent day. -func (s *StatsStore) ActiveRunsPerDayLastXDays(x int, filters *StatsFilters) ([]int, [][]int64) { - startOfDay := beginningOfTodayMillis() - endOfDay := endOfTodayMillis() - day := int64(86400000) - var daysAsStartAndEnd [][]int64 - - q := s.store.builder.Select() - for i := 0; i < x; i++ { - // a playbook run was active if it was created before the end of the day and ended after the - // start of the day (or still active) - if s.store.db.DriverName() == model.DatabaseDriverMysql { - q = q.Column(` - CAST( - COALESCE( - SUM( - CASE - WHEN (i.EndAt >= ? OR i.EndAt = 0) AND i.CreateAt < ? - THEN 1 - ELSE 0 - END) - , 0) - AS UNSIGNED) - `, startOfDay, endOfDay) - } else { - q = q.Column(` - COALESCE( - SUM(CASE - WHEN (i.EndAt >= ? OR i.EndAt = 0) AND i.CreateAt < ? - THEN 1 - ELSE 0 - END) - , 0) - `, startOfDay, endOfDay) - } - - daysAsStartAndEnd = append(daysAsStartAndEnd, []int64{startOfDay, endOfDay}) - - endOfDay -= day - startOfDay -= day - } - - q = q.From("IR_Incident as i") - q = applyFilters(q, filters) - - counts, err := s.performQueryForXCols(q, x) - if err != nil { - logrus.WithError(err).WithField("x", x).Error("failed to query active runs per day last x days") - return []int{}, [][]int64{} - } - - reverseSlice(counts) - reverseSlice(daysAsStartAndEnd) - - return counts, daysAsStartAndEnd -} - -// ActiveParticipantsPerDayLastXDays returns the number of active participants per day for the last X days. -// Returns data in order of oldest day to most recent day. -func (s *StatsStore) ActiveParticipantsPerDayLastXDays(x int, filters *StatsFilters) ([]int, [][]int64) { - startOfDay := beginningOfTodayMillis() - endOfDay := endOfTodayMillis() - day := int64(86400000) - var daysAsTimes [][]int64 - - q := s.store.builder.Select() - for i := 0; i < x; i++ { - // COUNT( DISTINCT( CASE: the CASE will return the userId if the row satisfies the conditions, - // therefore COUNT( DISTINCT will return the number of unique userIds - // - // first two lines of the WHEN: a playbook run was active if it was ended after the start of - // the day (or still active) and created before the end of the day - // - // second two lines: a user was active in the same way--if they left after the start of - // the day (or are still in the channel) and joined before the end of the day - q = q.Column(` - COALESCE( - COUNT(DISTINCT - (CASE - WHEN (i.EndAt >= ? OR i.EndAt = 0) AND - i.CreateAt < ? AND - (cmh.LeaveTime >= ? OR cmh.LeaveTime is NULL) AND - cmh.JoinTime < ? - THEN cmh.UserId - END)) - , 0) - `, startOfDay, endOfDay, startOfDay, endOfDay) - - daysAsTimes = append(daysAsTimes, []int64{startOfDay, endOfDay}) - - endOfDay -= day - startOfDay -= day - } - - q = q. - From("IR_Incident as i"). - InnerJoin("ChannelMemberHistory as cmh ON i.ChannelId = cmh.ChannelId"). - Where(sq.Expr("cmh.UserId NOT IN (SELECT UserId FROM Bots)")) - q = applyFilters(q, filters) - - counts, err := s.performQueryForXCols(q, x) - if err != nil { - logrus.WithError(err).WithField("x", x).Error("failed to query active participants per day last x days") - return []int{}, [][]int64{} - } - - reverseSlice(counts) - reverseSlice(daysAsTimes) - - return counts, daysAsTimes -} - -// MetricOverallAverage for a specific playbook returns a list that contains an average value for each metric. -// Only published metrics values are included. -// Returns empty list when Playbook doesn't have configured metrics -// If for some metrics there are no published values, the corresponding element will be nil in the resulting slice -func (s *StatsStore) MetricOverallAverage(filters StatsFilters) []null.Int { - // this query will return average values only for the metrics that have published data in the database - // so we need to add to the result array nil values for metrics that don't have data - query := s.store.builder. - Select("mc.ID as ID, FLOOR(AVG(m.Value)) as Value"). - From("IR_Metric as m"). - InnerJoin("IR_MetricConfig as mc ON m.MetricConfigID = mc.ID"). - Where(sq.Eq{"mc.PlaybookID": filters.PlaybookID}). - Where(sq.Eq{"m.Published": true}). - GroupBy("mc.ID"). - OrderBy("mc.Ordering ASC") - - type Average struct { - ID string - Value string - } - var averages []Average - if err := s.store.selectBuilder(s.store.db, &averages, query); err != nil { - logrus.WithError(err).Error("failed to query metric averages") - return []null.Int{} - } - - configs, err := s.retrieveMetricConfigs(filters.PlaybookID) - if err != nil { - logrus.WithError(err).WithField("playbook_id", filters.PlaybookID).Error("Error retrieving metrics configs ids for playbook") - return []null.Int{} - } - - // use metrics configurations to build a result array, where overallAverage[i] will be average value for - // the i-th metric or nil if there is no data in the database for this specific metric - overallAverage := make([]null.Int, len(configs)) - for i, id := range configs { - for _, av := range averages { - if av.ID == id { - val, _ := strconv.ParseInt(av.Value, 10, 64) - overallAverage[i] = null.IntFrom(val) - break - } - } - } - return overallAverage -} - -// MetricValueRange returns min and max values for each metric -// Only published metrics are included. -// If there are no configured metrics, returns an empty list -// If for some metrics there are no published values, the corresponding slice will be nil in the resulting slice -func (s *StatsStore) MetricValueRange(filters StatsFilters) [][]int64 { - type MinMax struct { - ID string - Min int64 - Max int64 - } - - // this query will return min-max values only for the metrics that have published data in the database - // so we need to add to the result array nil values for metrics that don't have data - q := s.store.builder. - Select("mc.ID as ID, MIN(Value) as Min, MAX(Value) as Max"). - From("IR_Metric as m"). - InnerJoin("IR_MetricConfig as mc ON m.MetricConfigID = mc.ID"). - Where(sq.Eq{"mc.PlaybookID": filters.PlaybookID}). - Where(sq.Eq{"m.Published": true}). - GroupBy("mc.ID"). - OrderBy("mc.Ordering ASC") - var res []MinMax - if err := s.store.selectBuilder(s.store.db, &res, q); err != nil { - logrus.WithError(err).Error("Error retrieving metric min and max values") - return [][]int64{} - } - - configs, err := s.retrieveMetricConfigs(filters.PlaybookID) - if err != nil { - logrus.WithError(err).WithField("playbook_id", filters.PlaybookID).Error("Error retrieving metrics configs ids for playbook") - return [][]int64{} - } - - // use metrics configurations to build a result array, where valueRange[i] will be min-max values for - // the i-th metric or nil if there is no data in the database for this specific metric - valueRange := make([][]int64, len(configs)) - for i, id := range configs { - for _, minMax := range res { - if minMax.ID == id { - valueRange[i] = []int64{minMax.Min, minMax.Max} - break - } - } - } - - return valueRange -} - -// MetricRollingValuesLastXRuns for each metric returns list of last `x` published values, starting from `offset` -// first element in the list is most recent. And returns the names of the last `x` runs. -// Returns empty list if Playbook doesn't have metrics. -// If for some metrics there are no published values, the corresponding slice will be nil in the resulting slice -func (s *StatsStore) MetricRollingValuesLastXRuns(x int, offset int, filters StatsFilters) ([][]int64, []string) { - logger := logrus.WithField("playbook_id", filters.PlaybookID) - - // retrieve metric configs metricsConfigsIDs for playbook - metricsConfigsIDs, err := s.retrieveMetricConfigs(filters.PlaybookID) - if err != nil { - logger.WithError(err).Error("failed to retrieve metrics configs") - return [][]int64{}, []string{} - } - - //NOTE: It would be possible to turn this into a single statement; keep in mind if the playbookStats call becomes slow - metricsValues := make([][]int64, 0) - runNames := make([]string, 0) - - for _, id := range metricsConfigsIDs { - query := s.store.builder. - Select("m.Value AS Value", "c.DisplayName AS Name"). - From("IR_Incident as i"). - Join("Channels AS c ON (c.Id = i.ChannelId)"). - InnerJoin("IR_Metric AS m ON (i.ID = m.IncidentID)"). - Where(sq.Eq{"i.PlaybookID": filters.PlaybookID}). - Where("i.RetrospectivePublishedAt > 0"). - Where(sq.Eq{"i.RetrospectiveWasCanceled": false}). - Where(sq.Eq{"m.MetricConfigID": id}). - OrderBy("i.RetrospectivePublishedAt DESC"). - Limit(uint64(x)). - Offset(uint64(offset)) - - var rows []struct { - Value int64 - Name string - } - if err := s.store.selectBuilder(s.store.db, &rows, query); err != nil { - logger.WithError(err).WithField("metric_config_id", id).Error("failed to query metrics") - return [][]int64{}, []string{} - } - - var values []int64 - var names []string - for _, r := range rows { - values = append(values, r.Value) - names = append(names, r.Name) - } - - metricsValues = append(metricsValues, values) - runNames = names // overwrites, but it'll be the same data each time -- simpler than making a separate query - } - - return metricsValues, runNames -} - -// MetricRollingAverageAndChange for each metric returns average of last `x` published values and -// change with comparison to the previous period -// returns empty list if the Playbook doesn't have metrics -// If for some metrics there are no published values, the corresponding element will be nil in the resulting slice -func (s *StatsStore) MetricRollingAverageAndChange(x int, filters StatsFilters) (metricRollingAverage []null.Int, metricRollingAverageChange []null.Int) { - metricValuesWholePeriod, _ := s.MetricRollingValuesLastXRuns(2*x, 0, filters) - - if len(metricValuesWholePeriod) == 0 { - return []null.Int{}, []null.Int{} - } - - metricRollingAverage = make([]null.Int, 0) - metricRollingAverageChange = make([]null.Int, 0) - for i, nums := range metricValuesWholePeriod { - firstPeriodEnd := int(math.Min(float64(x), float64(len(nums)))) - // add null values when there are no metric values available - if firstPeriodEnd == 0 { - metricRollingAverage = append(metricRollingAverage, null.NewInt(0, false)) - metricRollingAverageChange = append(metricRollingAverageChange, null.NewInt(0, false)) - continue - } - - metricRollingAverage = append(metricRollingAverage, null.IntFrom(getAverage(nums[:firstPeriodEnd]))) - - secondPeriodEnd := int(math.Min(float64(2*x), float64(len(nums)))) - // add null value when change can't be calculated - if firstPeriodEnd >= secondPeriodEnd || metricRollingAverage[i].IsZero() { - metricRollingAverageChange = append(metricRollingAverageChange, null.NewInt(0, false)) - continue - } - diff := metricRollingAverage[i].Int64*100/getAverage(nums[firstPeriodEnd:secondPeriodEnd]) - 100 - metricRollingAverageChange = append(metricRollingAverageChange, null.IntFrom(diff)) - } - return -} - -func (s *StatsStore) performQueryForXCols(q sq.SelectBuilder, x int) ([]int, error) { - sqlString, args, err := q.ToSql() - if err != nil { - return []int{}, errors.Wrap(err, "failed to build sql") - } - sqlString = s.store.db.Rebind(sqlString) - - rows, err := s.store.db.Queryx(sqlString, args...) - if err != nil { - return []int{}, errors.Wrap(err, "failed to get rows from Queryx") - } - - defer rows.Close() - if !rows.Next() { - return []int{}, errors.Wrap(rows.Err(), "failed to get rows.Next()") - } - - cols, err2 := rows.SliceScan() - if err2 != nil { - return []int{}, errors.Wrap(err, "failed to get SliceScan") - } - if len(cols) != x { - return []int{}, fmt.Errorf("failed to get correct length for columns, wanted %d, got %d", x, len(cols)) - } - - counts := make([]int, x) - for i := 0; i < x; i++ { - val, ok := cols[i].(int64) - if !ok { - return []int{}, fmt.Errorf("column was unexpected type, wanted int64, got: %T", cols[i]) - } - counts[i] = int(val) - } - - return counts, nil -} - -func (s *StatsStore) retrieveMetricConfigs(playbookID string) ([]string, error) { - query := s.store.builder. - Select("ID"). - From("IR_MetricConfig"). - Where(sq.Eq{"PlaybookID": playbookID}). - Where(sq.Eq{"DeleteAt": 0}). - OrderBy("Ordering ASC") - var ids []string - if err := s.store.selectBuilder(s.store.db, &ids, query); err != nil { - return nil, err - } - - return ids, nil -} - -func beginningOfTodayMillis() int64 { - year, month, day := time.Now().UTC().Date() - bod := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) - return bod.UnixNano() / int64(time.Millisecond) -} - -func endOfTodayMillis() int64 { - year, month, day := time.Now().UTC().Add(24 * time.Hour).Date() - bod := time.Date(year, month, day, 0, 0, 0, 0, time.UTC) - return bod.UnixNano()/int64(time.Millisecond) - 1 -} - -func beginningOfLastSundayMillis() int64 { - // Weekday is an iota where Sun = 0, Mon = 1, etc. So this is an offset to get back to Sun. - offset := int(time.Now().UTC().Weekday()) - now := time.Now().UTC() - startOfSunday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -offset) - return startOfSunday.UnixNano() / int64(time.Millisecond) -} - -func reverseSlice(s interface{}) { - value := reflect.ValueOf(s) - if value.Kind() != reflect.Slice { - panic(errors.New("s must be a slice type")) - } - n := reflect.ValueOf(s).Len() - swap := reflect.Swapper(s) - for i, j := 0, n-1; i < j; i, j = i+1, j-1 { - swap(i, j) - } -} - -func getAverage(nums []int64) int64 { - var sum int64 - for _, num := range nums { - sum += num - } - return sum / int64(len(nums)) -} diff --git a/server/playbooks/server/sqlstore/stats_for_test.go b/server/playbooks/server/sqlstore/stats_for_test.go deleted file mode 100644 index 493fb3fe55e..00000000000 --- a/server/playbooks/server/sqlstore/stats_for_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import "math" - -func mean(nums []int64) float64 { - if len(nums) == 0 { - return 0 - } - mean := float64(0) - for _, n := range nums { - mean += float64(n) - } - return mean / float64(len(nums)) -} - -func variance(nums []int64) float64 { - if len(nums) == 0 { - return 0 - } - - m := mean(nums) - v := float64(0) - for _, n := range nums { - v += math.Pow(float64(n)-m, 2) - } - return v / float64(len(nums)-1) -} - -func stdErr(nums []int64) float64 { - if len(nums) == 0 { - return 0 - } - - s2 := variance(nums) - s := math.Sqrt(s2) - - return s / math.Sqrt(float64(len(nums))) -} - -func ciForN30(nums []int64) (float64, float64) { - // assumes a sample size of 30 - tValue := 2.0423 - m := mean(nums) - se := stdErr(nums) - return m - tValue*se, m + tValue*se -} diff --git a/server/playbooks/server/sqlstore/stats_test.go b/server/playbooks/server/sqlstore/stats_test.go deleted file mode 100644 index 3d0d97a18bb..00000000000 --- a/server/playbooks/server/sqlstore/stats_test.go +++ /dev/null @@ -1,729 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "fmt" - "testing" - - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/guregu/null.v4" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - mock_sqlstore "github.com/mattermost/mattermost/server/v8/playbooks/server/sqlstore/mocks" -) - -func setupStatsStore(t *testing.T, db *sqlx.DB) *StatsStore { - mockCtrl := gomock.NewController(t) - - kvAPI := mock_sqlstore.NewMockKVAPI(mockCtrl) - configAPI := mock_sqlstore.NewMockConfigurationAPI(mockCtrl) - pluginAPIClient := PluginAPIClient{ - KV: kvAPI, - Configuration: configAPI, - } - - sqlStore := setupSQLStore(t, db) - - return NewStatsStore(pluginAPIClient, sqlStore) -} - -func TestTotalInProgressPlaybookRuns(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - - lucy := userInfo{ - ID: model.NewId(), - Name: "Lucy", - } - - john := userInfo{ - ID: model.NewId(), - Name: "john", - } - - jane := userInfo{ - ID: model.NewId(), - Name: "jane", - } - - phil := userInfo{ - ID: model.NewId(), - Name: "phil", - } - - quincy := userInfo{ - ID: model.NewId(), - Name: "quincy", - } - - notInvolved := userInfo{ - ID: model.NewId(), - Name: "notinvolved", - } - - bot1 := userInfo{ - ID: model.NewId(), - Name: "Mr. Bot", - } - - bot2 := userInfo{ - ID: model.NewId(), - Name: "Mrs. Bot", - } - - channel01 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 123, DeleteAt: 0} - channel02 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 199, DeleteAt: 0} - channel03 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 222, DeleteAt: 0} - channel04 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - channel05 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - channel06 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - channel07 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - channel08 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - channel09 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - - db := setupTestDB(t) - driverName := db.DriverName() - playbookRunStore := setupPlaybookRunStore(t, db) - statsStore := setupStatsStore(t, db) - - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - setupChannelMembersTable(t, db) - setupChannelMemberHistoryTable(t, db) - setupChannelsTable(t, db) - - addUsers(t, store, []userInfo{lucy, bob, john, jane, notInvolved, phil, quincy, bot1, bot2}) - addBots(t, store, []userInfo{bot1, bot2}) - addUsersToTeam(t, store, []userInfo{lucy, bob, john, jane, notInvolved, phil, quincy, bot1, bot2}, team1id) - addUsersToTeam(t, store, []userInfo{lucy, bob, john, jane, notInvolved, phil, quincy, bot1, bot2}, team2id) - createChannels(t, store, []model.Channel{channel01, channel02, channel03, channel04, channel05, channel06, channel07, channel08, channel09}) - makeAdmin(t, store, bob) - - inc01 := *NewBuilder(nil). - WithName("pr 1 - wheel cat aliens wheelbarrow"). - WithChannel(&channel01). - WithTeamID(team1id). - WithCurrentStatus(app.StatusInProgress). - WithCreateAt(123). - WithPlaybookID("playbook1"). - ToPlaybookRun() - - inc02 := *NewBuilder(nil). - WithName("pr 2"). - WithChannel(&channel02). - WithTeamID(team1id). - WithCurrentStatus(app.StatusInProgress). - WithCreateAt(123). - WithPlaybookID("playbook1"). - ToPlaybookRun() - - inc03 := *NewBuilder(nil). - WithName("pr 3"). - WithChannel(&channel03). - WithTeamID(team1id). - WithCurrentStatus(app.StatusFinished). - WithPlaybookID("playbook2"). - WithCreateAt(123). - ToPlaybookRun() - - inc04 := *NewBuilder(nil). - WithName("pr 4"). - WithChannel(&channel04). - WithTeamID(team2id). - WithCurrentStatus(app.StatusInProgress). - WithPlaybookID("playbook1"). - WithCreateAt(123). - ToPlaybookRun() - - inc05 := *NewBuilder(nil). - WithName("pr 5"). - WithChannel(&channel05). - WithTeamID(team2id). - WithCurrentStatus(app.StatusInProgress). - WithPlaybookID("playbook2"). - WithCreateAt(123). - ToPlaybookRun() - - inc06 := *NewBuilder(nil). - WithName("pr 6"). - WithChannel(&channel06). - WithTeamID(team1id). - WithCurrentStatus(app.StatusInProgress). - WithPlaybookID("playbook1"). - WithCreateAt(123). - ToPlaybookRun() - - inc07 := *NewBuilder(nil). - WithName("pr 7"). - WithChannel(&channel07). - WithTeamID(team2id). - WithCurrentStatus(app.StatusInProgress). - WithPlaybookID("playbook2"). - WithCreateAt(123). - ToPlaybookRun() - - inc08 := *NewBuilder(nil). - WithName("pr 8"). - WithChannel(&channel08). - WithTeamID(team1id). - WithCurrentStatus(app.StatusFinished). - WithPlaybookID("playbook1"). - WithCreateAt(123). - ToPlaybookRun() - - inc09 := *NewBuilder(nil). - WithName("pr 9"). - WithChannel(&channel09). - WithTeamID(team2id). - WithCurrentStatus(app.StatusFinished). - WithPlaybookID("playbook2"). - WithCreateAt(123). - ToPlaybookRun() - - playbookRuns := []app.PlaybookRun{inc01, inc02, inc03, inc04, inc05, inc06, inc07, inc08, inc09} - - for i := range playbookRuns { - created, err := playbookRunStore.CreatePlaybookRun(&playbookRuns[i]) - require.NoError(t, err) - playbookRuns[i] = *created - } - - addUsersToRuns(t, store, []userInfo{bob, lucy, phil}, []string{playbookRuns[0].ID, playbookRuns[1].ID, playbookRuns[2].ID, playbookRuns[3].ID, playbookRuns[5].ID, playbookRuns[6].ID, playbookRuns[7].ID, playbookRuns[8].ID}) - addUsersToRuns(t, store, []userInfo{bob, quincy}, []string{playbookRuns[4].ID}) - addUsersToRuns(t, store, []userInfo{john}, []string{playbookRuns[0].ID}) - addUsersToRuns(t, store, []userInfo{jane}, []string{playbookRuns[0].ID, playbookRuns[1].ID}) - - t.Run(driverName+" Active Participants - team1", func(t *testing.T) { - result := statsStore.TotalActiveParticipants(&StatsFilters{ - TeamID: team1id, - }) - assert.Equal(t, 5, result) - }) - - t.Run(driverName+" Active Participants - team2", func(t *testing.T) { - result := statsStore.TotalActiveParticipants(&StatsFilters{ - TeamID: team2id, - }) - assert.Equal(t, 4, result) - }) - - t.Run(driverName+" Active Participants, playbook1", func(t *testing.T) { - result := statsStore.TotalActiveParticipants(&StatsFilters{ - PlaybookID: "playbook1", - }) - assert.Equal(t, 5, result) - }) - - t.Run(driverName+" Active Participants, playbook2", func(t *testing.T) { - result := statsStore.TotalActiveParticipants(&StatsFilters{ - PlaybookID: "playbook2", - }) - assert.Equal(t, 4, result) - }) - - t.Run(driverName+" Active Participants, all", func(t *testing.T) { - result := statsStore.TotalActiveParticipants(&StatsFilters{}) - assert.Equal(t, 6, result) - }) - - t.Run(driverName+" In-progress Playbook Runs - team1", func(t *testing.T) { - result := statsStore.TotalInProgressPlaybookRuns(&StatsFilters{ - TeamID: team1id, - }) - assert.Equal(t, 3, result) - }) - - t.Run(driverName+" In-progress Playbook Runs - team2", func(t *testing.T) { - result := statsStore.TotalInProgressPlaybookRuns(&StatsFilters{ - TeamID: team2id, - }) - assert.Equal(t, 3, result) - }) - - t.Run(driverName+" In-progress Playbook Runs - playbook1", func(t *testing.T) { - result := statsStore.TotalInProgressPlaybookRuns(&StatsFilters{ - PlaybookID: "playbook1", - }) - assert.Equal(t, 4, result) - }) - - t.Run(driverName+" In-progress Playbook Runs - playbook2", func(t *testing.T) { - result := statsStore.TotalInProgressPlaybookRuns(&StatsFilters{ - PlaybookID: "playbook2", - }) - assert.Equal(t, 2, result) - }) - - t.Run(driverName+" In-progress Playbook Runs - all", func(t *testing.T) { - result := statsStore.TotalInProgressPlaybookRuns(&StatsFilters{}) - assert.Equal(t, 6, result) - }) - - /* This can't be tested well because it uses model.GetMillis() inside - t.Run(driverName+" Average Druation Active Playbook Runs Minutes", func(t *testing.T) { - result := statsStore.AverageDurationActivePlaybookRunsMinutes() - assert.Equal(t, 26912080, result) - })*/ - - t.Run(driverName+" RunsStartedPerWeekLastXWeeks for a playbook with no runs", func(t *testing.T) { - runsStartedPerWeek, _ := statsStore.RunsStartedPerWeekLastXWeeks(4, &StatsFilters{ - PlaybookID: "playbook101test123123", - }) - assert.Equal(t, []int{0, 0, 0, 0}, runsStartedPerWeek) - }) - - t.Run(driverName+" ActiveRunsPerDayLastXDays for a playbook with no runs", func(t *testing.T) { - activeRunsPerDay, _ := statsStore.ActiveRunsPerDayLastXDays(4, &StatsFilters{ - PlaybookID: "playbook101test1234", - }) - assert.Equal(t, []int{0, 0, 0, 0}, activeRunsPerDay) - }) - - t.Run(driverName+" ActiveParticipantsPerDayLastXDays for a playbook with no runs", func(t *testing.T) { - activeParticipantsPerDay, _ := statsStore.ActiveParticipantsPerDayLastXDays(4, &StatsFilters{ - PlaybookID: "playbook101test32412", - }) - assert.Equal(t, []int{0, 0, 0, 0}, activeParticipantsPerDay) - }) -} - -func TestTotalPlaybookRuns(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - - lucy := userInfo{ - ID: model.NewId(), - Name: "Lucy", - } - - john := userInfo{ - ID: model.NewId(), - Name: "john", - } - - bot1 := userInfo{ - ID: model.NewId(), - Name: "Mr. Bot", - } - - bot2 := userInfo{ - ID: model.NewId(), - Name: "Mrs. Bot", - } - - chanOpen01 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 123, DeleteAt: 0} - chanOpen02 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 199, DeleteAt: 0} - chanOpen03 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 222, DeleteAt: 0} - chanPrivate01 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - chanPrivate02 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - chanPrivate03 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - chanPrivate04 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - chanPrivate05 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - chanPrivate06 := model.Channel{Id: model.NewId(), Type: "P", CreateAt: 333, DeleteAt: 0} - - db := setupTestDB(t) - driverName := db.DriverName() - playbookRunStore := setupPlaybookRunStore(t, db) - statsStore := setupStatsStore(t, db) - - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - setupChannelMembersTable(t, db) - setupChannelMemberHistoryTable(t, db) - setupChannelsTable(t, db) - - addUsers(t, store, []userInfo{lucy, bob, john, bot1, bot2}) - addBots(t, store, []userInfo{bot1, bot2}) - addUsersToTeam(t, store, []userInfo{lucy, bob, john, bot2}, team1id) - addUsersToTeam(t, store, []userInfo{lucy, bob, bot1, bot2}, team2id) - createChannels(t, store, []model.Channel{chanOpen01, chanOpen02, chanOpen03, chanPrivate01, chanPrivate02, chanPrivate03, chanPrivate04, chanPrivate05, chanPrivate06}) - makeAdmin(t, store, bob) - - // create run with different statuses, channels, teams and playbooks - run01 := *NewBuilder(nil). - WithName("pr 1 - team1-channel1-inprogress"). - WithChannel(&chanOpen01). - WithTeamID(team1id). - WithCurrentStatus(app.StatusInProgress). - WithCreateAt(123). - WithPlaybookID("playbook1"). - ToPlaybookRun() - - run02 := *NewBuilder(nil). - WithName("pr 2 - team1-channel2-inprogress"). - WithChannel(&chanOpen02). - WithTeamID(team1id). - WithCurrentStatus(app.StatusInProgress). - WithCreateAt(123). - WithPlaybookID("playbook1"). - ToPlaybookRun() - - run03 := *NewBuilder(nil). - WithName("pr 3 - team1-channel3-finished"). - WithChannel(&chanOpen03). - WithTeamID(team1id). - WithCurrentStatus(app.StatusFinished). - WithPlaybookID("playbook2"). - WithCreateAt(123). - ToPlaybookRun() - - run04 := *NewBuilder(nil). - WithName("pr 4 - team2-channel4-inprogress"). - WithChannel(&chanPrivate01). - WithTeamID(team2id). - WithCurrentStatus(app.StatusInProgress). - WithPlaybookID("playbook1"). - WithCreateAt(123). - ToPlaybookRun() - - run05 := *NewBuilder(nil). - WithName("pr 5 - team2-channel5-inprogress"). - WithChannel(&chanPrivate02). - WithTeamID(team2id). - WithCurrentStatus(app.StatusInProgress). - WithPlaybookID("playbook2"). - WithCreateAt(123). - ToPlaybookRun() - - run06 := *NewBuilder(nil). - WithName("pr 6 - team2-channel5-finished"). - WithChannel(&chanPrivate03). - WithTeamID(team2id). - WithCurrentStatus(app.StatusFinished). - WithPlaybookID("playbook1"). - WithCreateAt(123). - ToPlaybookRun() - - playbookRuns := []app.PlaybookRun{run01, run02, run03, run04, run05, run06} - - for i := range playbookRuns { - created, err := playbookRunStore.CreatePlaybookRun(&playbookRuns[i]) - playbookRuns[i] = *created - require.NoError(t, err) - } - - addUsersToRuns(t, store, []userInfo{bob, lucy, bot1, bot2}, []string{playbookRuns[0].ID, playbookRuns[1].ID, playbookRuns[2].ID, playbookRuns[3].ID, playbookRuns[5].ID}) - addUsersToRuns(t, store, []userInfo{bob}, []string{playbookRuns[4].ID}) - addUsersToRuns(t, store, []userInfo{john}, []string{playbookRuns[0].ID}) - - t.Run(driverName+" TotalPlaybookRuns", func(t *testing.T) { - result, err := statsStore.TotalPlaybookRuns() - assert.NoError(t, err) - assert.Equal(t, 6, result) - }) -} - -func TestTotalPlaybooks(t *testing.T) { - team1id := model.NewId() - team2id := model.NewId() - - bob := userInfo{ - ID: model.NewId(), - Name: "bob", - } - - lucy := userInfo{ - ID: model.NewId(), - Name: "Lucy", - } - - bot1 := userInfo{ - ID: model.NewId(), - Name: "Mr. Bot", - } - - bot2 := userInfo{ - ID: model.NewId(), - Name: "Mrs. Bot", - } - - channel01 := model.Channel{Id: model.NewId(), Type: "O", CreateAt: 123, DeleteAt: 0} - - db := setupTestDB(t) - driverName := db.DriverName() - playbookStore := setupPlaybookStore(t, db) - playbookRunStore := setupPlaybookRunStore(t, db) - statsStore := setupStatsStore(t, db) - - store := setupSQLStore(t, db) - setupTeamMembersTable(t, db) - setupChannelMembersTable(t, db) - setupChannelMemberHistoryTable(t, db) - setupChannelsTable(t, db) - - addUsers(t, store, []userInfo{lucy, bob, bot1, bot2}) - addBots(t, store, []userInfo{bot1, bot2}) - addUsersToTeam(t, store, []userInfo{lucy, bot2}, team1id) - addUsersToTeam(t, store, []userInfo{lucy, bob, bot1, bot2}, team2id) - createChannels(t, store, []model.Channel{channel01}) - addUsersToChannels(t, store, []userInfo{bob, lucy, bot1, bot2}, []string{channel01.Id}) - makeAdmin(t, store, bob) - - pb01 := NewPBBuilder(). - WithTeamID(team1id). - WithTitle("playbook 1"). - ToPlaybook() - pb02 := NewPBBuilder(). - WithTeamID(team2id). - WithTitle("Playbook 2"). - ToPlaybook() - for _, pb := range []app.Playbook{pb01, pb02} { - _, err := playbookStore.Create(pb) - require.NoError(t, err) - } - - // create at least a run to have playbooks with and without runs - run01 := *NewBuilder(nil). - WithName("pr 1"). - WithChannel(&channel01). - WithTeamID(team1id). - WithCurrentStatus(app.StatusInProgress). - WithCreateAt(123). - WithPlaybookID("playbook1"). - ToPlaybookRun() - - _, err := playbookRunStore.CreatePlaybookRun(&run01) - require.NoError(t, err) - - t.Run(driverName+" TotalPlaybooks", func(t *testing.T) { - result, err := statsStore.TotalPlaybooks() - assert.NoError(t, err) - assert.Equal(t, 2, result) - }) -} - -func TestMetricsStats(t *testing.T) { - teamID := model.NewId() - - db := setupTestDB(t) - playbookRunStore := setupPlaybookRunStore(t, db) - playbookStore := setupPlaybookStore(t, db) - statsStore := setupStatsStore(t, db) - store := setupSQLStore(t, db) - - setupChannelsTable(t, db) - setupPostsTable(t, db) - - publishTime := model.GetMillis() - - t.Run("no metrics configured", func(t *testing.T) { - playbook := NewPBBuilder(). - WithTitle("pb1"). - WithTeamID(teamID). - WithCreateAt(500). - ToPlaybook() - - playbookID, err := playbookStore.Create(playbook) - require.NoError(t, err) - - // create 4 runs - createRunsWithMetrics(t, playbookRunStore, store, playbookID, [][]app.RunMetricData{nil, nil, nil}, true, &publishTime) - - filters := StatsFilters{ - PlaybookID: playbookID, - } - - actualAverage := statsStore.MetricOverallAverage(filters) - actualRollingAverage, actualRollingAverageChange := statsStore.MetricRollingAverageAndChange(2, filters) - actualRollingValues, _ := statsStore.MetricRollingValuesLastXRuns(2, 1, filters) - actualRange := statsStore.MetricValueRange(filters) - require.Equal(t, []null.Int{}, actualAverage) - require.Equal(t, []null.Int{}, actualRollingAverage) - require.Equal(t, []null.Int{}, actualRollingAverageChange) - require.Equal(t, [][]int64{}, actualRollingValues) - require.Equal(t, [][]int64{}, actualRange) - }) - - t.Run("no published metrics", func(t *testing.T) { - playbook := NewPBBuilder(). - WithTitle("pb1"). - WithTeamID(teamID). - WithCreateAt(500). - WithMetrics([]string{"metric1", "metric2"}). - ToPlaybook() - - playbookID, err := playbookStore.Create(playbook) - require.NoError(t, err) - playbook, err = playbookStore.Get(playbookID) - require.NoError(t, err) - - metricsData := createMetricsData(playbook.Metrics, [][]int64{{2, 3}, {9, 8}, {11, 1}, {7, 3}, {3, 10}}) - createRunsWithMetrics(t, playbookRunStore, store, playbookID, metricsData, false, &publishTime) - - filters := StatsFilters{ - PlaybookID: playbookID, - } - - actualAverage := statsStore.MetricOverallAverage(filters) - actualRollingAverage, actualRollingAverageChange := statsStore.MetricRollingAverageAndChange(2, filters) - actualRollingValues, _ := statsStore.MetricRollingValuesLastXRuns(2, 1, filters) - actualRange := statsStore.MetricValueRange(filters) - require.Equal(t, []null.Int{null.NewInt(0, false), null.NewInt(0, false)}, actualAverage) - require.Equal(t, []null.Int{null.NewInt(0, false), null.NewInt(0, false)}, actualRollingAverage) - require.Equal(t, []null.Int{null.NewInt(0, false), null.NewInt(0, false)}, actualRollingAverageChange) - require.Equal(t, [][]int64{nil, nil}, actualRollingValues) - require.Equal(t, [][]int64{nil, nil}, actualRange) - }) - - t.Run("publish runs with metrics", func(t *testing.T) { - playbook := NewPBBuilder(). - WithTitle("pb1"). - WithTeamID(teamID). - WithCreateAt(500). - WithMetrics([]string{"metric1", "metric2"}). - ToPlaybook() - - playbookID, err := playbookStore.Create(playbook) - require.NoError(t, err) - playbook, err = playbookStore.Get(playbookID) - require.NoError(t, err) - - metricsData := createMetricsData(playbook.Metrics, [][]int64{{2, 3}, {9, 8}, {11, 1}, {7, 3}, {3, 10}}) - createRunsWithMetrics(t, playbookRunStore, store, playbookID, metricsData, true, &publishTime) - - filters := StatsFilters{ - PlaybookID: playbookID, - } - - // period value is 2, tests case when there is available data for full two periods - actualAverage := statsStore.MetricOverallAverage(filters) - actualRollingAverage, actualRollingAverageChange := statsStore.MetricRollingAverageAndChange(2, filters) - actualRollingValues, _ := statsStore.MetricRollingValuesLastXRuns(2, 1, filters) - actualRange := statsStore.MetricValueRange(filters) - require.Equal(t, []null.Int{null.IntFrom(6), null.IntFrom(5)}, actualAverage) - require.Equal(t, []null.Int{null.IntFrom(5), null.IntFrom(6)}, actualRollingAverage) - require.Equal(t, []null.Int{null.IntFrom(-50), null.IntFrom(50)}, actualRollingAverageChange) - require.Equal(t, [][]int64{{7, 11}, {3, 1}}, actualRollingValues) - require.Equal(t, [][]int64{{2, 11}, {1, 10}}, actualRange) - - // period value is 4 - actualRollingAverage, actualRollingAverageChange = statsStore.MetricRollingAverageAndChange(4, filters) - actualRollingValues, _ = statsStore.MetricRollingValuesLastXRuns(3, 3, filters) - require.Equal(t, []null.Int{null.IntFrom(7), null.IntFrom(5)}, actualRollingAverage) - require.Equal(t, []null.Int{null.IntFrom(250), null.IntFrom(66)}, actualRollingAverageChange) - require.Equal(t, [][]int64{{9, 2}, {8, 3}}, actualRollingValues) - }) - - t.Run("publish runs with metrics, then add additional metric to the playbook", func(t *testing.T) { - playbook := NewPBBuilder(). - WithTitle("pb1"). - WithTeamID(teamID). - WithCreateAt(500). - WithMetrics([]string{"metric1"}). - ToPlaybook() - - playbookID, err := playbookStore.Create(playbook) - require.NoError(t, err) - playbook, err = playbookStore.Get(playbookID) - require.NoError(t, err) - - metricsData := createMetricsData(playbook.Metrics, [][]int64{{2}, {9}, {11}, {7}, {3}}) - createRunsWithMetrics(t, playbookRunStore, store, playbookID, metricsData, true, &publishTime) - createRunsWithMetrics(t, playbookRunStore, store, playbookID, metricsData[2:], false, &publishTime) - - filters := StatsFilters{ - PlaybookID: playbookID, - } - - // add a metric to the playbook at first position - playbook.Metrics = append(playbook.Metrics, playbook.Metrics[0]) - playbook.Metrics[0] = app.PlaybookMetricConfig{ - Title: "metric2", - Type: app.MetricTypeInteger, - } - - err = playbookStore.Update(playbook) - require.NoError(t, err) - - // the first metric's values should not be available - actualAverage := statsStore.MetricOverallAverage(filters) - actualRollingAverage, actualRollingAverageChange := statsStore.MetricRollingAverageAndChange(3, filters) - actualRollingValues, _ := statsStore.MetricRollingValuesLastXRuns(3, 1, filters) - actualRange := statsStore.MetricValueRange(filters) - require.Equal(t, []null.Int{null.NewInt(0, false), null.IntFrom(6)}, actualAverage) - require.Equal(t, []null.Int{null.NewInt(0, false), null.IntFrom(7)}, actualRollingAverage) - require.Equal(t, []null.Int{null.NewInt(0, false), null.IntFrom(40)}, actualRollingAverageChange) - require.Equal(t, [][]int64{nil, {7, 11, 9}}, actualRollingValues) - require.Equal(t, [][]int64{nil, {2, 11}}, actualRange) - - // publish more data, now with two metrics - playbook, err = playbookStore.Get(playbookID) - require.NoError(t, err) - - metricsData = createMetricsData(playbook.Metrics, [][]int64{{200, 3}, {103, 9}}) - createRunsWithMetrics(t, playbookRunStore, store, playbookID, metricsData, true, &publishTime) - - actualAverage = statsStore.MetricOverallAverage(filters) - actualRollingAverage, actualRollingAverageChange = statsStore.MetricRollingAverageAndChange(4, filters) - actualRollingValues, _ = statsStore.MetricRollingValuesLastXRuns(4, 0, filters) - actualRange = statsStore.MetricValueRange(filters) - require.Equal(t, []null.Int{null.IntFrom(151), null.IntFrom(6)}, actualAverage) - require.Equal(t, []null.Int{null.IntFrom(151), null.IntFrom(5)}, actualRollingAverage) - require.Equal(t, []null.Int{null.NewInt(0, false), null.IntFrom(-29)}, actualRollingAverageChange) - require.Equal(t, [][]int64{{103, 200}, {9, 3, 3, 7}}, actualRollingValues) - require.Equal(t, [][]int64{{103, 200}, {2, 11}}, actualRange) - }) -} - -func createRunsWithMetrics(t *testing.T, playbookRunStore app.PlaybookRunStore, store *SQLStore, playbookID string, metricsData [][]app.RunMetricData, publish bool, publishTime *int64) { - var channels []model.Channel - for i, md := range metricsData { - channel := model.Channel{Id: model.NewId(), Type: "O", DisplayName: "displayname for channel", Name: "channel"} - channels = append(channels, channel) - - playbookRun := NewBuilder(t). - WithName(fmt.Sprint("run", i)). - WithPlaybookID(playbookID). - WithChannel(&channel). - ToPlaybookRun() - - playbookRun, err := playbookRunStore.CreatePlaybookRun(playbookRun) - assert.NoError(t, err) - assert.NotNil(t, playbookRun) - - playbookRun.Retrospective = "retro text" - playbookRun.MetricsData = md - - if publish { - // increase time by 10 sec to avoid duplicate values. Otherwise, metric values sorted by `PublishedAt` may be inconsistent. - *publishTime += 10000 - playbookRun.RetrospectivePublishedAt = *publishTime - playbookRun.RetrospectiveWasCanceled = false - } - - _, err = playbookRunStore.UpdatePlaybookRun(playbookRun) - require.NoError(t, err) - } - - if len(channels) > 0 { - createChannels(t, store, channels) - } -} - -func createMetricsData(metricsConfigs []app.PlaybookMetricConfig, data [][]int64) [][]app.RunMetricData { - metricsData := make([][]app.RunMetricData, len(data)) - for i, d := range data { - md := make([]app.RunMetricData, len(metricsConfigs)) - for j, c := range metricsConfigs { - md[j] = app.RunMetricData{MetricConfigID: c.ID, Value: null.IntFrom(d[j])} - } - metricsData[i] = md - } - return metricsData -} diff --git a/server/playbooks/server/sqlstore/store.go b/server/playbooks/server/sqlstore/store.go deleted file mode 100644 index ebd4a3bb8cd..00000000000 --- a/server/playbooks/server/sqlstore/store.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/sirupsen/logrus" - - sq "github.com/Masterminds/squirrel" - "github.com/jmoiron/sqlx" - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" -) - -// maxJSONLength holds the limit we set for JSON data in postgres -// Since JSON data type is unboounded, we need to set a limit -// that we'll control manually. -const maxJSONLength = 256 * 1024 // 256KB - -type SQLStore struct { - db *sqlx.DB - builder sq.StatementBuilderType - scheduler app.JobOnceScheduler -} - -// New constructs a new instance of SQLStore. -func New(pluginAPI PluginAPIClient, scheduler app.JobOnceScheduler) (*SQLStore, error) { - var db *sqlx.DB - - origDB, err := pluginAPI.Store.GetMasterDB() - if err != nil { - return nil, err - } - db = sqlx.NewDb(origDB, pluginAPI.Store.DriverName()) - - builder := sq.StatementBuilder.PlaceholderFormat(sq.Question) - if pluginAPI.Store.DriverName() == model.DatabaseDriverPostgres { - builder = builder.PlaceholderFormat(sq.Dollar) - } - - if pluginAPI.Store.DriverName() == model.DatabaseDriverMysql { - db.MapperFunc(func(s string) string { return s }) - } - - return &SQLStore{ - db, - builder, - scheduler, - }, nil -} - -// queryer is an interface describing a resource that can query. -// -// It exactly matches sqlx.Queryer, existing simply to constrain sqlx usage to this file. -type queryer interface { - sqlx.Queryer -} - -// builder is an interface describing a resource that can construct SQL and arguments. -// -// It exists to allow consuming any squirrel.*Builder type. -type builder interface { - ToSql() (string, []interface{}, error) -} - -// get queries for a single row, building the sql, and writing the result into dest. -// -// Use this to simplify querying for a single row or column. Dest may be a pointer to a simple -// type, or a struct with fields to be populated from the returned columns. -func (sqlStore *SQLStore) getBuilder(q sqlx.Queryer, dest interface{}, b builder) error { - sqlString, args, err := b.ToSql() - if err != nil { - return errors.Wrap(err, "failed to build sql") - } - - sqlString = sqlStore.db.Rebind(sqlString) - - return sqlx.Get(q, dest, sqlString, args...) -} - -// selectBuilder queries for one or more rows, building the sql, and writing the result into dest. -// -// Use this to simplify querying for multiple rows (and possibly columns). Dest may be a slice of -// a simple, or a slice of a struct with fields to be populated from the returned columns. -func (sqlStore *SQLStore) selectBuilder(q sqlx.Queryer, dest interface{}, b builder) error { - sqlString, args, err := b.ToSql() - if err != nil { - return errors.Wrap(err, "failed to build sql") - } - - sqlString = sqlStore.db.Rebind(sqlString) - - return sqlx.Select(q, dest, sqlString, args...) -} - -// execer is an interface describing a resource that can execute write queries. -// -// It allows the use of *sqlx.Db and *sqlx.Tx. -type execer interface { - Exec(query string, args ...interface{}) (sql.Result, error) - DriverName() string -} - -type queryExecer interface { - queryer - execer -} - -// exec executes the given query using positional arguments, automatically rebinding for the db. -func (sqlStore *SQLStore) exec(e execer, sqlString string, args ...interface{}) (sql.Result, error) { - sqlString = sqlStore.db.Rebind(sqlString) - return e.Exec(sqlString, args...) -} - -// exec executes the given query, building the necessary sql. -func (sqlStore *SQLStore) execBuilder(e execer, b builder) (sql.Result, error) { - sqlString, args, err := b.ToSql() - if err != nil { - return nil, errors.Wrap(err, "failed to build sql") - } - - return sqlStore.exec(e, sqlString, args...) -} - -// finalizeTransaction ensures a transaction is closed after use, rolling back if not already committed. -func (sqlStore *SQLStore) finalizeTransaction(tx *sqlx.Tx) { - // Rollback returns sql.ErrTxDone if the transaction was already closed. - if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { - logrus.WithError(err).Error("Failed to rollback transaction") - } -} diff --git a/server/playbooks/server/sqlstore/store_test.go b/server/playbooks/server/sqlstore/store_test.go deleted file mode 100644 index 6b9dbaee434..00000000000 --- a/server/playbooks/server/sqlstore/store_test.go +++ /dev/null @@ -1,879 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "testing" - "time" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - - mock_app "github.com/mattermost/mattermost/server/v8/playbooks/server/app/mocks" - - sq "github.com/Masterminds/squirrel" - "github.com/blang/semver" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" -) - -func TestMigrations(t *testing.T) { - mockCtrl := gomock.NewController(t) - scheduler := mock_app.NewMockJobOnceScheduler(mockCtrl) - - builder := sq.StatementBuilder.PlaceholderFormat(sq.Question) - if getDriverName() == model.DatabaseDriverPostgres { - builder = builder.PlaceholderFormat(sq.Dollar) - } - - t.Run("Run every migration twice", func(t *testing.T) { - db := setupTestDB(t) - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - // Make sure we start from scratch - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, semver.Version{}) - - // Migration to 0.10.0 needs the Channels table to work - setupChannelsTable(t, db) - // Migration to 0.21.0 need the Posts table - setupPostsTable(t, db) - // Migration to 0.31.0 needs the PluginKeyValueStore - setupKVStoreTable(t, db) - // Migration to 0.55.0 needs the TeamMembers table - setupTeamMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupChannelMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupBotsTable(t, db) - - // Apply each migration twice - for _, migration := range migrations { - for i := 0; i < 2; i++ { - err := sqlStore.migrate(migration) - require.NoError(t, err) - - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, migration.toVersion) - } - } - }) - - t.Run("Run the whole set of migrations twice", func(t *testing.T) { - db := setupTestDB(t) - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - // Make sure we start from scratch - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, semver.Version{}) - - // Migration to 0.10.0 needs the Channels table to work - setupChannelsTable(t, db) - // Migration to 0.21.0 need the Posts table - setupPostsTable(t, db) - // Migration to 0.31.0 needs the PluginKeyValueStore - setupKVStoreTable(t, db) - // Migration to 0.55.0 needs the TeamMembers table - setupTeamMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupChannelMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupBotsTable(t, db) - - // Apply the whole set of migrations twice - for i := 0; i < 2; i++ { - for _, migration := range migrations { - err := sqlStore.migrate(migration) - require.NoError(t, err) - - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, migration.toVersion) - } - } - }) - - t.Run("force incidents to have a reminder set", func(t *testing.T) { - db := setupTestDB(t) - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - // Make sure we start from scratch - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, semver.Version{}) - - // Migration to 0.10.0 needs the Channels table to work - setupChannelsTable(t, db) - // Migration to 0.21.0 need the Posts table - setupPostsTable(t, db) - // Migration to 0.31.0 needs the PluginKeyValueStore - setupKVStoreTable(t, db) - // Migration to 0.55.0 needs the TeamMembers table - setupTeamMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupChannelMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupBotsTable(t, db) - - // Apply the migrations up to and including 0.36 - migrateUpTo(t, sqlStore, semver.MustParse("0.36.0")) - - now := time.Now() - // Insert runs to test - expired, err := insertRunWithExpiredReminder(sqlStore, 1*time.Minute) - require.NoError(t, err) - noReminder, err := insertRunWithNoReminder(sqlStore) - require.NoError(t, err) - oldExpired, err := insertRunWithExpiredReminder(sqlStore, 4*24*time.Hour) - require.NoError(t, err) - activeReminder, err := insertRunWithActiveReminder(sqlStore, 24*time.Hour) - require.NoError(t, err) - inactive1, err := insertInactiveRunWithExpiredReminder(sqlStore, 23*time.Hour) - require.NoError(t, err) - inactive2, err := insertInactiveRunWithNoReminder(sqlStore) - require.NoError(t, err) - - // set expected calls we will get below when we run migration - newReminder := 24 * 7 * time.Hour - scheduler.EXPECT().Cancel(expired) - scheduler.EXPECT().ScheduleOnce(expired, gomock.Any()). - Return(nil, nil). - Times(1). - Do(func(id string, at time.Time) { - shouldHaveReminderBefore := now.Add(newReminder + 1*time.Second) - shouldHaveReminderAfter := now.Add(newReminder - 1*time.Second) - if at.Before(shouldHaveReminderAfter) || at.After(shouldHaveReminderBefore) { - t.Errorf("expected call to ScheduleOnce: %d to be after: %d and before: %d", - model.GetMillisForTime(at), model.GetMillisForTime(shouldHaveReminderAfter), - model.GetMillisForTime(shouldHaveReminderBefore)) - } - }) - scheduler.EXPECT().Cancel(noReminder) - scheduler.EXPECT().ScheduleOnce(noReminder, gomock.Any()). - Return(nil, nil). - Times(1). - Do(func(id string, at time.Time) { - shouldHaveReminderBefore := now.Add(newReminder + 1*time.Second) - shouldHaveReminderAfter := now.Add(newReminder - 1*time.Second) - if at.Before(shouldHaveReminderAfter) || at.After(shouldHaveReminderBefore) { - t.Errorf("expected call to ScheduleOnce: %d to be after: %d and before: %d", - model.GetMillisForTime(at), model.GetMillisForTime(shouldHaveReminderAfter), - model.GetMillisForTime(shouldHaveReminderBefore)) - } - }) - scheduler.EXPECT().Cancel(oldExpired) - scheduler.EXPECT().ScheduleOnce(oldExpired, gomock.Any()). - Return(nil, nil). - Times(1). - Do(func(id string, at time.Time) { - shouldHaveReminderBefore := now.Add(newReminder + 1*time.Second) - shouldHaveReminderAfter := now.Add(newReminder - 1*time.Second) - if at.Before(shouldHaveReminderAfter) || at.After(shouldHaveReminderBefore) { - t.Errorf("expected call to ScheduleOnce: %d to be after: %d and before: %d", - model.GetMillisForTime(at), model.GetMillisForTime(shouldHaveReminderAfter), - model.GetMillisForTime(shouldHaveReminderBefore)) - } - }) - - // Apply the migrations from 0.37-on - migrateFrom(t, sqlStore, semver.MustParse("0.36.0")) - - // Test that the runs that should have been changed now have new reminders - expiredRun, err := getRun(expired, sqlStore) - require.NoError(t, err) - require.Equal(t, expiredRun.PreviousReminder, newReminder) - noReminderRun, err := getRun(noReminder, sqlStore) - require.NoError(t, err) - require.Equal(t, noReminderRun.PreviousReminder, newReminder) - - // Test that the runs that should not have been changed do /not/ have new reminders - activeReminderRun, err := getRun(activeReminder, sqlStore) - require.NoError(t, err) - require.Equal(t, activeReminderRun.PreviousReminder, 24*time.Hour) - inactive1Run, err := getRun(inactive1, sqlStore) - require.NoError(t, err) - require.Equal(t, inactive1Run.PreviousReminder, 23*time.Hour) - inactive2Run, err := getRun(inactive2, sqlStore) - require.NoError(t, err) - require.Equal(t, inactive2Run.PreviousReminder, time.Duration(0)) - }) - - t.Run("copy Description column into new RunSummaryTemplate", func(t *testing.T) { - db := setupTestDB(t) - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - // Make sure we start from scratch - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, semver.Version{}) - - // Migration to 0.10.0 needs the Channels table to work - setupChannelsTable(t, db) - // Migration to 0.21.0 need the Posts table - setupPostsTable(t, db) - // Migration to 0.31.0 needs the PluginKeyValueStore - setupKVStoreTable(t, db) - // Migration to 0.55.0 needs the TeamMembers table - setupTeamMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupChannelMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupBotsTable(t, db) - - // Apply the migrations up to and including 0.38 - migrateUpTo(t, sqlStore, semver.MustParse("0.38.0")) - - playbookWithDescriptionID := model.NewId() - nonEmptyDescription := "a non-empty description" - - // Insert a playbook with a non-empty description - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Playbook"). - SetMap(map[string]interface{}{ - "ID": playbookWithDescriptionID, - "Description": nonEmptyDescription, - // Have to be set: - "Title": "Playbook", - "TeamID": model.NewId(), - "CreatePublicIncident": true, - "CreateAt": 0, - "DeleteAt": 0, - "ChecklistsJSON": []byte("{}"), - "NumStages": 0, - "NumSteps": 0, - "ReminderTimerDefaultSeconds": 0, - "RetrospectiveReminderIntervalSeconds": 0, - "UpdateAt": 0, - "ExportChannelOnFinishedEnabled": false, - })) - require.NoError(t, err) - - playbookWithEmptyDescriptionID := model.NewId() - - // Insert a playbook with an empty description - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Playbook"). - SetMap(map[string]interface{}{ - "ID": playbookWithEmptyDescriptionID, - "Description": "", - // Have to be set: - "Title": "Playbook", - "Teamid": model.NewId(), - "CreatePublicIncident": true, - "CreateAt": 0, - "DeleteAt": 0, - "ChecklistsJSON": []byte("{}"), - "NumStages": 0, - "NumSteps": 0, - "ReminderTimerDefaultSeconds": 0, - "RetrospectiveReminderIntervalSeconds": 0, - "UpdateAt": 0, - "ExportChannelOnFinishedEnabled": false, - })) - require.NoError(t, err) - - // Apply the migrations from 0.38-on - migrateFrom(t, sqlStore, semver.MustParse("0.38.0")) - - // Get the playbook with the non-empty description - var playbookWithDescription app.Playbook - err = sqlStore.getBuilder(sqlStore.db, &playbookWithDescription, sqlStore.builder. - Select("ID", "Description", "RunSummaryTemplate"). - From("IR_Playbook"). - Where(sq.Eq{"ID": playbookWithDescriptionID})) - require.NoError(t, err) - - // Get the playbook with the empty description - var playbookWithEmptyDescription app.Playbook - err = sqlStore.getBuilder(sqlStore.db, &playbookWithEmptyDescription, sqlStore.builder. - Select("ID", "Description", "RunSummaryTemplate"). - From("IR_Playbook"). - Where(sq.Eq{"ID": playbookWithEmptyDescriptionID})) - require.NoError(t, err) - - // Check that the copy was successful in the playbook with the non-empty description - require.Equal(t, playbookWithDescription.Description, "") - require.Equal(t, playbookWithDescription.RunSummaryTemplate, nonEmptyDescription) - - // Check that the copy was successful in the playbook with the empty description - require.Equal(t, playbookWithEmptyDescription.Description, "") - require.Equal(t, playbookWithEmptyDescription.RunSummaryTemplate, "") - }) - - t.Run("playbook member migration", func(t *testing.T) { - db := setupTestDB(t) - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - // Make sure we start from scratch - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, semver.Version{}) - - // Migration to 0.10.0 needs the Channels table to work - setupChannelsTable(t, db) - // Migration to 0.21.0 need the Posts table - setupPostsTable(t, db) - // Migration to 0.31.0 needs the PluginKeyValueStore - setupKVStoreTable(t, db) - // Migration to 0.55.0 needs the TeamMembers table - setupTeamMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupChannelMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupBotsTable(t, db) - - migrateUpTo(t, sqlStore, semver.MustParse("0.55.0")) - - // Public playbook - publicPlaybookID := model.NewId() - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Playbook"). - SetMap(map[string]interface{}{ - "ID": publicPlaybookID, - "Description": "", - "Public": true, - // Have to be set: - "Title": "Playbook", - "Teamid": model.NewId(), - "CreatePublicIncident": true, - "CreateAt": 0, - "DeleteAt": 0, - "ChecklistsJSON": []byte("{}"), - "NumStages": 0, - "NumSteps": 0, - "ReminderTimerDefaultSeconds": 0, - "RetrospectiveReminderIntervalSeconds": 0, - "UpdateAt": 0, - "ExportChannelOnFinishedEnabled": false, - })) - require.NoError(t, err) - - // Private playbook - privatePlaybookID := model.NewId() - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Playbook"). - SetMap(map[string]interface{}{ - "ID": privatePlaybookID, - "Description": "", - "Public": true, - // Have to be set: - "Title": "Playbook", - "Teamid": model.NewId(), - "CreatePublicIncident": true, - "CreateAt": 0, - "DeleteAt": 0, - "ChecklistsJSON": []byte("{}"), - "NumStages": 0, - "NumSteps": 0, - "ReminderTimerDefaultSeconds": 0, - "RetrospectiveReminderIntervalSeconds": 0, - "UpdateAt": 0, - "ExportChannelOnFinishedEnabled": false, - })) - require.NoError(t, err) - - channel1ID := model.NewId() - user1ID := model.NewId() - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("ChannelMembers"). - SetMap(map[string]interface{}{ - "UserID": user1ID, - "ChannelID": channel1ID, - })) - require.NoError(t, err) - - channel2ID := model.NewId() - user2ID := model.NewId() - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("ChannelMembers"). - SetMap(map[string]interface{}{ - "UserID": user2ID, - "ChannelID": channel2ID, - })) - require.NoError(t, err) - - publicPlaybookRunID := model.NewId() - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": publicPlaybookRunID, - "CreateAt": model.GetMillis(), - "CurrentStatus": app.StatusInProgress, - "PlaybookID": publicPlaybookID, - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": channel1ID, - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - require.NoError(t, err) - - privatePlaybookRunID := model.NewId() - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": privatePlaybookRunID, - "CreateAt": model.GetMillis(), - "CurrentStatus": app.StatusInProgress, - "PlaybookID": privatePlaybookRunID, - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": channel2ID, - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - require.NoError(t, err) - - migrateFrom(t, sqlStore, semver.MustParse("0.55.0")) - - // Check to see if we added the playbook member correctly - var member playbookMember - err = sqlStore.getBuilder(sqlStore.db, &member, sqlStore.builder. - Select("PlaybookID", "MemberID", "Roles"). - From("IR_PlaybookMember"). - Where(sq.Eq{"PlaybookID": publicPlaybookID}). - Where(sq.Eq{"MemberID": user1ID})) - require.NoError(t, err) - assert.Equal(t, publicPlaybookID, member.PlaybookID) - assert.Equal(t, user1ID, member.MemberID) - assert.Equal(t, "playbook_member", member.Roles) - - // Make sure we don't add to private playbooks - err = sqlStore.getBuilder(sqlStore.db, &member, sqlStore.builder. - Select("PlaybookID", "MemberID", "Roles"). - From("IR_PlaybookMember"). - Where(sq.Eq{"PlaybookID": privatePlaybookID}). - Where(sq.Eq{"MemberID": user2ID})) - require.ErrorIs(t, err, sql.ErrNoRows) - - // Must be a member of that playbooks run - err = sqlStore.getBuilder(sqlStore.db, &member, sqlStore.builder. - Select("PlaybookID", "MemberID", "Roles"). - From("IR_PlaybookMember"). - Where(sq.Eq{"PlaybookID": publicPlaybookID}). - Where(sq.Eq{"MemberID": user2ID})) - require.ErrorIs(t, err, sql.ErrNoRows) - }) - - t.Run("run participants migration", func(t *testing.T) { - db := setupTestDB(t) - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - // Make sure we start from scratch - currentSchemaVersion, err := sqlStore.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, semver.Version{}) - - // Migration to 0.10.0 needs the Channels table to work - setupChannelsTable(t, db) - // Migration to 0.21.0 need the Posts table - setupPostsTable(t, db) - // Migration to 0.31.0 needs the PluginKeyValueStore - setupKVStoreTable(t, db) - // Migration to 0.55.0 needs the TeamMembers table - setupTeamMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupChannelMembersTable(t, db) - // Migration to 0.56.0 needs ChannelMembers table - setupBotsTable(t, db) - - // Apply the migrations up to and including 0.57.0 - migrateUpTo(t, sqlStore, semver.MustParse("0.57.0")) - - bot1 := userInfo{ - ID: model.NewId(), - Name: "Mr. Bot", - } - bot2 := userInfo{ - ID: model.NewId(), - Name: "Mrs. Bot", - } - // Add two bots - addBots(t, sqlStore, []userInfo{bot1, bot2}) - - userIDs := []string{model.NewId(), model.NewId(), model.NewId(), model.NewId()} - runs := []struct { - ID string - ChannelID string - ChannelMemberIDs []string - }{ - {ID: model.NewId(), ChannelID: model.NewId(), ChannelMemberIDs: []string{userIDs[0], userIDs[1], userIDs[2], bot1.ID}}, - {ID: model.NewId(), ChannelID: model.NewId(), ChannelMemberIDs: []string{userIDs[0], userIDs[1], bot2.ID}}, - {ID: model.NewId(), ChannelID: model.NewId(), ChannelMemberIDs: []string{userIDs[0]}}, - {ID: model.NewId(), ChannelID: model.NewId(), ChannelMemberIDs: []string{bot1.ID, bot2.ID}}, - } - for _, run := range runs { - // Insert runs - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": run.ID, - "CreateAt": model.GetMillis(), - "CurrentStatus": app.StatusInProgress, - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": run.ChannelID, - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - require.NoError(t, err) - - // Insert channel members - for _, userID := range run.ChannelMemberIDs { - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("ChannelMembers"). - SetMap(map[string]interface{}{ - "UserID": userID, - "ChannelID": run.ChannelID, - })) - require.NoError(t, err) - } - } - - // Add users to IR_Run_Participants - // Channel member and follower - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Run_Participants"). - SetMap(map[string]interface{}{ - "UserID": userIDs[0], - "IncidentID": runs[0].ID, - "IsFollower": true, - })) - require.NoError(t, err) - // Channel member, not follower - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Run_Participants"). - SetMap(map[string]interface{}{ - "UserID": userIDs[0], - "IncidentID": runs[1].ID, - "IsFollower": false, - })) - require.NoError(t, err) - // Not channel member, follower - _, err = sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Run_Participants"). - SetMap(map[string]interface{}{ - "UserID": userIDs[3], - "IncidentID": runs[3].ID, - "IsFollower": false, - })) - require.NoError(t, err) - - type RunParticipant struct { - UserID string - IncidentID string - } - - var runMembers1 []RunParticipant - err = sqlStore.selectBuilder(sqlStore.db, &runMembers1, sqlStore.builder. - Select("UserID", "IncidentID"). - From("IR_Run_Participants"). - OrderBy("UserID ASC")) - require.NoError(t, err) - - // Apply the migrations from 0.57.0-on - migrateFrom(t, sqlStore, semver.MustParse("0.57.0")) - - // Compare run members list and channel members list - var runMembers []RunParticipant - err = sqlStore.selectBuilder(sqlStore.db, &runMembers, sqlStore.builder. - Select("UserID", "IncidentID"). - From("IR_Run_Participants"). - Where(sq.Eq{"IsParticipant": true}). - OrderBy("UserID ASC"). - OrderBy("IncidentID ASC")) - require.NoError(t, err) - - var channelMembers []RunParticipant - err = sqlStore.selectBuilder(sqlStore.db, &channelMembers, sqlStore.builder. - Select("cm.UserID as UserID", "i.ID as IncidentID"). - From("ChannelMembers as cm"). - Join("IR_Incident AS i ON i.ChannelID = cm.ChannelID"). - OrderBy("UserID ASC"). - OrderBy("IncidentID ASC")) - require.NoError(t, err) - require.Len(t, runMembers, 10) - require.Equal(t, runMembers, channelMembers) - - var count int64 - - // Verify followers number - err = sqlStore.getBuilder(sqlStore.db, &count, sqlStore.builder. - Select("COUNT(*)"). - From("IR_Run_Participants"). - Where(sq.Eq{"IsFollower": true})) - require.NoError(t, err) - require.Equal(t, int64(1), count) - }) -} - -func insertRunWithExpiredReminder(sqlStore *SQLStore, reminderExpiredAgo time.Duration) (string, error) { - id := model.NewId() - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": id, - "CreateAt": model.GetMillis(), - "PreviousReminder": 24 * time.Hour, - "CurrentStatus": app.StatusInProgress, - "LastStatusUpdateAt": model.GetMillisForTime(time.Now().Add(-24*time.Hour - reminderExpiredAgo)), - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": model.NewId(), - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - - return id, err -} - -func insertRunWithNoReminder(sqlStore *SQLStore) (string, error) { - id := model.NewId() - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": id, - "CreateAt": model.GetMillis(), - "CurrentStatus": app.StatusInProgress, - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": model.NewId(), - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - - return id, err -} - -func insertRunWithActiveReminder(sqlStore *SQLStore, previousReminder time.Duration) (string, error) { - id := model.NewId() - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": id, - "CreateAt": model.GetMillis(), - "PreviousReminder": previousReminder, - "CurrentStatus": app.StatusInProgress, - "LastStatusUpdateAt": model.GetMillisForTime(time.Now().Add(-24*time.Hour + 10*time.Second)), - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": model.NewId(), - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - - return id, err -} - -func insertInactiveRunWithExpiredReminder(sqlStore *SQLStore, previousReminder time.Duration) (string, error) { - id := model.NewId() - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": id, - "CreateAt": model.GetMillis(), - "PreviousReminder": previousReminder, - "CurrentStatus": app.StatusFinished, - "LastStatusUpdateAt": model.GetMillisForTime(time.Now().Add(-25 * time.Hour)), - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": model.NewId(), - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - - return id, err -} - -func insertInactiveRunWithNoReminder(sqlStore *SQLStore) (string, error) { - id := model.NewId() - _, err := sqlStore.execBuilder(sqlStore.db, sq. - Insert("IR_Incident"). - SetMap(map[string]interface{}{ - "ID": id, - "CreateAt": model.GetMillis(), - "CurrentStatus": app.StatusFinished, - // have to be set: - "Name": "test", - "Description": "test", - "IsActive": true, - "CommanderUserID": "commander", - "TeamID": "testTeam", - "ChannelID": model.NewId(), - "ActiveStage": 0, - "ChecklistsJSON": "{}", - })) - - return id, err -} - -func getRun(id string, sqlStore *SQLStore) (app.PlaybookRun, error) { - var run app.PlaybookRun - err := sqlStore.getBuilder(sqlStore.db, &run, sqlStore.builder. - Select("ID", "Name", "CreateAt", "PreviousReminder", "CurrentStatus", "LastStatusUpdateAt"). - From("IR_Incident"). - Where(sq.Eq{"ID": id})) - return run, err -} - -func TestHasConsistentCharset(t *testing.T) { - if getDriverName() != model.DatabaseDriverMysql { - t.Skip("TestHasConsistentCharset only needed for MySQL") - return - } - - t.Run("MySQL", func(t *testing.T) { - db := setupTestDB(t) - setupPlaybookStore(t, db) // To run the migrations and everything - badCharsets := []string{} - err := db.Select(&badCharsets, ` - SELECT tab.table_name - FROM information_schema.tables tab - WHERE tab.table_schema NOT IN ( 'mysql', 'information_schema', - 'performance_schema', - 'sys' ) - AND tab.table_schema = (SELECT DATABASE()) - AND NOT (tab.table_collation = 'utf8mb4_general_ci' OR tab.table_collation = 'utf8mb4_0900_ai_ci') - `) - require.Len(t, badCharsets, 0) - require.NoError(t, err) - }) -} - -func TestHasPrimaryKeys(t *testing.T) { - t.Run("MySQL", func(t *testing.T) { - if getDriverName() != model.DatabaseDriverMysql { - t.Skip("TestHasPrimaryKeys skipping MySQL specific test") - return - } - - db := setupTestDB(t) - setupPlaybookStore(t, db) // To run the migrations and everything - tablesWithoutPrimaryKeys := []string{} - err := db.Select(&tablesWithoutPrimaryKeys, ` - SELECT tab.table_name - AS tablename - FROM information_schema.tables tab - LEFT JOIN information_schema.table_constraints tco - ON tab.table_schema = tco.table_schema - AND tab.table_name = tco.table_name - AND tco.constraint_type = 'PRIMARY KEY' - LEFT JOIN information_schema.key_column_usage kcu - ON tco.constraint_schema = kcu.constraint_schema - AND tco.constraint_name = kcu.constraint_name - AND tco.table_name = kcu.table_name - WHERE tab.table_schema = (SELECT DATABASE()) - AND tco.constraint_name is NULL - GROUP BY tab.table_schema, - tab.table_name, - tco.constraint_name - `) - require.Len(t, tablesWithoutPrimaryKeys, 0) - require.NoError(t, err) - }) - - t.Run("Postgres", func(t *testing.T) { - if getDriverName() != model.DatabaseDriverPostgres { - t.Skip("TestHasPrimaryKeys skipping Postgres specific test") - return - } - - db := setupTestDB(t) - setupPlaybookStore(t, db) // To run the migrations and everything - tablesWithoutPrimaryKeys := []string{} - err := db.Select(&tablesWithoutPrimaryKeys, ` - SELECT tab.table_name AS pk_name - FROM information_schema.tables tab - LEFT JOIN information_schema.table_constraints tco - ON tco.table_schema = tab.table_schema - AND tco.table_name = tab.table_name - AND tco.constraint_type = 'PRIMARY KEY' - LEFT JOIN information_schema.key_column_usage kcu - ON kcu.constraint_name = tco.constraint_name - AND kcu.constraint_schema = tco.constraint_schema - AND kcu.constraint_name = tco.constraint_name - WHERE tab.table_schema NOT IN ( 'pg_catalog', 'information_schema' ) - AND tab.table_type = 'BASE TABLE' - AND tab.table_catalog = (SELECT current_database()) - AND tco.constraint_name is NULL - GROUP BY tab.table_schema, - tab.table_name, - tco.constraint_name - `) - tablesToBeFiltered := []string{"teammembers"} - for _, table := range tablesToBeFiltered { - tablesWithoutPrimaryKeys = removeFromSlice(tablesWithoutPrimaryKeys, table) - } - require.Len(t, tablesWithoutPrimaryKeys, 0) - require.NoError(t, err) - }) - -} - -func removeFromSlice(slice []string, item string) []string { - for i, elem := range slice { - if elem == item { - return append(slice[:i], slice[i+1:]...) - } - } - return slice -} diff --git a/server/playbooks/server/sqlstore/support_for_test.go b/server/playbooks/server/sqlstore/support_for_test.go deleted file mode 100644 index f40dab67e35..00000000000 --- a/server/playbooks/server/sqlstore/support_for_test.go +++ /dev/null @@ -1,841 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "os" - "testing" - - "github.com/blang/semver" - - mock_app "github.com/mattermost/mattermost/server/v8/playbooks/server/app/mocks" - - sq "github.com/Masterminds/squirrel" - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/channels/store/storetest" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" -) - -func getDriverName() string { - driver := os.Getenv("MM_SQLSETTINGS_DRIVERNAME") - if driver == "" { - driver = model.DatabaseDriverPostgres - } - return driver -} - -func setupTestDB(t testing.TB) *sqlx.DB { - t.Helper() - - driverName := getDriverName() - sqlSettings := storetest.MakeSqlSettings(driverName, false) - - origDB, err := sql.Open(*sqlSettings.DriverName, *sqlSettings.DataSource) - require.NoError(t, err) - - db := sqlx.NewDb(origDB, driverName) - if driverName == model.DatabaseDriverMysql { - db.MapperFunc(func(s string) string { return s }) - } - - t.Cleanup(func() { - err := db.Close() - require.NoError(t, err) - storetest.CleanupSqlSettings(sqlSettings) - }) - - return db -} - -func setupTables(t *testing.T, db *sqlx.DB) *SQLStore { - t.Helper() - - mockCtrl := gomock.NewController(t) - scheduler := mock_app.NewMockJobOnceScheduler(mockCtrl) - - driverName := db.DriverName() - - builder := sq.StatementBuilder.PlaceholderFormat(sq.Question) - if driverName == model.DatabaseDriverPostgres { - builder = builder.PlaceholderFormat(sq.Dollar) - } - - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - setupChannelsTable(t, db) - setupPostsTable(t, db) - setupBotsTable(t, db) - setupChannelMembersTable(t, db) - setupKVStoreTable(t, db) - setupUsersTable(t, db) - setupTeamsTable(t, db) - setupRolesTable(t, db) - setupSchemesTable(t, db) - setupTeamMembersTable(t, db) - - return sqlStore -} - -func setupSQLStore(t *testing.T, db *sqlx.DB) *SQLStore { - sqlStore := setupTables(t, db) - - err := sqlStore.RunMigrations() - require.NoError(t, err) - - return sqlStore -} - -func setupUsersTable(t *testing.T, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - // NOTE: for this and the other tables below, this is a now out-of-date schema, which doesn't - // reflect any of the changes past v5.0. If the test code requires a new column, you will - // need to update these tables accordingly. - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.users ( - id character varying(26) NOT NULL, - createat bigint, - updateat bigint, - deleteat bigint, - username character varying(64), - password character varying(128), - authdata character varying(128), - authservice character varying(32), - email character varying(128), - emailverified boolean, - nickname character varying(64), - firstname character varying(64), - lastname character varying(64), - "position" character varying(128), - roles character varying(256), - allowmarketing boolean, - props character varying(4000), - notifyprops character varying(2000), - lastpasswordupdate bigint, - lastpictureupdate bigint, - failedattempts integer, - locale character varying(5), - timezone character varying(256), - mfaactive boolean, - mfasecret character varying(128), - PRIMARY KEY (Id) - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Users ( - Id varchar(26) NOT NULL, - CreateAt bigint(20) DEFAULT NULL, - UpdateAt bigint(20) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - Username varchar(64) DEFAULT NULL, - Password varchar(128) DEFAULT NULL, - AuthData varchar(128) DEFAULT NULL, - AuthService varchar(32) DEFAULT NULL, - Email varchar(128) DEFAULT NULL, - EmailVerified tinyint(1) DEFAULT NULL, - Nickname varchar(64) DEFAULT NULL, - FirstName varchar(64) DEFAULT NULL, - LastName varchar(64) DEFAULT NULL, - Position varchar(128) DEFAULT NULL, - Roles text, - AllowMarketing tinyint(1) DEFAULT NULL, - Props text, - NotifyProps text, - LastPasswordUpdate bigint(20) DEFAULT NULL, - LastPictureUpdate bigint(20) DEFAULT NULL, - FailedAttempts int(11) DEFAULT NULL, - Locale varchar(5) DEFAULT NULL, - Timezone text, - MfaActive tinyint(1) DEFAULT NULL, - MfaSecret varchar(128) DEFAULT NULL, - PRIMARY KEY (Id), - UNIQUE KEY Username (Username), - UNIQUE KEY AuthData (AuthData), - UNIQUE KEY Email (Email), - KEY idx_users_email (Email), - KEY idx_users_update_at (UpdateAt), - KEY idx_users_create_at (CreateAt), - KEY idx_users_delete_at (DeleteAt), - FULLTEXT KEY idx_users_all_txt (Username,FirstName,LastName,Nickname,Email), - FULLTEXT KEY idx_users_all_no_full_name_txt (Username,Nickname,Email), - FULLTEXT KEY idx_users_names_txt (Username,FirstName,LastName,Nickname), - FULLTEXT KEY idx_users_names_no_full_name_txt (Username,Nickname) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupChannelMemberHistoryTable(t *testing.T, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.channelmemberhistory ( - channelid character varying(26) NOT NULL, - userid character varying(26) NOT NULL, - jointime bigint NOT NULL, - leavetime bigint - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS ChannelMemberHistory ( - ChannelId varchar(26) NOT NULL, - UserId varchar(26) NOT NULL, - JoinTime bigint(20) NOT NULL, - LeaveTime bigint(20) DEFAULT NULL, - PRIMARY KEY (ChannelId,UserId,JoinTime) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupTeamMembersTable(t *testing.T, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.teammembers ( - teamid character varying(26) NOT NULL, - userid character varying(26) NOT NULL, - roles character varying(64), - deleteat bigint, - schemeuser boolean, - schemeadmin boolean - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS TeamMembers ( - TeamId varchar(26) NOT NULL, - UserId varchar(26) NOT NULL, - Roles varchar(64) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - SchemeUser tinyint(4) DEFAULT NULL, - SchemeAdmin tinyint(4) DEFAULT NULL, - PRIMARY KEY (TeamId,UserId), - KEY idx_teammembers_team_id (TeamId), - KEY idx_teammembers_user_id (UserId), - KEY idx_teammembers_delete_at (DeleteAt) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupChannelMembersTable(t *testing.T, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.channelmembers ( - channelid character varying(26) NOT NULL, - userid character varying(26) NOT NULL, - roles character varying(64), - lastviewedat bigint, - msgcount bigint, - mentioncount bigint, - notifyprops character varying(2000), - lastupdateat bigint, - schemeuser boolean, - PRIMARY KEY (ChannelId,UserId), - schemeadmin boolean - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS ChannelMembers ( - ChannelId varchar(26) NOT NULL, - UserId varchar(26) NOT NULL, - Roles varchar(64) DEFAULT NULL, - LastViewedAt bigint(20) DEFAULT NULL, - MsgCount bigint(20) DEFAULT NULL, - MentionCount bigint(20) DEFAULT NULL, - NotifyProps text, - LastUpdateAt bigint(20) DEFAULT NULL, - SchemeUser tinyint(4) DEFAULT NULL, - SchemeAdmin tinyint(4) DEFAULT NULL, - PRIMARY KEY (ChannelId,UserId), - KEY idx_channelmembers_channel_id (ChannelId), - KEY idx_channelmembers_user_id (UserId) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupChannelsTable(t *testing.T, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.channels ( - id character varying(26) NOT NULL, - createat bigint, - updateat bigint, - deleteat bigint, - teamid character varying(26), - type character varying(1), - displayname character varying(64), - name character varying(64), - header character varying(1024), - purpose character varying(250), - lastpostat bigint, - totalmsgcount bigint, - extraupdateat bigint, - creatorid character varying(26), - PRIMARY KEY (Id), - schemeid character varying(26) - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Channels ( - Id varchar(26) NOT NULL, - CreateAt bigint(20) DEFAULT NULL, - UpdateAt bigint(20) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - TeamId varchar(26) DEFAULT NULL, - Type varchar(1) DEFAULT NULL, - DisplayName varchar(64) DEFAULT NULL, - Name varchar(64) DEFAULT NULL, - Header text, - Purpose varchar(250) DEFAULT NULL, - LastPostAt bigint(20) DEFAULT NULL, - TotalMsgCount bigint(20) DEFAULT NULL, - ExtraUpdateAt bigint(20) DEFAULT NULL, - CreatorId varchar(26) DEFAULT NULL, - SchemeId varchar(26) DEFAULT NULL, - PRIMARY KEY (Id), - UNIQUE KEY Name (Name,TeamId), - KEY idx_channels_team_id (TeamId), - KEY idx_channels_name (Name), - KEY idx_channels_update_at (UpdateAt), - KEY idx_channels_create_at (CreateAt), - KEY idx_channels_delete_at (DeleteAt), - FULLTEXT KEY idx_channels_txt (Name,DisplayName) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupPostsTable(t testing.TB, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.posts ( - id character varying(26) NOT NULL, - createat bigint, - updateat bigint, - editat bigint, - deleteat bigint, - ispinned boolean, - userid character varying(26), - channelid character varying(26), - rootid character varying(26), - parentid character varying(26), - originalid character varying(26), - message character varying(65535), - type character varying(26), - props character varying(8000), - hashtags character varying(1000), - filenames character varying(4000), - fileids character varying(150), - PRIMARY KEY (Id), - hasreactions boolean - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Posts ( - Id varchar(26) NOT NULL, - CreateAt bigint(20) DEFAULT NULL, - UpdateAt bigint(20) DEFAULT NULL, - EditAt bigint(20) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - IsPinned tinyint(1) DEFAULT NULL, - UserId varchar(26) DEFAULT NULL, - ChannelId varchar(26) DEFAULT NULL, - RootId varchar(26) DEFAULT NULL, - ParentId varchar(26) DEFAULT NULL, - OriginalId varchar(26) DEFAULT NULL, - Message text, - Type varchar(26) DEFAULT NULL, - Props text, - Hashtags text, - Filenames text, - FileIds varchar(150) DEFAULT NULL, - HasReactions tinyint(1) DEFAULT NULL, - PRIMARY KEY (Id), - KEY idx_posts_update_at (UpdateAt), - KEY idx_posts_create_at (CreateAt), - KEY idx_posts_delete_at (DeleteAt), - KEY idx_posts_channel_id (ChannelId), - KEY idx_posts_root_id (RootId), - KEY idx_posts_user_id (UserId), - KEY idx_posts_is_pinned (IsPinned), - KEY idx_posts_channel_id_update_at (ChannelId,UpdateAt), - KEY idx_posts_channel_id_delete_at_create_at (ChannelId,DeleteAt,CreateAt), - FULLTEXT KEY idx_posts_message_txt (Message), - FULLTEXT KEY idx_posts_hashtags_txt (Hashtags) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupTeamsTable(t testing.TB, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-6.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.teams ( - id character varying(26) NOT NULL, - PRIMARY KEY (Id), - createat bigint, - updateat bigint, - deleteat bigint, - displayname character varying(64), - name character varying(64), - description character varying(255), - email character varying(128), - type character varying(255), - companyname character varying(64), - alloweddomains character varying(1000), - inviteid character varying(32), - schemeid character varying(26), - allowopeninvite boolean, - lastteamiconupdate bigint, - groupconstrained boolean - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-6.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Teams ( - Id varchar(26) NOT NULL, - CreateAt bigint(20) DEFAULT NULL, - UpdateAt bigint(20) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - DisplayName varchar(64) DEFAULT NULL, - Name varchar(64) DEFAULT NULL, - Description varchar(255) DEFAULT NULL, - Email varchar(128) DEFAULT NULL, - Type varchar(255) DEFAULT NULL, - CompanyName varchar(64) DEFAULT NULL, - AllowedDomains text, - InviteId varchar(32) DEFAULT NULL, - SchemeId varchar(26) DEFAULT NULL, - AllowOpenInvite tinyint(1) DEFAULT NULL, - LastTeamIconUpdate bigint(20) DEFAULT NULL, - GroupConstrained tinyint(1) DEFAULT NULL, - PRIMARY KEY (Id), - UNIQUE KEY Name (Name), - KEY idx_teams_invite_id (InviteId), - KEY idx_teams_update_at (UpdateAt), - KEY idx_teams_create_at (CreateAt), - KEY idx_teams_delete_at (DeleteAt), - KEY idx_teams_scheme_id (SchemeId) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupRolesTable(t testing.TB, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-6.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.roles ( - id character varying(26) NOT NULL, - PRIMARY KEY (Id), - name character varying(64), - displayname character varying(128), - description character varying(1024), - createat bigint, - updateat bigint, - deleteat bigint, - permissions text, - schememanaged boolean, - builtin boolean - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-6.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Roles ( - Id varchar(26) NOT NULL, - Name varchar(64) DEFAULT NULL, - DisplayName varchar(128) DEFAULT NULL, - Description text, - CreateAt bigint(20) DEFAULT NULL, - UpdateAt bigint(20) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - Permissions text, - SchemeManaged tinyint(1) DEFAULT NULL, - BuiltIn tinyint(1) DEFAULT NULL, - PRIMARY KEY (Id), - UNIQUE KEY Name (Name) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupSchemesTable(t testing.TB, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-6.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.schemes ( - id character varying(26) NOT NULL, - PRIMARY KEY (Id), - name character varying(64), - displayname character varying(128), - description character varying(1024), - createat bigint, - updateat bigint, - deleteat bigint, - scope character varying(32), - defaultteamadminrole character varying(64), - defaultteamuserrole character varying(64), - defaultchanneladminrole character varying(64), - defaultchanneluserrole character varying(64), - defaultteamguestrole character varying(64), - defaultchannelguestrole character varying(64), - defaultplaybookadminrole character varying(64), - defaultplaybookmemberrole character varying(64), - defaultrunadminrole character varying(64), - defaultrunmemberrole character varying(64) - ); - `) - require.NoError(t, err) - - return - } - - // Statements copied from mattermost-server/scripts/mattermost-mysql-6.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Schemes ( - Id varchar(26) NOT NULL, - Name varchar(64) DEFAULT NULL, - DisplayName varchar(128) DEFAULT NULL, - Description text, - CreateAt bigint(20) DEFAULT NULL, - UpdateAt bigint(20) DEFAULT NULL, - DeleteAt bigint(20) DEFAULT NULL, - Scope varchar(32) DEFAULT NULL, - DefaultTeamAdminRole varchar(64) DEFAULT NULL, - DefaultTeamUserRole varchar(64) DEFAULT NULL, - DefaultChannelAdminRole varchar(64) DEFAULT NULL, - DefaultChannelUserRole varchar(64) DEFAULT NULL, - DefaultTeamGuestRole varchar(64) DEFAULT NULL, - DefaultChannelGuestRole varchar(64) DEFAULT NULL, - DefaultPlaybookAdminRole varchar(64) DEFAULT NULL, - DefaultPlaybookMemberRole varchar(64) DEFAULT NULL, - DefaultRunAdminRole varchar(64) DEFAULT NULL, - DefaultRunMemberRole varchar(64) DEFAULT NULL, - PRIMARY KEY (Id), - UNIQUE KEY Name (Name), - KEY idx_schemes_channel_guest_role (DefaultChannelGuestRole), - KEY idx_schemes_channel_user_role (DefaultChannelUserRole), - KEY idx_schemes_channel_admin_role (DefaultChannelAdminRole) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupBotsTable(t testing.TB, db *sqlx.DB) { - t.Helper() - - // This is completely handmade - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.bots ( - userid character varying(26) NOT NULL PRIMARY KEY, - description character varying(1024), - ownerid character varying(190) - ); - `) - require.NoError(t, err) - - return - } - - // handmade - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS Bots ( - UserId varchar(26) NOT NULL PRIMARY KEY, - Description varchar(1024), - OwnerId varchar(190) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) -} - -func setupKVStoreTable(t *testing.T, db *sqlx.DB) { - t.Helper() - - // Statements copied from mattermost-server/scripts/mattermost-postgresql-5.0.sql - if db.DriverName() == model.DatabaseDriverPostgres { - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS public.pluginkeyvaluestore ( - pluginid character varying(190) NOT NULL, - pkey character varying(50) NOT NULL, - pvalue bytea, - expireat bigint, - PRIMARY KEY (PluginId,PKey) - ); - `) - require.NoError(t, err) - } else { - // Statements copied from mattermost-server/scripts/mattermost-mysql-5.0.sql - _, err := db.Exec(` - CREATE TABLE IF NOT EXISTS PluginKeyValueStore ( - PluginId varchar(190) NOT NULL, - PKey varchar(50) NOT NULL, - PValue mediumblob, - ExpireAt bigint(20) DEFAULT NULL, - PRIMARY KEY (PluginId,PKey) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - `) - require.NoError(t, err) - } - -} - -type userInfo struct { - ID string - Name string -} - -func addUsers(t *testing.T, store *SQLStore, users []userInfo) { - t.Helper() - - insertBuilder := store.builder.Insert("Users").Columns("ID", "Username") - - for _, u := range users { - insertBuilder = insertBuilder.Values(u.ID, u.Name) - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func addBots(t *testing.T, store *SQLStore, bots []userInfo) { - t.Helper() - - insertBuilder := store.builder.Insert("Bots").Columns("UserId", "Description") - - for _, u := range bots { - insertBuilder = insertBuilder.Values(u.ID, u.Name) - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func addUsersToTeam(t *testing.T, store *SQLStore, users []userInfo, teamID string) { - t.Helper() - - insertBuilder := store.builder.Insert("TeamMembers").Columns("TeamId", "UserId", "DeleteAt") - - for _, u := range users { - insertBuilder = insertBuilder.Values(teamID, u.ID, 0) - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func addUsersToChannels(t *testing.T, store *SQLStore, users []userInfo, channelIDs []string) { - t.Helper() - - insertBuilder := store.builder.Insert("ChannelMembers").Columns("ChannelId", "UserId") - - for _, u := range users { - for _, c := range channelIDs { - insertBuilder = insertBuilder.Values(c, u.ID) - } - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func addUsersToRuns(t *testing.T, store *SQLStore, users []userInfo, runIDs []string) { - t.Helper() - - insertBuilder := store.builder.Insert("IR_Run_Participants").Columns("IncidentID", "UserId", "IsParticipant", "IsFollower") - - for _, u := range users { - for _, runID := range runIDs { - insertBuilder = insertBuilder.Values(runID, u.ID, true, false) - } - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func createChannels(t testing.TB, store *SQLStore, channels []model.Channel) { - t.Helper() - - insertBuilder := store.builder.Insert("Channels").Columns("Id", "DisplayName", "Type", "CreateAt", "DeleteAt", "Name") - - for _, channel := range channels { - insertBuilder = insertBuilder.Values(channel.Id, channel.DisplayName, channel.Type, channel.CreateAt, channel.DeleteAt, channel.Name) - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func createTeams(t testing.TB, store *SQLStore, teams []model.Team) { - t.Helper() - - insertBuilder := store.builder.Insert("Teams").Columns("Id", "Name") - - for _, team := range teams { - insertBuilder = insertBuilder.Values(team.Id, team.Name) - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func createPlaybookRunChannel(t testing.TB, store *SQLStore, playbookRun *app.PlaybookRun) { - t.Helper() - - if playbookRun.CreateAt == 0 { - playbookRun.CreateAt = model.GetMillis() - } - - insertBuilder := store.builder.Insert("Channels").Columns("Id", "DisplayName", "CreateAt", "DeleteAt").Values(playbookRun.ChannelID, playbookRun.Name, playbookRun.CreateAt, 0) - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func makeAdmin(t *testing.T, store *SQLStore, user userInfo) { - t.Helper() - - updateBuilder := store.builder. - Update("Users"). - Where(sq.Eq{"Id": user.ID}). - Set("Roles", "role1 role2 system_admin role3") - - _, err := store.execBuilder(store.db, updateBuilder) - require.NoError(t, err) -} - -func savePosts(t testing.TB, store *SQLStore, posts []*model.Post) { - t.Helper() - - insertBuilder := store.builder.Insert("Posts").Columns("Id", "CreateAt", "DeleteAt") - - for _, p := range posts { - insertBuilder = insertBuilder.Values(p.Id, p.CreateAt, p.DeleteAt) - } - - _, err := store.execBuilder(store.db, insertBuilder) - require.NoError(t, err) -} - -func migrateUpTo(t *testing.T, store *SQLStore, lastExpectedVersion semver.Version) { - t.Helper() - - for _, migration := range migrations { - if migration.toVersion.GT(lastExpectedVersion) { - break - } - - err := store.migrate(migration) - require.NoError(t, err) - - currentSchemaVersion, err := store.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, migration.toVersion) - } - - currentSchemaVersion, err := store.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, lastExpectedVersion) -} - -func migrateFrom(t *testing.T, store *SQLStore, firstExpectedVersion semver.Version) { - t.Helper() - - currentSchemaVersion, err := store.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, firstExpectedVersion) - - for _, migration := range migrations { - if migration.toVersion.LE(firstExpectedVersion) { - continue - } - - err := store.migrate(migration) - require.NoError(t, err) - - currentSchemaVersion, err := store.GetCurrentVersion() - require.NoError(t, err) - require.Equal(t, currentSchemaVersion, migration.toVersion) - } -} diff --git a/server/playbooks/server/sqlstore/system.go b/server/playbooks/server/sqlstore/system.go deleted file mode 100644 index 0c862ce90d6..00000000000 --- a/server/playbooks/server/sqlstore/system.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" -) - -// getSystemValue queries the IR_System table for the given key -func (sqlStore *SQLStore) getSystemValue(q queryer, key string) (string, error) { - var value string - - err := sqlStore.getBuilder(q, &value, - sq.Select("SValue"). - From("IR_System"). - Where(sq.Eq{"SKey": key}), - ) - if err == sql.ErrNoRows { - return "", nil - } else if err != nil { - return "", errors.Wrapf(err, "failed to query system key %s", key) - } - - return value, nil -} - -// setSystemValue updates the IR_System table for the given key. -func (sqlStore *SQLStore) setSystemValue(e queryExecer, key, value string) error { - // MySQL reports 0 rows affected in the update below when the key and value - // already exist. We can use its native support for upsert instead. Postgres - // 9.4 does not have native support for upsert, but it reports 1 row - // affected even when the key and value are already present. - if sqlStore.db.DriverName() == model.DatabaseDriverMysql { - _, err := sqlStore.execBuilder(e, - sq.Insert("IR_System"). - Columns("SKey", "SValue"). - Values(key, value). - Suffix("ON DUPLICATE KEY UPDATE SValue = ?", value), - ) - - return err - } - - result, err := sqlStore.execBuilder(e, - sq.Update("IR_System"). - Set("SValue", value). - Where(sq.Eq{"SKey": key}), - ) - if err != nil { - return errors.Wrapf(err, "failed to update system key %s", key) - } - - rowsAffected, _ := result.RowsAffected() - if rowsAffected > 0 { - return nil - } - - _, err = sqlStore.execBuilder(e, - sq.Insert("IR_System"). - Columns("SKey", "SValue"). - Values(key, value), - ) - if err != nil { - return errors.Wrapf(err, "failed to insert system key %s", key) - } - - return nil -} diff --git a/server/playbooks/server/sqlstore/timeline_event_test.go b/server/playbooks/server/sqlstore/timeline_event_test.go deleted file mode 100644 index 6a70cf566ad..00000000000 --- a/server/playbooks/server/sqlstore/timeline_event_test.go +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" -) - -func TestPlaybookRunStore_CreateTimelineEvent(t *testing.T) { - db := setupTestDB(t) - iStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - setupChannelsTable(t, db) - setupPostsTable(t, db) - - t.Run("Save and retrieve 4 timeline events", func(t *testing.T) { - createAt := model.GetMillis() - inc01 := NewBuilder(nil). - WithName("playbook run 1"). - WithCreateAt(createAt). - WithChecklists([]int{8}). - ToPlaybookRun() - - playbookRun, err := iStore.CreatePlaybookRun(inc01) - require.NoError(t, err) - - createPlaybookRunChannel(t, store, inc01) - - event1 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt, - EventAt: 1234, - EventType: app.PlaybookRunCreated, - Summary: "this is a summary", - Details: "these are the details", - PostID: "testpostID", - SubjectUserID: "testuserID", - CreatorUserID: "testUserID2", - } - _, err = iStore.CreateTimelineEvent(event1) - require.NoError(t, err) - - event2 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt + 1, - EventAt: 1235, - EventType: app.AssigneeChanged, - Summary: "this is a summary", - Details: "these are the details", - PostID: "testpostID2", - SubjectUserID: "testuserID", - CreatorUserID: "testUserID2", - } - _, err = iStore.CreateTimelineEvent(event2) - require.NoError(t, err) - - event3 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt + 2, - EventAt: 1236, - EventType: app.StatusUpdated, - Summary: "this is a summary", - Details: "these are the details", - PostID: "testpostID3", - SubjectUserID: "testuserID", - CreatorUserID: "testUserID2", - } - _, err = iStore.CreateTimelineEvent(event3) - require.NoError(t, err) - - event4 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt + 3, - EventAt: 123734, - EventType: app.StatusUpdated, - Summary: "this is a summary", - Details: "these are the details", - PostID: "testpostID4", - SubjectUserID: "testuserID", - CreatorUserID: "testUserID2", - } - _, err = iStore.CreateTimelineEvent(event4) - require.NoError(t, err) - - retPlaybookRun, err := iStore.GetPlaybookRun(playbookRun.ID) - require.NoError(t, err) - - require.Len(t, retPlaybookRun.TimelineEvents, 4) - require.Equal(t, *event1, retPlaybookRun.TimelineEvents[0]) - require.Equal(t, *event2, retPlaybookRun.TimelineEvents[1]) - require.Equal(t, *event3, retPlaybookRun.TimelineEvents[2]) - require.Equal(t, *event4, retPlaybookRun.TimelineEvents[3]) - }) -} - -func TestPlaybookRunStore_UpdateTimelineEvent(t *testing.T) { - db := setupTestDB(t) - iStore := setupPlaybookRunStore(t, db) - store := setupSQLStore(t, db) - setupChannelsTable(t, db) - setupPostsTable(t, db) - - t.Run("Save 4 and delete 2 timeline events", func(t *testing.T) { - createAt := model.GetMillis() - inc01 := NewBuilder(nil). - WithName("playbook run 1"). - WithCreateAt(createAt). - WithChecklists([]int{8}). - ToPlaybookRun() - - playbookRun, err := iStore.CreatePlaybookRun(inc01) - require.NoError(t, err) - - createPlaybookRunChannel(t, store, inc01) - - event1 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt, - EventAt: createAt, - EventType: app.PlaybookRunCreated, - PostID: "testpostID", - SubjectUserID: "testuserID", - } - _, err = iStore.CreateTimelineEvent(event1) - require.NoError(t, err) - - event2 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt + 1, - EventAt: createAt + 1, - EventType: app.AssigneeChanged, - PostID: "testpostID2", - SubjectUserID: "testuserID", - } - _, err = iStore.CreateTimelineEvent(event2) - require.NoError(t, err) - - event3 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt + 2, - EventAt: createAt + 2, - EventType: app.StatusUpdated, - Summary: "this is a summary", - Details: "these are the details", - PostID: "testpostID3", - SubjectUserID: "testuserID", - CreatorUserID: "testUserID2", - } - _, err = iStore.CreateTimelineEvent(event3) - require.NoError(t, err) - - event4 := &app.TimelineEvent{ - PlaybookRunID: playbookRun.ID, - CreateAt: createAt + 3, - EventAt: createAt + 3, - EventType: app.StatusUpdated, - PostID: "testpostID4", - SubjectUserID: "testuserID", - } - _, err = iStore.CreateTimelineEvent(event4) - require.NoError(t, err) - - retPlaybookRun, err := iStore.GetPlaybookRun(playbookRun.ID) - require.NoError(t, err) - - require.Len(t, retPlaybookRun.TimelineEvents, 4) - require.Equal(t, *event1, retPlaybookRun.TimelineEvents[0]) - require.Equal(t, *event2, retPlaybookRun.TimelineEvents[1]) - require.Equal(t, *event3, retPlaybookRun.TimelineEvents[2]) - require.Equal(t, *event4, retPlaybookRun.TimelineEvents[3]) - - event3.DeleteAt = model.GetMillis() - event3.EventType = app.AssigneeChanged - event3.Summary = "new summary" - event3.Details = "new details" - event3.PostID = "23abc34" - event3.SubjectUserID = "23409agbcef" - event3.CreatorUserID = "someoneelse" - err = iStore.UpdateTimelineEvent(event3) - require.NoError(t, err) - - event4.DeleteAt = model.GetMillis() - err = iStore.UpdateTimelineEvent(event4) - require.NoError(t, err) - - retPlaybookRun, err = iStore.GetPlaybookRun(playbookRun.ID) - require.NoError(t, err) - - require.Len(t, retPlaybookRun.TimelineEvents, 2) - require.Equal(t, *event1, retPlaybookRun.TimelineEvents[0]) - require.Equal(t, *event2, retPlaybookRun.TimelineEvents[1]) - }) -} diff --git a/server/playbooks/server/sqlstore/user_info.go b/server/playbooks/server/sqlstore/user_info.go deleted file mode 100644 index 43c4a7c27cb..00000000000 --- a/server/playbooks/server/sqlstore/user_info.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "database/sql" - "encoding/json" - - "github.com/mattermost/mattermost/server/public/model" - - sq "github.com/Masterminds/squirrel" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - "github.com/pkg/errors" -) - -type sqlUserInfo struct { - app.UserInfo - DigestNotificationSettingsJSON json.RawMessage -} - -type userInfoStore struct { - store *SQLStore - queryBuilder sq.StatementBuilderType - userInfoSelect sq.SelectBuilder -} - -// Ensure userInfoStore implements the userInfo.Store interface -var _ app.UserInfoStore = (*userInfoStore)(nil) - -func NewUserInfoStore(sqlStore *SQLStore) app.UserInfoStore { - userInfoSelect := sqlStore.builder. - Select("ID", "LastDailyTodoDMAt", "COALESCE(DigestNotificationSettingsJSON, '{}') DigestNotificationSettingsJSON"). - From("IR_UserInfo") - - newStore := &userInfoStore{ - store: sqlStore, - queryBuilder: sqlStore.builder, - userInfoSelect: userInfoSelect, - } - return newStore -} - -// Get retrieves a UserInfo struct by the user's userID. -func (s *userInfoStore) Get(userID string) (app.UserInfo, error) { - var raw sqlUserInfo - err := s.store.getBuilder(s.store.db, &raw, s.userInfoSelect.Where(sq.Eq{"ID": userID})) - if err == sql.ErrNoRows { - return app.UserInfo{}, errors.Wrapf(app.ErrNotFound, "userInfo does not exist for userId '%s'", userID) - } else if err != nil { - return app.UserInfo{}, errors.Wrapf(err, "failed to get userInfo by userId '%s'", userID) - } - - return toUserInfo(raw) -} - -// Upsert inserts (creates) or updates the UserInfo in info. -func (s *userInfoStore) Upsert(info app.UserInfo) error { - if info.ID == "" { - return errors.New("ID should not be empty") - } - raw, err := toSQLUserInfo(info) - if err != nil { - return err - } - - if s.store.db.DriverName() == model.DatabaseDriverMysql { - _, err = s.store.execBuilder(s.store.db, - sq.Insert("IR_UserInfo"). - Columns("ID", "LastDailyTodoDMAt", "DigestNotificationSettingsJSON"). - Values(raw.ID, raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON). - Suffix("ON DUPLICATE KEY UPDATE LastDailyTodoDMAt = ?, DigestNotificationSettingsJSON = ?", - raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON)) - } else { - _, err = s.store.execBuilder(s.store.db, - sq.Insert("IR_UserInfo"). - Columns("ID", "LastDailyTodoDMAt", "DigestNotificationSettingsJSON"). - Values(raw.ID, raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON). - Suffix("ON CONFLICT (ID) DO UPDATE SET LastDailyTodoDMAt = ?, DigestNotificationSettingsJSON = ?", - raw.LastDailyTodoDMAt, raw.DigestNotificationSettingsJSON)) - } - - if err != nil { - return errors.Wrapf(err, "failed to upsert userInfo with id '%s'", raw.ID) - } - - return nil -} - -func toUserInfo(rawUserInfo sqlUserInfo) (app.UserInfo, error) { - userInfo := rawUserInfo.UserInfo - if len(rawUserInfo.DigestNotificationSettingsJSON) == 0 { - return userInfo, nil - } - - if err := json.Unmarshal(rawUserInfo.DigestNotificationSettingsJSON, &userInfo.DigestNotificationSettings); err != nil { - return userInfo, errors.Wrapf(err, "failed to unmarshal DigestNotificationSettings for userid: %s", userInfo.ID) - } - - return userInfo, nil -} - -func toSQLUserInfo(userInfo app.UserInfo) (*sqlUserInfo, error) { - digestNotificationSettingsJSON, err := json.Marshal(userInfo.DigestNotificationSettings) - if err != nil { - return nil, errors.Wrapf(err, "failed to marshal DigestNotificationSettings for userid: %s", userInfo.ID) - } - - if len(digestNotificationSettingsJSON) > maxJSONLength { - return nil, errors.Wrapf(errors.New("invalid data"), "digestNotificationSettings json for user id '%s' is too long (max %d)", userInfo.ID, maxJSONLength) - } - - return &sqlUserInfo{ - UserInfo: userInfo, - DigestNotificationSettingsJSON: digestNotificationSettingsJSON, - }, nil -} diff --git a/server/playbooks/server/sqlstore/user_info_test.go b/server/playbooks/server/sqlstore/user_info_test.go deleted file mode 100644 index 69221cefcbb..00000000000 --- a/server/playbooks/server/sqlstore/user_info_test.go +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "reflect" - "testing" - - mock_app "github.com/mattermost/mattermost/server/v8/playbooks/server/app/mocks" - - "github.com/pkg/errors" - - sq "github.com/Masterminds/squirrel" - "github.com/golang/mock/gomock" - "github.com/jmoiron/sqlx" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" -) - -func Test_userInfoStore_Get(t *testing.T) { - db := setupTestDB(t) - userInfoStore := setupUserInfoStore(t, db) - - t.Run("gets existing userInfo correctly", func(t *testing.T) { - expected := app.UserInfo{ - ID: model.NewId(), - LastDailyTodoDMAt: 12345678, - DigestNotificationSettings: app.DigestNotificationSettings{DisableDailyDigest: false, DisableWeeklyDigest: false}, - } - err := userInfoStore.Upsert(expected) - require.NoError(t, err) - - actual, err := userInfoStore.Get(expected.ID) - require.NoError(t, err) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - }) - - t.Run("gets non-existing userInfo correctly", func(t *testing.T) { - expected := app.UserInfo{} - actual, err := userInfoStore.Get(model.NewId()) - require.Error(t, err) - require.True(t, errors.Is(err, app.ErrNotFound)) - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - }) - - t.Run("gets null DigestNotificationSettingsJSON correctly", func(t *testing.T) { - expected := app.UserInfo{ - ID: model.NewId(), - LastDailyTodoDMAt: 12345678, - DigestNotificationSettings: app.DigestNotificationSettings{DisableDailyDigest: false, DisableWeeklyDigest: false}, - } - - statement, args, err := sq.Insert("IR_UserInfo"). - Columns("ID", "LastDailyTodoDMAt", "DigestNotificationSettingsJSON"). - Values(expected.ID, expected.LastDailyTodoDMAt, nil).ToSql() - require.NoError(t, err) - _, err = db.Exec(db.Rebind(statement), args...) - require.NoError(t, err) - - actual, err := userInfoStore.Get(expected.ID) - require.NoError(t, err) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - }) -} - -func Test_userInfoStore_Upsert(t *testing.T) { - db := setupTestDB(t) - userInfoStore := setupUserInfoStore(t, db) - - t.Run("inserts userInfo correctly", func(t *testing.T) { - userID := model.NewId() - expected := app.UserInfo{} - - // assert doesn't exist yet: - actual, err := userInfoStore.Get(expected.ID) - require.Error(t, err) - require.True(t, errors.Is(err, app.ErrNotFound)) - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - - // insert: - expected = app.UserInfo{ - ID: userID, - LastDailyTodoDMAt: 12345678, - DigestNotificationSettings: app.DigestNotificationSettings{DisableDailyDigest: false, DisableWeeklyDigest: false}, - } - - err = userInfoStore.Upsert(expected) - require.NoError(t, err) - - actual, err = userInfoStore.Get(expected.ID) - require.NoError(t, err) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - }) - - t.Run("upserts userInfo correctly", func(t *testing.T) { - expected := app.UserInfo{ - ID: model.NewId(), - LastDailyTodoDMAt: 12345678, - DigestNotificationSettings: app.DigestNotificationSettings{DisableDailyDigest: false, DisableWeeklyDigest: false}, - } - - // insert: - err := userInfoStore.Upsert(expected) - require.NoError(t, err) - - actual, err := userInfoStore.Get(expected.ID) - require.NoError(t, err) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - - // update: - expected.LastDailyTodoDMAt = 48102939451 - expected.DisableDailyDigest = true - err = userInfoStore.Upsert(expected) - require.NoError(t, err) - - actual, err = userInfoStore.Get(expected.ID) - require.NoError(t, err) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - - // update dailyDigest one more time: - expected.DisableDailyDigest = false - err = userInfoStore.Upsert(expected) - require.NoError(t, err) - - actual, err = userInfoStore.Get(expected.ID) - require.NoError(t, err) - - if !reflect.DeepEqual(actual, expected) { - t.Errorf("Get() actual = %#v, expected %#v", actual, expected) - } - }) -} - -func setupUserInfoStore(t *testing.T, db *sqlx.DB) app.UserInfoStore { - sqlStore := setupSQLStoreForUserInfo(t, db) - - return NewUserInfoStore(sqlStore) -} - -func setupSQLStoreForUserInfo(t *testing.T, db *sqlx.DB) *SQLStore { - t.Helper() - - mockCtrl := gomock.NewController(t) - scheduler := mock_app.NewMockJobOnceScheduler(mockCtrl) - - driverName := db.DriverName() - - builder := sq.StatementBuilder.PlaceholderFormat(sq.Question) - if driverName == model.DatabaseDriverPostgres { - builder = builder.PlaceholderFormat(sq.Dollar) - } - - sqlStore := &SQLStore{ - db, - builder, - scheduler, - } - - setupChannelsTable(t, db) - setupPostsTable(t, db) - setupKVStoreTable(t, db) - setupTeamMembersTable(t, db) - setupChannelMembersTable(t, db) - setupBotsTable(t, db) - - err := sqlStore.RunMigrations() - require.NoError(t, err) - - return sqlStore -} diff --git a/server/playbooks/server/sqlstore/versions.go b/server/playbooks/server/sqlstore/versions.go deleted file mode 100644 index 84eab544e0e..00000000000 --- a/server/playbooks/server/sqlstore/versions.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package sqlstore - -import ( - "github.com/blang/semver" - "github.com/pkg/errors" -) - -const systemDatabaseVersionKey = "DatabaseVersion" - -func LatestVersion() semver.Version { - return migrations[len(migrations)-1].toVersion -} - -func (sqlStore *SQLStore) GetCurrentVersion() (semver.Version, error) { - currentVersionStr, err := sqlStore.getSystemValue(sqlStore.db, systemDatabaseVersionKey) - - if currentVersionStr == "" { - return semver.Version{}, nil - } - - if err != nil { - return semver.Version{}, errors.Wrapf(err, "failed retrieving the DatabaseVersion key from the IR_System table") - } - - currentSchemaVersion, err := semver.Parse(currentVersionStr) - if err != nil { - return semver.Version{}, errors.Wrapf(err, "unable to parse current schema version") - } - - return currentSchemaVersion, nil -} - -func (sqlStore *SQLStore) SetCurrentVersion(e queryExecer, currentVersion semver.Version) error { - return sqlStore.setSystemValue(e, systemDatabaseVersionKey, currentVersion.String()) -} diff --git a/server/playbooks/server/telemetry/noop.go b/server/playbooks/server/telemetry/noop.go deleted file mode 100644 index 5801ad65988..00000000000 --- a/server/playbooks/server/telemetry/noop.go +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package telemetry - -import ( - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" -) - -// NoopTelemetry satisfies the Telemetry interface with no-op implementations. -type NoopTelemetry struct { -} - -// Enable does nothing, returning always nil. -func (t *NoopTelemetry) Enable() error { - return nil -} - -// Disable does nothing, returning always nil. -func (t *NoopTelemetry) Disable() error { - return nil -} - -// Page does nothing -func (t *NoopTelemetry) Page(name app.TelemetryPage, properties map[string]interface{}) { -} - -// Track does nothing -func (t *NoopTelemetry) Track(name app.TelemetryTrack, properties map[string]interface{}) { -} - -// CreatePlaybookRun does nothing -func (t *NoopTelemetry) CreatePlaybookRun(*app.PlaybookRun, string, bool) { -} - -// EndPlaybookRun does nothing -func (t *NoopTelemetry) FinishPlaybookRun(*app.PlaybookRun, string) { -} - -// RestorePlaybookRun does nothing -func (t *NoopTelemetry) RestorePlaybookRun(*app.PlaybookRun, string) { -} - -// RestartPlaybookRun does nothing -func (t *NoopTelemetry) RestartPlaybookRun(*app.PlaybookRun, string) { -} - -// UpdateStatus does nothing -func (t *NoopTelemetry) UpdateStatus(*app.PlaybookRun, string) { -} - -// FrontendTelemetryForPlaybookRun does nothing -func (t *NoopTelemetry) FrontendTelemetryForPlaybookRun(*app.PlaybookRun, string, string) { -} - -// AddPostToTimeline does nothing -func (t *NoopTelemetry) AddPostToTimeline(*app.PlaybookRun, string) { -} - -// RemoveTimelineEvent does nothing -func (t *NoopTelemetry) RemoveTimelineEvent(*app.PlaybookRun, string) { -} - -// AddTask does nothing. -func (t *NoopTelemetry) AddTask(string, string, app.ChecklistItem) { -} - -// RemoveTask does nothing. -func (t *NoopTelemetry) RemoveTask(string, string, app.ChecklistItem) { -} - -// RenameTask does nothing. -func (t *NoopTelemetry) RenameTask(string, string, app.ChecklistItem) { -} - -// SkipChecklist does nothing. -func (t *NoopTelemetry) SkipChecklist(string, string, app.Checklist) { -} - -// RestoreChecklist does nothing. -func (t *NoopTelemetry) RestoreChecklist(string, string, app.Checklist) { -} - -// SkipTask does nothing. -func (t *NoopTelemetry) SkipTask(string, string, app.ChecklistItem) { -} - -// RestoreTask does nothing. -func (t *NoopTelemetry) RestoreTask(string, string, app.ChecklistItem) { -} - -// ModifyCheckedState does nothing. -func (t *NoopTelemetry) ModifyCheckedState(string, string, app.ChecklistItem, bool) { -} - -// SetAssignee does nothing. -func (t *NoopTelemetry) SetAssignee(string, string, app.ChecklistItem) { -} - -// MoveChecklist does nothing. -func (t *NoopTelemetry) MoveChecklist(string, string, app.Checklist) { -} - -// MoveTask does nothing. -func (t *NoopTelemetry) MoveTask(string, string, app.ChecklistItem) { -} - -// CreatePlaybook does nothing. -func (t *NoopTelemetry) CreatePlaybook(app.Playbook, string) { -} - -// ImportPlaybook does nothing. -func (t *NoopTelemetry) ImportPlaybook(app.Playbook, string) { -} - -// UpdatePlaybook does nothing. -func (t *NoopTelemetry) UpdatePlaybook(app.Playbook, string) { -} - -// DeletePlaybook does nothing. -func (t *NoopTelemetry) DeletePlaybook(app.Playbook, string) { -} - -// RestorePlaybook does nothing either. -func (t *NoopTelemetry) RestorePlaybook(app.Playbook, string) { -} - -// ChangeOwner does nothing -func (t *NoopTelemetry) ChangeOwner(*app.PlaybookRun, string) { -} - -// RunTaskSlashCommand does nothing -func (t *NoopTelemetry) RunTaskSlashCommand(string, string, app.ChecklistItem) { -} - -// AddChecklist does nothing -func (t *NoopTelemetry) AddChecklist(playbookRunID, userID string, checklist app.Checklist) { -} - -// RemoveChecklist does nothing -func (t *NoopTelemetry) RemoveChecklist(playbookRunID, userID string, checklist app.Checklist) { -} - -// RenameChecklist does nothing -func (t *NoopTelemetry) RenameChecklist(playbookRunID, userID string, checklist app.Checklist) { -} - -func (t *NoopTelemetry) UpdateRetrospective(playbookRun *app.PlaybookRun, userID string) { -} - -func (t *NoopTelemetry) PublishRetrospective(playbookRun *app.PlaybookRun, userID string) { -} - -// StartTrial does nothing. -func (t *NoopTelemetry) StartTrial(userID string, action string) { -} - -// NotifyAdmins does nothing. -func (t *NoopTelemetry) NotifyAdmins(userID string, action string) { -} - -// FrontendTelemetryForPlaybook does nothing. -func (t *NoopTelemetry) FrontendTelemetryForPlaybook(playbook app.Playbook, userID, action string) { -} - -// FrontendTelemetryForPlaybookTemplate does nothing. -func (t *NoopTelemetry) FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string) { -} - -// ChangeDigestSettings does nothing -func (t *NoopTelemetry) ChangeDigestSettings(userID string, old app.DigestNotificationSettings, new app.DigestNotificationSettings) { -} - -// Follow tracks userID following a playbook run. -func (t *NoopTelemetry) Follow(playbookRun *app.PlaybookRun, userID string) { -} - -// Unfollow tracks userID following a playbook run. -func (t *NoopTelemetry) Unfollow(playbookRun *app.PlaybookRun, userID string) { -} - -// AutoFollowPlaybook tracks the auto-follow of a playbook. -func (t *NoopTelemetry) AutoFollowPlaybook(playbook app.Playbook, userID string) { -} - -// AutoUnfollowPlaybook tracks the auto-unfollow of a playbook. -func (t *NoopTelemetry) AutoUnfollowPlaybook(playbook app.Playbook, userID string) { -} - -// RunChannelAction does nothing -func (t *NoopTelemetry) RunChannelAction(action app.GenericChannelAction, userID string) { -} - -// UpdateChannelAction does nothing -func (t *NoopTelemetry) UpdateChannelAction(action app.GenericChannelAction, userID string) { -} - -// RunAction does nothing -func (t *NoopTelemetry) RunAction(playbookRun *app.PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) { -} - -// FavoriteItem does nothing -func (t *NoopTelemetry) FavoriteItem(item app.CategoryItem, userID string) { -} - -// UnfavoriteItem does nothing -func (t *NoopTelemetry) UnfavoriteItem(item app.CategoryItem, userID string) { -} diff --git a/server/playbooks/server/telemetry/rudder.go b/server/playbooks/server/telemetry/rudder.go deleted file mode 100644 index 7f79eecba95..00000000000 --- a/server/playbooks/server/telemetry/rudder.go +++ /dev/null @@ -1,728 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package telemetry - -import ( - "sync" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - - "github.com/pkg/errors" - rudder "github.com/rudderlabs/analytics-go" -) - -// RudderTelemetry implements Telemetry using a Rudder backend. -type RudderTelemetry struct { - client rudder.Client - diagnosticID string - pluginVersion string - serverVersion string - writeKey string - dataPlaneURL string - enabled bool - mutex sync.RWMutex -} - -// Unique strings that identify each of the tracked events -const ( - eventPlaybookRun = "incident" - actionCreate = "create" - actionImport = "import" - actionEnd = "end" - actionRestart = "restart" - actionChangeOwner = "change_commander" - actionUpdateStatus = "update_status" - actionAddTimelineEventFromPost = "add_timeline_event_from_post" - actionUpdateRetrospective = "update_retrospective" - actionPublishRetrospective = "publish_retrospective" - actionRemoveTimelineEvent = "remove_timeline_event" - actionFollow = "follow" - actionUnfollow = "unfollow" - - eventTasks = "tasks" - actionAddTask = "add_task" - actionRemoveTask = "remove_task" - actionRenameTask = "rename_task" - actionSkipTask = "skip_task" - actionRestoreTask = "restore_task" - actionModifyTaskState = "modify_task_state" - actionMoveTask = "move_task" - actionSetAssigneeForTask = "set_assignee_for_task" - actionRunTaskSlashCommand = "run_task_slash_command" - - eventChecklists = "checklists" - actionAddChecklist = "add_checklist" - actionRemoveChecklist = "remove_checklist" - actionRenameChecklist = "rename_checklist" - actionMoveChecklist = "move_checklist" - actionSkipChecklist = "skip_checklist" - actionRestoreChecklist = "restore_checklist" - - eventPlaybook = "playbook" - actionUpdate = "update" - actionDelete = "delete" - actionRestore = "restore" - actionAutoFollow = "auto_follow" - actionAutoUnfollow = "auto_unfollow" - - eventFrontend = "frontend" - - eventNotifyAdmins = "notify_admins" - - eventStartTrial = "start_trial" - - // telemetryKeyPlaybookRunID records the legacy name used to identify a playbook run via telemetry. - telemetryKeyPlaybookRunID = "IncidentID" - - eventSettings = "settings" - actionDigest = "digest" - - eventChannelAction = "channel_action" - actionRunChannelAction = "run_channel_action" - actionChannelActionUpdate = "update_channel_action" - - eventRunAction = "playbookrun_action" - actionRunAction = "run_playbookrun_action" - - eventSidebarCategory = "lhs_category" - actionFavoriteRun = "favorite_run" - actionUnfavoriteRun = "unfavorite_run" - actionFavoritePlaybook = "favorite_playbook" - actionUnfavoritePlaybook = "unfavorite_playbook" -) - -// Migrated -// actionRunActionsUpdate = "update_playbookrun_actions" => playbookrun_update_actions - -// NewRudder builds a new RudderTelemetry client that will send the events to -// dataPlaneURL with the writeKey, identified with the diagnosticID. The -// version of the server is also sent with every event tracked. -// If either diagnosticID or serverVersion are empty, an error is returned. -func NewRudder(dataPlaneURL, writeKey, diagnosticID, serverVersion string) (*RudderTelemetry, error) { - if diagnosticID == "" { - return nil, errors.New("diagnosticID should not be empty") - } - - if serverVersion == "" { - return nil, errors.New("serverVersion should not be empty") - } - - client, err := rudder.NewWithConfig(writeKey, dataPlaneURL, rudder.Config{}) - if err != nil { - return nil, err - } - - // Continue to emit the pluginVersion for backwards compatibility, but just set it to - // the server version given we're permanently part of the monorepo now. - pluginVersion := serverVersion - - return &RudderTelemetry{ - client: client, - diagnosticID: diagnosticID, - pluginVersion: pluginVersion, - serverVersion: serverVersion, - writeKey: writeKey, - dataPlaneURL: dataPlaneURL, - enabled: true, - }, nil -} - -// trackOld is the generic tracker for events to rudderstack that is backwards compatible with -// old events (string based instead of enum). -// -// All new and migrated events should use Track/Page instead. This should be removed after -// event migration is complete -func (t *RudderTelemetry) trackOld(name string, properties map[string]interface{}) { - t.mutex.RLock() - defer t.mutex.RUnlock() - - if !t.enabled { - return - } - - properties["PluginVersion"] = t.pluginVersion - properties["ServerVersion"] = t.serverVersion - - _ = t.client.Enqueue(rudder.Track{ - UserId: t.diagnosticID, - Event: name, - Properties: properties, - }) -} - -// Track is the generic tracker for events to rudderstack -func (t *RudderTelemetry) Track(name app.TelemetryTrack, properties map[string]interface{}) { - t.mutex.RLock() - defer t.mutex.RUnlock() - - if !t.enabled { - return - } - - properties["PluginVersion"] = t.pluginVersion - properties["ServerVersion"] = t.serverVersion - - _ = t.client.Enqueue(rudder.Track{ - UserId: t.diagnosticID, - Event: name.String(), - Properties: properties, - }) -} - -// Page is the generic tracker for pageviews to rudderstack -func (t *RudderTelemetry) Page(name app.TelemetryPage, properties map[string]interface{}) { - t.mutex.RLock() - defer t.mutex.RUnlock() - - if !t.enabled { - return - } - - properties["PluginVersion"] = t.pluginVersion - properties["ServerVersion"] = t.serverVersion - - _ = t.client.Enqueue(rudder.Page{ - UserId: t.diagnosticID, - Name: name.String(), - Properties: properties, - }) -} - -func tasksWithDueDate(list app.Checklist) int { - count := 0 - for _, item := range list.Items { - if item.DueDate > 0 { - count++ - } - } - return count -} - -func playbookRunProperties(playbookRun *app.PlaybookRun, userID string) map[string]interface{} { - totalChecklistItems := 0 - itemsWithDueDate := 0 - for _, checklist := range playbookRun.Checklists { - totalChecklistItems += len(checklist.Items) - itemsWithDueDate += tasksWithDueDate(checklist) - } - - role := "viewer" - for _, p := range playbookRun.ParticipantIDs { - if p == userID { - role = "participant" - break - } - } - - return map[string]interface{}{ - "UserActualID": userID, - "UserActualRole": role, - telemetryKeyPlaybookRunID: playbookRun.ID, - "HasDescription": playbookRun.Summary != "", - "CommanderUserID": playbookRun.OwnerUserID, - "ReporterUserID": playbookRun.ReporterUserID, - "TeamID": playbookRun.TeamID, - "ChannelID": playbookRun.ChannelID, - "CreateAt": playbookRun.CreateAt, - "EndAt": playbookRun.EndAt, - "DeleteAt": playbookRun.DeleteAt, //nolint - "PostID": playbookRun.PostID, - "PlaybookID": playbookRun.PlaybookID, - "NumChecklists": len(playbookRun.Checklists), - "TotalChecklistItems": totalChecklistItems, - "ChecklistItemsWithDueDate": itemsWithDueDate, - "NumStatusPosts": len(playbookRun.StatusPosts), - "CurrentStatus": playbookRun.CurrentStatus, - "PreviousReminder": playbookRun.PreviousReminder, - "NumTimelineEvents": len(playbookRun.TimelineEvents), - "StatusUpdateBroadcastChannelsEnabled": playbookRun.StatusUpdateBroadcastChannelsEnabled, - "StatusUpdateBroadcastWebhooksEnabled": playbookRun.StatusUpdateBroadcastWebhooksEnabled, - } -} - -// CreatePlaybookRun tracks the creation of the playbook run passed. -func (t *RudderTelemetry) CreatePlaybookRun(playbookRun *app.PlaybookRun, userID string, public bool) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionCreate - properties["Public"] = public - t.trackOld(eventPlaybookRun, properties) -} - -// FinishPlaybookRun tracks the end of the playbook run passed. -func (t *RudderTelemetry) FinishPlaybookRun(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionEnd - t.trackOld(eventPlaybookRun, properties) -} - -// RestorePlaybookRun tracks the restoration of the playbook run. -func (t *RudderTelemetry) RestorePlaybookRun(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionRestore - t.trackOld(eventPlaybookRun, properties) -} - -// RestartPlaybookRun tracks the restart of the playbook run. -func (t *RudderTelemetry) RestartPlaybookRun(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionRestart - t.trackOld(eventPlaybookRun, properties) -} - -// ChangeOwner tracks changes in owner -func (t *RudderTelemetry) ChangeOwner(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionChangeOwner - t.trackOld(eventPlaybookRun, properties) -} - -func (t *RudderTelemetry) UpdateStatus(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionUpdateStatus - properties["ReminderTimerSeconds"] = int(playbookRun.PreviousReminder) - t.trackOld(eventPlaybookRun, properties) -} - -func (t *RudderTelemetry) FrontendTelemetryForPlaybookRun(playbookRun *app.PlaybookRun, userID, action string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = action - t.trackOld(eventFrontend, properties) -} - -// AddPostToTimeline tracks userID creating a timeline event from a post. -func (t *RudderTelemetry) AddPostToTimeline(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionAddTimelineEventFromPost - t.trackOld(eventPlaybookRun, properties) -} - -// RemoveTimelineEvent tracks userID removing a timeline event. -func (t *RudderTelemetry) RemoveTimelineEvent(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionRemoveTimelineEvent - t.trackOld(eventPlaybookRun, properties) -} - -// Follow tracks userID following a playbook run. -func (t *RudderTelemetry) Follow(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionFollow - t.trackOld(eventPlaybookRun, properties) -} - -// Unfollow tracks userID following a playbook run. -func (t *RudderTelemetry) Unfollow(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionUnfollow - t.trackOld(eventPlaybookRun, properties) -} - -func taskProperties(playbookRunID, userID string, task app.ChecklistItem) map[string]interface{} { - return map[string]interface{}{ - telemetryKeyPlaybookRunID: playbookRunID, - "UserActualID": userID, - "TaskID": task.ID, - "State": task.State, - "AssigneeID": task.AssigneeID, - "HasCommand": task.Command != "", - "CommandLastRun": task.CommandLastRun, - "HasDescription": task.Description != "", - "HasDueDate": task.DueDate > 0, - } -} - -// AddTask tracks the creation of a new checklist item by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) AddTask(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionAddTask - t.trackOld(eventTasks, properties) -} - -// RemoveTask tracks the removal of a checklist item by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) RemoveTask(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionRemoveTask - t.trackOld(eventTasks, properties) -} - -// RenameTask tracks the update of a checklist item by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) RenameTask(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionRenameTask - t.trackOld(eventTasks, properties) -} - -// SkipChecklist tracks the skipping of a checklist by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) SkipChecklist(playbookRunID, userID string, checklist app.Checklist) { - properties := checklistProperties(playbookRunID, userID, checklist) - properties["Action"] = actionSkipChecklist - t.trackOld(eventChecklists, properties) -} - -// RestoreChecklist tracks the restoring of a checklist by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) RestoreChecklist(playbookRunID, userID string, checklist app.Checklist) { - properties := checklistProperties(playbookRunID, userID, checklist) - properties["Action"] = actionRestoreChecklist - t.trackOld(eventChecklists, properties) -} - -// SkipTask tracks the skipping of a checklist item by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) SkipTask(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionSkipTask - t.trackOld(eventTasks, properties) -} - -// RestoreTask tracks the restoring of a checklist item by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) RestoreTask(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionRestoreTask - t.trackOld(eventTasks, properties) -} - -// ModifyCheckedState tracks the checking and unchecking of items by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) ModifyCheckedState(playbookRunID, userID string, task app.ChecklistItem, wasOwner bool) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionModifyTaskState - properties["NewState"] = task.State - properties["WasCommander"] = wasOwner - properties["WasAssignee"] = task.AssigneeID == userID - t.trackOld(eventTasks, properties) -} - -// SetAssignee tracks the changing of an assignee on an item by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) SetAssignee(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionSetAssigneeForTask - t.trackOld(eventTasks, properties) -} - -// MoveTask tracks the movement of checklist items by the user -// identified by userID in the given playbook run. -func (t *RudderTelemetry) MoveTask(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionMoveTask - t.trackOld(eventTasks, properties) -} - -// RunTaskSlashCommand tracks the execution of a slash command on a checklist item. -func (t *RudderTelemetry) RunTaskSlashCommand(playbookRunID, userID string, task app.ChecklistItem) { - properties := taskProperties(playbookRunID, userID, task) - properties["Action"] = actionRunTaskSlashCommand - t.trackOld(eventTasks, properties) -} - -func checklistProperties(playbookRunID, userID string, checklist app.Checklist) map[string]interface{} { - return map[string]interface{}{ - telemetryKeyPlaybookRunID: playbookRunID, - "UserActualID": userID, - "ChecklistID": checklist.ID, - "ChecklistNumItems": len(checklist.Items), - } -} - -// AddChecklist tracks the creation of a new checklist. -func (t *RudderTelemetry) AddChecklist(playbookRunID, userID string, checklist app.Checklist) { - properties := checklistProperties(playbookRunID, userID, checklist) - properties["Action"] = actionAddChecklist - t.trackOld(eventChecklists, properties) -} - -// RemoveChecklist tracks the removal of a checklist. -func (t *RudderTelemetry) RemoveChecklist(playbookRunID, userID string, checklist app.Checklist) { - properties := checklistProperties(playbookRunID, userID, checklist) - properties["Action"] = actionRemoveChecklist - t.trackOld(eventChecklists, properties) -} - -// RenameChecklist tracks the renaming of a checklist -func (t *RudderTelemetry) RenameChecklist(playbookRunID, userID string, checklist app.Checklist) { - properties := checklistProperties(playbookRunID, userID, checklist) - properties["Action"] = actionRenameChecklist - t.trackOld(eventChecklists, properties) -} - -// MoveChecklist tracks the movement of a checklist -func (t *RudderTelemetry) MoveChecklist(playbookRunID, userID string, checklist app.Checklist) { - properties := checklistProperties(playbookRunID, userID, checklist) - properties["Action"] = actionMoveChecklist - t.trackOld(eventChecklists, properties) -} - -func (t *RudderTelemetry) UpdateRetrospective(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionUpdateRetrospective - t.trackOld(eventTasks, properties) -} - -func (t *RudderTelemetry) PublishRetrospective(playbookRun *app.PlaybookRun, userID string) { - properties := playbookRunProperties(playbookRun, userID) - properties["Action"] = actionPublishRetrospective - properties["NumMetrics"] = len(playbookRun.MetricsData) - t.trackOld(eventTasks, properties) -} - -func playbookProperties(playbook app.Playbook, userID string) map[string]interface{} { - totalChecklistItems := 0 - totalChecklistItemsWithCommands := 0 - for _, checklist := range playbook.Checklists { - totalChecklistItems += len(checklist.Items) - for _, item := range checklist.Items { - if item.Command != "" { - totalChecklistItemsWithCommands++ - } - } - } - - return map[string]interface{}{ - "UserActualID": userID, - "PlaybookID": playbook.ID, - "HasDescription": playbook.Description != "", - "TeamID": playbook.TeamID, - "IsPublic": playbook.CreatePublicPlaybookRun, - "CreateAt": playbook.CreateAt, - "DeleteAt": playbook.DeleteAt, - "NumChecklists": len(playbook.Checklists), - "TotalChecklistItems": totalChecklistItems, - "NumSlashCommands": totalChecklistItemsWithCommands, - "NumMembers": len(playbook.Members), - "UsesReminderMessageTemplate": playbook.ReminderMessageTemplate != "", - "ReminderTimerDefaultSeconds": playbook.ReminderTimerDefaultSeconds, - "NumInvitedUserIDs": len(playbook.InvitedUserIDs), - "NumInvitedGroupIDs": len(playbook.InvitedGroupIDs), - "InviteUsersEnabled": playbook.InviteUsersEnabled, - "DefaultCommanderID": playbook.DefaultOwnerID, - "DefaultCommanderEnabled": playbook.DefaultOwnerEnabled, - "BroadcastChannelIDs": playbook.BroadcastChannelIDs, - "BroadcastEnabled": playbook.BroadcastEnabled, //nolint - "NumWebhookOnCreationURLs": len(playbook.WebhookOnCreationURLs), - "WebhookOnCreationEnabled": playbook.WebhookOnCreationEnabled, - "SignalAnyKeywordsEnabled": playbook.SignalAnyKeywordsEnabled, - "NumSignalAnyKeywords": len(playbook.SignalAnyKeywords), - "HasChannelNameTemplate": playbook.ChannelNameTemplate != "", - "NumMetrics": len(playbook.Metrics), - } -} - -func playbookTemplateProperties(templateName string, userID string) map[string]interface{} { - return map[string]interface{}{ - "UserActualID": userID, - "TemplateName": templateName, - } -} - -// CreatePlaybook tracks the creation of a playbook. -func (t *RudderTelemetry) CreatePlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionCreate - t.trackOld(eventPlaybook, properties) -} - -// ImportPlaybook tracks the import of a playbook. -func (t *RudderTelemetry) ImportPlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionImport - t.trackOld(eventPlaybook, properties) -} - -// UpdatePlaybook tracks the update of a playbook. -func (t *RudderTelemetry) UpdatePlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionUpdate - t.trackOld(eventPlaybook, properties) -} - -// DeletePlaybook tracks the deletion of a playbook. -func (t *RudderTelemetry) DeletePlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionDelete - t.trackOld(eventPlaybook, properties) -} - -// RestorePlaybook tracks the deletion of a playbook. -func (t *RudderTelemetry) RestorePlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionRestore - t.trackOld(eventPlaybook, properties) -} - -// AutoFollowPlaybook tracks the auto-follow of a playbook. -func (t *RudderTelemetry) AutoFollowPlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionAutoFollow - t.trackOld(eventPlaybook, properties) -} - -// AutoUnfollowPlaybook tracks the auto-unfollow of a playbook. -func (t *RudderTelemetry) AutoUnfollowPlaybook(playbook app.Playbook, userID string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = actionAutoUnfollow - t.trackOld(eventPlaybook, properties) -} - -// FrontendTelemetryForPlaybook tracks an event originating from the frontend -func (t *RudderTelemetry) FrontendTelemetryForPlaybook(playbook app.Playbook, userID, action string) { - properties := playbookProperties(playbook, userID) - properties["Action"] = action - t.trackOld(eventFrontend, properties) -} - -// FrontendTelemetryForPlaybookTemplate tracks a playbook template event originating from the frontend -func (t *RudderTelemetry) FrontendTelemetryForPlaybookTemplate(templateName string, userID, action string) { - properties := playbookTemplateProperties(templateName, userID) - properties["Action"] = action - t.trackOld(eventFrontend, properties) -} - -func commonProperties(userID string) map[string]interface{} { - return map[string]interface{}{ - "UserActualID": userID, - } -} - -func (t *RudderTelemetry) StartTrial(userID string, action string) { - properties := commonProperties(userID) - properties["Action"] = action - t.trackOld(eventStartTrial, properties) -} - -func (t *RudderTelemetry) NotifyAdmins(userID string, action string) { - properties := commonProperties(userID) - properties["Action"] = action - t.trackOld(eventNotifyAdmins, properties) -} - -// Enable creates a new client to track all future events. It does nothing if -// a client is already enabled. -func (t *RudderTelemetry) Enable() error { - t.mutex.Lock() - defer t.mutex.Unlock() - - if t.enabled { - return nil - } - - newClient, err := rudder.NewWithConfig(t.writeKey, t.dataPlaneURL, rudder.Config{}) - if err != nil { - return errors.Wrap(err, "creating a new Rudder client in Enable failed") - } - - t.client = newClient - t.enabled = true - return nil -} - -// Disable disables telemetry for all future events. It does nothing if the -// client is already disabled. -func (t *RudderTelemetry) Disable() error { - t.mutex.Lock() - defer t.mutex.Unlock() - - if !t.enabled { - return nil - } - - if err := t.client.Close(); err != nil { - return errors.Wrap(err, "closing the Rudder client in Disable failed") - } - - t.enabled = false - return nil -} - -func digestSettingsProperties(userID string) map[string]interface{} { - return map[string]interface{}{ - "UserActualID": userID, - } -} - -// ChangeDigestSettings tracks when a user changes one of the digest settings -func (t *RudderTelemetry) ChangeDigestSettings(userID string, old app.DigestNotificationSettings, new app.DigestNotificationSettings) { - properties := digestSettingsProperties(userID) - properties["Action"] = actionDigest - properties["OldDisableDailyDigest"] = old.DisableDailyDigest - properties["NewDisableDailyDigest"] = new.DisableDailyDigest - properties["OldDisableWeeklyDigest"] = old.DisableWeeklyDigest - properties["NewDisableWeeklyDigest"] = new.DisableWeeklyDigest - t.trackOld(eventSettings, properties) -} - -func channelActionProperties(action app.GenericChannelAction, userID string) map[string]interface{} { - return map[string]interface{}{ - "UserActualID": userID, - "ChannelID": action.ChannelID, - "ActionType": action.ActionType, - "TriggerType": action.TriggerType, - } -} - -func (t *RudderTelemetry) RunChannelAction(action app.GenericChannelAction, userID string) { - properties := channelActionProperties(action, userID) - properties["Action"] = actionRunChannelAction - t.trackOld(eventChannelAction, properties) -} - -// UpdateRunActions tracks actions settings update -func (t *RudderTelemetry) UpdateChannelAction(action app.GenericChannelAction, userID string) { - properties := channelActionProperties(action, userID) - properties["Action"] = actionChannelActionUpdate - t.trackOld(eventChannelAction, properties) -} - -func runActionProperties(playbookRun *app.PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) map[string]interface{} { - return map[string]interface{}{ - "UserActualID": userID, - "ActionType": actionType, - "TriggerType": triggerType, - "NumBroadcasts": numBroadcasts, - "PlaybookRunID": playbookRun.ID, - "PlaybookID": playbookRun.PlaybookID, - } -} - -// RunAction tracks the run actions, i.e., status broadcast action -func (t *RudderTelemetry) RunAction(playbookRun *app.PlaybookRun, userID, triggerType, actionType string, numBroadcasts int) { - properties := runActionProperties(playbookRun, userID, triggerType, actionType, numBroadcasts) - properties["Action"] = actionRunAction - t.trackOld(eventRunAction, properties) -} - -// FavoriteItem tracks run favoriting of an item. Item can be run or a playbook -func (t *RudderTelemetry) FavoriteItem(item app.CategoryItem, userID string) { - properties := map[string]interface{}{} - properties["UserActualID"] = userID - switch item.Type { - case app.PlaybookItemType: - properties["PlaybookID"] = item.ItemID - properties["Action"] = actionFavoritePlaybook - case app.RunItemType: - properties["PlaybookRunID"] = item.ItemID - properties["Action"] = actionFavoriteRun - } - t.trackOld(eventSidebarCategory, properties) -} - -// UnfavoriteItem tracks run unfavoriting of an item. Item can be run or a playbook -func (t *RudderTelemetry) UnfavoriteItem(item app.CategoryItem, userID string) { - properties := map[string]interface{}{} - properties["UserActualID"] = userID - switch item.Type { - case app.PlaybookItemType: - properties["PlaybookID"] = item.ItemID - properties["Action"] = actionUnfavoritePlaybook - case app.RunItemType: - properties["PlaybookRunID"] = item.ItemID - properties["Action"] = actionUnfavoriteRun - } - t.trackOld(eventSidebarCategory, properties) -} diff --git a/server/playbooks/server/telemetry/rudder_test.go b/server/playbooks/server/telemetry/rudder_test.go deleted file mode 100644 index d612c3a3542..00000000000 --- a/server/playbooks/server/telemetry/rudder_test.go +++ /dev/null @@ -1,559 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package telemetry - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "testing" - "time" - - "gopkg.in/guregu/null.v4" - - "github.com/mattermost/mattermost/server/v8/playbooks/server/app" - - rudder "github.com/rudderlabs/analytics-go" - "github.com/stretchr/testify/require" -) - -var ( - diagnosticID = "dummy_diagnostic_id" - pluginVersion = "dummy_plugin_version" - serverVersion = "dummy_server_version" - dummyPlaybookRunID = "dummy_playbook_run_id" - dummyUserID = "dummy_user_id" -) - -func TestNewRudder(t *testing.T) { - r, err := NewRudder("dummy_key", "dummy_url", diagnosticID, serverVersion) - require.Equal(t, r.diagnosticID, diagnosticID) - require.Equal(t, r.serverVersion, serverVersion) - require.NoError(t, err) -} - -type rudderPayload struct { - MessageID string - SentAt time.Time - Batch []struct { - MessageID string - UserID string - Event string - Timestamp time.Time - Properties map[string]interface{} - } - Context struct { - Library struct { - Name string - Version string - } - } -} - -func setupRudder(t *testing.T, data chan<- rudderPayload) (*RudderTelemetry, *httptest.Server) { - t.Helper() - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - body, err := io.ReadAll(r.Body) - require.NoError(t, err) - - var p rudderPayload - err = json.Unmarshal(body, &p) - require.NoError(t, err) - - data <- p - })) - - writeKey := "dummy_key" - client, err := rudder.NewWithConfig(writeKey, server.URL, rudder.Config{ - BatchSize: 1, - Interval: 1 * time.Millisecond, - Verbose: true, - }) - require.NoError(t, err) - - return &RudderTelemetry{ - client: client, - diagnosticID: diagnosticID, - pluginVersion: pluginVersion, - serverVersion: serverVersion, - writeKey: writeKey, - dataPlaneURL: server.URL, - enabled: true, - }, server -} - -var dummyPlaybookRun = &app.PlaybookRun{ - ID: "id", - Name: "name", - Summary: "description", - OwnerUserID: "owner_user_id", - ReporterUserID: "reporter_user_id", - TeamID: "team_id", - ChannelID: "channel_id_1", - CreateAt: 1234, - EndAt: 5678, - DeleteAt: 9999, - PostID: "post_id", - PlaybookID: "playbookID1", - ParticipantIDs: []string{"owner_user_id", "dummy_user_id"}, - Type: app.RunTypePlaybook, - Checklists: []app.Checklist{ - { - Title: "Checklist", - Items: []app.ChecklistItem{ - { - ID: "task_id_1", - Title: "Test Item", - State: "", - StateModified: 1234, - AssigneeID: "assignee_id", - AssigneeModified: 5678, - Command: "command", - CommandLastRun: 100000, - Description: "description", - DueDate: 100000000000, - }, - }, - }, - { - Title: "Checklist 2", - Items: []app.ChecklistItem{ - {Title: "Test Item 2"}, - {Title: "Test Item 3"}, - }, - }, - }, - StatusPosts: []app.StatusPost{ - {ID: "status_post_1"}, - {ID: "status_post_2"}, - }, - PreviousReminder: 5 * time.Second, - TimelineEvents: []app.TimelineEvent{ - {ID: "timeline_event_1"}, - {ID: "timeline_event_2"}, - {ID: "timeline_event_3"}, - }, -} - -var dummyTask = dummyPlaybookRun.Checklists[0].Items[0] - -func assertPayload(t *testing.T, actual rudderPayload, expectedEvent string, expectedAction string) { - t.Helper() - - playbookRunFromProperties := func(properties map[string]interface{}) *app.PlaybookRun { - require.Contains(t, properties, telemetryKeyPlaybookRunID) - require.Contains(t, properties, "HasDescription") - require.Contains(t, properties, "CommanderUserID") - require.Contains(t, properties, "ReporterUserID") - require.Contains(t, properties, "TeamID") - require.Contains(t, properties, "ChannelID") - require.Contains(t, properties, "CreateAt") - require.Contains(t, properties, "EndAt") - require.Contains(t, properties, "DeleteAt") - require.Contains(t, properties, "PostID") - require.Contains(t, properties, "PlaybookID") - require.Contains(t, properties, "NumChecklists") - require.Contains(t, properties, "TotalChecklistItems") - require.Contains(t, properties, "NumStatusPosts") - require.Contains(t, properties, "CurrentStatus") - require.Contains(t, properties, "PreviousReminder") - require.Contains(t, properties, "NumTimelineEvents") - - return &app.PlaybookRun{ - ID: properties[telemetryKeyPlaybookRunID].(string), - Name: dummyPlaybookRun.Name, // not included in the tracked event - Summary: dummyPlaybookRun.Summary, - OwnerUserID: properties["CommanderUserID"].(string), - ReporterUserID: properties["ReporterUserID"].(string), - TeamID: properties["TeamID"].(string), - CreateAt: int64(properties["CreateAt"].(float64)), - EndAt: int64(properties["EndAt"].(float64)), - DeleteAt: int64(properties["DeleteAt"].(float64)), - ChannelID: "channel_id_1", - PostID: properties["PostID"].(string), - PlaybookID: dummyPlaybookRun.PlaybookID, - Checklists: dummyPlaybookRun.Checklists, // not included as self in tracked event - StatusPosts: dummyPlaybookRun.StatusPosts, - PreviousReminder: time.Duration((properties["PreviousReminder"]).(float64)), - TimelineEvents: dummyPlaybookRun.TimelineEvents, - ParticipantIDs: []string{"owner_user_id", "dummy_user_id"}, - Type: app.RunTypePlaybook, - } - } - - require.Len(t, actual.Batch, 1) - require.Equal(t, diagnosticID, actual.Batch[0].UserID) - require.Equal(t, expectedEvent, actual.Batch[0].Event) - - properties := actual.Batch[0].Properties - require.Equal(t, expectedAction, properties["Action"]) - require.Contains(t, properties, "ServerVersion") - require.Equal(t, properties["ServerVersion"], serverVersion) - require.Contains(t, properties, "PluginVersion") - require.Equal(t, properties["PluginVersion"], pluginVersion) - - if expectedEvent == eventPlaybookRun && expectedAction == actionCreate { - require.Contains(t, properties, "Public") - } - - if expectedEvent == eventPlaybookRun && (expectedAction == actionCreate || expectedAction == actionEnd || expectedAction == actionRestore) { - require.Equal(t, dummyPlaybookRun, playbookRunFromProperties(properties)) - } else { - require.Contains(t, properties, telemetryKeyPlaybookRunID) - require.Equal(t, properties[telemetryKeyPlaybookRunID], dummyPlaybookRunID) - require.Contains(t, properties, "UserActualID") - require.Equal(t, properties["UserActualID"], dummyUserID) - } -} - -func TestRudderTelemetry(t *testing.T) { - data := make(chan rudderPayload) - rudderClient, rudderServer := setupRudder(t, data) - defer rudderServer.Close() - - for name, tc := range map[string]struct { - ExpectedEvent string - ExpectedAction string - FuncToTest func() - }{ - "create playbook run": {eventPlaybookRun, actionCreate, func() { - rudderClient.CreatePlaybookRun(dummyPlaybookRun, dummyUserID, true) - }}, - "end playbook run": {eventPlaybookRun, actionEnd, func() { - rudderClient.FinishPlaybookRun(dummyPlaybookRun, dummyUserID) - }}, - "restore playbook run": {eventPlaybookRun, actionRestore, func() { - rudderClient.RestorePlaybookRun(dummyPlaybookRun, dummyUserID) - }}, - "add checklist item": {eventTasks, actionAddTask, func() { - rudderClient.AddTask(dummyPlaybookRunID, dummyUserID, dummyTask) - }}, - "remove checklist item": {eventTasks, actionRemoveTask, func() { - rudderClient.RemoveTask(dummyPlaybookRunID, dummyUserID, dummyTask) - }}, - "rename checklist item": {eventTasks, actionRenameTask, func() { - rudderClient.RenameTask(dummyPlaybookRunID, dummyUserID, dummyTask) - }}, - "modify checked checklist item": {eventTasks, actionModifyTaskState, func() { - rudderClient.ModifyCheckedState(dummyPlaybookRunID, dummyUserID, dummyTask, true) - }}, - "move checklist item": {eventTasks, actionMoveTask, func() { - rudderClient.MoveTask(dummyPlaybookRunID, dummyUserID, dummyTask) - }}, - } { - t.Run(name, func(t *testing.T) { - tc.FuncToTest() - - select { - case payload := <-data: - assertPayload(t, payload, tc.ExpectedEvent, tc.ExpectedAction) - case <-time.After(time.Second * 1): - require.Fail(t, "Did not receive Event message") - } - }) - } -} - -func TestDisableTelemetry(t *testing.T) { - t.Run("disable client", func(t *testing.T) { - data := make(chan rudderPayload) - rudderClient, rudderServer := setupRudder(t, data) - defer rudderServer.Close() - - err := rudderClient.Disable() - require.NoError(t, err) - - rudderClient.CreatePlaybookRun(dummyPlaybookRun, dummyUserID, true) - - select { - case <-data: - require.Fail(t, "Received Event message while being disabled") - case <-time.After(time.Second * 1): - break - } - }) - - t.Run("disable client is idempotent", func(t *testing.T) { - data := make(chan rudderPayload) - rudderClient, rudderServer := setupRudder(t, data) - defer rudderServer.Close() - - err := rudderClient.Disable() - require.NoError(t, err) - - err = rudderClient.Disable() - require.NoError(t, err) - - rudderClient.CreatePlaybookRun(dummyPlaybookRun, dummyUserID, true) - - select { - case <-data: - require.Fail(t, "Received Event message while being disabled") - case <-time.After(time.Second * 1): - break - } - }) - - t.Run("re-disable client", func(t *testing.T) { - data := make(chan rudderPayload) - rudderClient, rudderServer := setupRudder(t, data) - defer rudderServer.Close() - - // Make sure it's enabled before disabling - err := rudderClient.Enable() - require.NoError(t, err) - - err = rudderClient.Disable() - require.NoError(t, err) - - rudderClient.CreatePlaybookRun(dummyPlaybookRun, dummyUserID, true) - - select { - case <-data: - require.Fail(t, "Received Event message while being disabled") - case <-time.After(time.Second * 1): - break - } - }) - - t.Run("re-enable client", func(t *testing.T) { - // The default timeout in a new Rudder client is 5s. When enabling a - // disabled client, the config is reset to these defaults. - // We could replace the client directly in the test, but that kind of - // defeats the purpose of testing Enable. - if testing.Short() { - t.Skip("Skipping re-enable client test: takes at least 6 seconds") - } - - data := make(chan rudderPayload) - rudderClient, rudderServer := setupRudder(t, data) - defer rudderServer.Close() - - // Make sure it's disabled before enabling - err := rudderClient.Disable() - require.NoError(t, err) - - err = rudderClient.Enable() - require.NoError(t, err) - - rudderClient.CreatePlaybookRun(dummyPlaybookRun, dummyUserID, true) - - select { - case payload := <-data: - assertPayload(t, payload, eventPlaybookRun, actionCreate) - case <-time.After(time.Second * 6): - require.Fail(t, "Did not receive Event message") - } - }) -} - -func TestPlaybookProperties(t *testing.T) { - var dummyPlaybook = app.Playbook{ - ID: "id", - Title: "title", - Description: "description", - TeamID: "team_id", - CreatePublicPlaybookRun: true, - CreateAt: 1234, - DeleteAt: 9999, - NumStages: 2, - NumSteps: 3, - Checklists: []app.Checklist{ - { - Title: "Checklist", - Items: []app.ChecklistItem{ - { - ID: "task_id_1", - Title: "Test Item", - State: "", - StateModified: 1234, - AssigneeID: "assignee_id", - AssigneeModified: 5678, - Command: "command", - CommandLastRun: 100000, - Description: "description", - }, - }, - }, - { - Title: "Checklist 2", - Items: []app.ChecklistItem{ - {Title: "Test Item 2"}, - {Title: "Test Item 3"}, - }, - }, - }, - Members: []app.PlaybookMember{{}, {}}, - ReminderMessageTemplate: "reminder_message_template", - ReminderTimerDefaultSeconds: 1000, - InvitedUserIDs: []string{"invited_user_id_1", "invited_user_id_2"}, - InvitedGroupIDs: []string{"invited_group_id_1", "invited_group_id_2"}, - InviteUsersEnabled: true, - DefaultOwnerID: "default_owner_id", - DefaultOwnerEnabled: false, - BroadcastChannelIDs: []string{"broadcast_channel_id"}, - BroadcastEnabled: true, - WebhookOnCreationURLs: []string{"webhook_on_creation_url_1", "webhook_on_creation_url_2"}, - WebhookOnCreationEnabled: false, - SignalAnyKeywordsEnabled: true, - SignalAnyKeywords: []string{"SEV1, SEV2"}, - ChannelNameTemplate: "channel_name_template", - Metrics: []app.PlaybookMetricConfig{{ - ID: "metricid", - PlaybookID: "id", - Title: "metric 1", - Description: "this is a descr", - Type: "Duration", - Target: null.IntFrom(12345), - }}, - } - - properties := playbookProperties(dummyPlaybook, dummyUserID) - - // ID field is reserved by Rudder to uniquely identify every event - require.NotContains(t, properties, "ID") - - expectedProperties := map[string]interface{}{ - "UserActualID": dummyUserID, - "PlaybookID": dummyPlaybook.ID, - "HasDescription": true, - "TeamID": dummyPlaybook.TeamID, - "IsPublic": dummyPlaybook.CreatePublicPlaybookRun, - "CreateAt": dummyPlaybook.CreateAt, - "DeleteAt": dummyPlaybook.DeleteAt, - "NumChecklists": len(dummyPlaybook.Checklists), - "TotalChecklistItems": 3, - "NumSlashCommands": 1, - "NumMembers": 2, - "UsesReminderMessageTemplate": true, - "ReminderTimerDefaultSeconds": dummyPlaybook.ReminderTimerDefaultSeconds, - "NumInvitedUserIDs": len(dummyPlaybook.InvitedUserIDs), - "NumInvitedGroupIDs": len(dummyPlaybook.InvitedGroupIDs), - "InviteUsersEnabled": dummyPlaybook.InviteUsersEnabled, - "DefaultCommanderID": dummyPlaybook.DefaultOwnerID, - "DefaultCommanderEnabled": dummyPlaybook.DefaultOwnerEnabled, - "BroadcastChannelIDs": dummyPlaybook.BroadcastChannelIDs, - "BroadcastEnabled": dummyPlaybook.BroadcastEnabled, //nolint - "NumWebhookOnCreationURLs": 2, - "WebhookOnCreationEnabled": dummyPlaybook.WebhookOnCreationEnabled, - "SignalAnyKeywordsEnabled": dummyPlaybook.SignalAnyKeywordsEnabled, - "NumSignalAnyKeywords": len(dummyPlaybook.SignalAnyKeywords), - "HasChannelNameTemplate": true, - "NumMetrics": 1, - } - - require.Equal(t, expectedProperties, properties) -} - -func TestPlaybookRunPropertiesParticipant(t *testing.T) { - properties := playbookRunProperties(dummyPlaybookRun, dummyUserID) - - // ID field is reserved by Rudder to uniquely identify every event - require.NotContains(t, properties, "ID") - - expectedProperties := map[string]interface{}{ - "UserActualID": dummyUserID, - "UserActualRole": "participant", - telemetryKeyPlaybookRunID: dummyPlaybookRun.ID, - "HasDescription": true, - "CommanderUserID": dummyPlaybookRun.OwnerUserID, - "ReporterUserID": dummyPlaybookRun.ReporterUserID, - "TeamID": dummyPlaybookRun.TeamID, - "ChannelID": dummyPlaybookRun.ChannelID, - "CreateAt": dummyPlaybookRun.CreateAt, - "EndAt": dummyPlaybookRun.EndAt, - "DeleteAt": dummyPlaybookRun.DeleteAt, //nolint - "PostID": dummyPlaybookRun.PostID, - "PlaybookID": dummyPlaybookRun.PlaybookID, - "NumChecklists": 2, - "TotalChecklistItems": 3, - "ChecklistItemsWithDueDate": 1, - "NumStatusPosts": 2, - "CurrentStatus": dummyPlaybookRun.CurrentStatus, - "PreviousReminder": dummyPlaybookRun.PreviousReminder, - "NumTimelineEvents": len(dummyPlaybookRun.TimelineEvents), - "StatusUpdateBroadcastChannelsEnabled": dummyPlaybookRun.StatusUpdateBroadcastChannelsEnabled, - "StatusUpdateBroadcastWebhooksEnabled": dummyPlaybookRun.StatusUpdateBroadcastWebhooksEnabled, - } - - require.Equal(t, expectedProperties, properties) -} - -func TestPlaybookRunPropertiesViewer(t *testing.T) { - properties := playbookRunProperties(dummyPlaybookRun, "other_user_id") - - // ID field is reserved by Rudder to uniquely identify every event - require.NotContains(t, properties, "ID") - - expectedProperties := map[string]interface{}{ - "UserActualID": "other_user_id", - "UserActualRole": "viewer", - telemetryKeyPlaybookRunID: dummyPlaybookRun.ID, - "HasDescription": true, - "CommanderUserID": dummyPlaybookRun.OwnerUserID, - "ReporterUserID": dummyPlaybookRun.ReporterUserID, - "TeamID": dummyPlaybookRun.TeamID, - "ChannelID": dummyPlaybookRun.ChannelID, - "CreateAt": dummyPlaybookRun.CreateAt, - "EndAt": dummyPlaybookRun.EndAt, - "DeleteAt": dummyPlaybookRun.DeleteAt, //nolint - "PostID": dummyPlaybookRun.PostID, - "PlaybookID": dummyPlaybookRun.PlaybookID, - "NumChecklists": 2, - "TotalChecklistItems": 3, - "ChecklistItemsWithDueDate": 1, - "NumStatusPosts": 2, - "CurrentStatus": dummyPlaybookRun.CurrentStatus, - "PreviousReminder": dummyPlaybookRun.PreviousReminder, - "NumTimelineEvents": len(dummyPlaybookRun.TimelineEvents), - "StatusUpdateBroadcastChannelsEnabled": dummyPlaybookRun.StatusUpdateBroadcastChannelsEnabled, - "StatusUpdateBroadcastWebhooksEnabled": dummyPlaybookRun.StatusUpdateBroadcastWebhooksEnabled, - } - - require.Equal(t, expectedProperties, properties) -} - -func TestTaskProperties(t *testing.T) { - properties := taskProperties(dummyPlaybookRunID, dummyUserID, dummyTask) - - // ID field is reserved by Rudder to uniquely identify every event - require.NotContains(t, properties, "ID") - - expectedProperties := map[string]interface{}{ - telemetryKeyPlaybookRunID: dummyPlaybookRunID, - "UserActualID": dummyUserID, - "TaskID": dummyTask.ID, - "State": dummyTask.State, - "AssigneeID": dummyTask.AssigneeID, - "HasCommand": true, - "CommandLastRun": dummyTask.CommandLastRun, - "HasDescription": true, - "HasDueDate": true, - } - - require.Equal(t, expectedProperties, properties) -} - -func TestRunActionProperties(t *testing.T) { - dummyTriggerType := "dummy_trigger_type" - dummyActionType := "dummy_action_type" - numBroadcasts := 7 - properties := runActionProperties(dummyPlaybookRun, dummyUserID, dummyTriggerType, dummyActionType, numBroadcasts) - - // ID field is reserved by Rudder to uniquely identify every event - require.NotContains(t, properties, "ID") - - expectedProperties := map[string]interface{}{ - "UserActualID": dummyUserID, - "TriggerType": dummyTriggerType, - "ActionType": dummyActionType, - "NumBroadcasts": numBroadcasts, - "PlaybookID": dummyPlaybookRun.PlaybookID, - "PlaybookRunID": dummyPlaybookRun.ID, - } - - require.Equal(t, expectedProperties, properties) -} diff --git a/server/playbooks/server/timeutils/timeutils.go b/server/playbooks/server/timeutils/timeutils.go deleted file mode 100644 index 6acb1a2fe9d..00000000000 --- a/server/playbooks/server/timeutils/timeutils.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package timeutils - -import ( - "fmt" - "math" - "time" - - "github.com/mattermost/mattermost/server/public/model" -) - -func GetTimeForMillis(unixMillis int64) time.Time { - return time.Unix(0, unixMillis*int64(1000000)) -} - -func DurationString(start, end time.Time) string { - duration := end.Sub(start).Round(time.Second) - - if duration.Seconds() < 60 { - return "< 1m" - } - - if duration.Minutes() < 60 { - return fmt.Sprintf("%.fm", math.Floor(duration.Minutes())) - } - - if duration.Hours() < 24 { - hours := math.Floor(duration.Hours()) - minutes := math.Mod(math.Floor(duration.Minutes()), 60) - if minutes == 0 { - return fmt.Sprintf("%.fh", hours) - } - return fmt.Sprintf("%.fh %.fm", hours, minutes) - } - - days := math.Floor(duration.Hours() / 24) - duration %= 24 * time.Hour - hours := math.Floor(duration.Hours()) - minutes := math.Mod(math.Floor(duration.Minutes()), 60) - if minutes == 0 { - if hours == 0 { - return fmt.Sprintf("%.fd", days) - } - return fmt.Sprintf("%.fd %.fh", days, hours) - } - if hours == 0 { - return fmt.Sprintf("%.fd %.fm", days, minutes) - } - return fmt.Sprintf("%.fd %.fh %.fm", days, hours, minutes) -} - -func GetUserTimezone(user *model.User) (*time.Location, error) { - key := "automaticTimezone" - if user.Timezone["useAutomaticTimezone"] == "false" { - key = "manualTimezone" - } - return time.LoadLocation(user.Timezone[key]) -} - -func IsSameDay(time1, time2 time.Time) bool { - return time1.YearDay() == time2.YearDay() && time1.Year() == time2.Year() -} - -// getDaysDiff returns days difference between two date. -func GetDaysDiff(start, end time.Time) int { - days := int(end.Sub(start).Hours() / 24) - - if start.AddDate(0, 0, days).YearDay() != end.YearDay() { - days++ - } - return days -} diff --git a/server/playbooks/server/timeutils/timeutils_test.go b/server/playbooks/server/timeutils/timeutils_test.go deleted file mode 100644 index cafd586add5..00000000000 --- a/server/playbooks/server/timeutils/timeutils_test.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package timeutils - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func TestDurationString(t *testing.T) { - now := time.Now() - - testCases := []struct { - name string - start time.Time - end time.Time - expected string - }{ - { - name: "Duration zero", - start: now, - end: now, - expected: "< 1m", - }, - { - name: "Only seconds", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 1, 10, 0, 25, 0, time.UTC), - expected: "< 1m", - }, - { - name: "Exact minutes", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 1, 10, 15, 0, 0, time.UTC), - expected: "15m", - }, - { - name: "Minutes and seconds", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 1, 10, 30, 25, 0, time.UTC), - expected: "30m", - }, - { - name: "Exact hours", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 1, 13, 0, 0, 0, time.UTC), - expected: "3h", - }, - { - name: "Hours and minutes", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 1, 12, 45, 0, 0, time.UTC), - expected: "2h 45m", - }, - { - name: "Hours, minutes and seconds", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 1, 20, 59, 10, 0, time.UTC), - expected: "10h 59m", - }, - { - name: "Exact days", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 2, 10, 0, 0, 0, time.UTC), - expected: "1d", - }, - { - name: "Days and seconds", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 3, 10, 0, 25, 0, time.UTC), - expected: "2d", - }, - { - name: "Days and exact minutes", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 5, 10, 15, 0, 0, time.UTC), - expected: "4d 15m", - }, - { - name: "Days, minutes and seconds", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 10, 10, 30, 25, 0, time.UTC), - expected: "9d 30m", - }, - { - name: "Days and hours", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 21, 13, 0, 0, 0, time.UTC), - expected: "20d 3h", - }, - { - name: "Days, hours and minutes", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 26, 12, 45, 0, 0, time.UTC), - expected: "25d 2h 45m", - }, - { - name: "Days, hours, minutes and seconds", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 1, 31, 20, 59, 10, 0, time.UTC), - expected: "30d 10h 59m", - }, - { - name: "Days, hours, minutes and seconds over months", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2000, 2, 31, 20, 59, 10, 0, time.UTC), - expected: "61d 10h 59m", - }, - { - name: "Days, hours, minutes and seconds over years", - start: time.Date(2000, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2001, 2, 31, 20, 59, 10, 0, time.UTC), - expected: "427d 10h 59m", - }, - { - name: "An exact year", - start: time.Date(2001, 1, 1, 10, 0, 0, 0, time.UTC), - end: time.Date(2002, 1, 1, 10, 0, 0, 0, time.UTC), - expected: "365d", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - actual := DurationString(testCase.start, testCase.end) - require.Equal(t, testCase.expected, actual) - }) - } -} diff --git a/server/public/model/config.go b/server/public/model/config.go index 6d4c63a147f..9837a654188 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -2864,13 +2864,9 @@ func (s *CloudSettings) SetDefaults() { } type ProductSettings struct { - EnablePlaybooks *bool } func (s *ProductSettings) SetDefaults() { - if s.EnablePlaybooks == nil { - s.EnablePlaybooks = NewBool(true) - } } type PluginState struct { @@ -2938,6 +2934,11 @@ func (s *PluginSettings) SetDefaults(ls LogSettings) { s.PluginStates[PluginIdCalls] = &PluginState{Enable: true} } + if s.PluginStates[PluginIdPlaybooks] == nil { + // Enable the playbooks plugin by default + s.PluginStates[PluginIdPlaybooks] = &PluginState{Enable: true} + } + if s.EnableMarketplace == nil { s.EnableMarketplace = NewBool(PluginSettingsDefaultEnableMarketplace) } diff --git a/server/public/plugin/environment.go b/server/public/plugin/environment.go index 20fd2183011..cb9e70316fc 100644 --- a/server/public/plugin/environment.go +++ b/server/public/plugin/environment.go @@ -104,7 +104,6 @@ func scanSearchPath(path string) ([]*model.BundleInfo, error) { } var pluginIDBlocklist = map[string]bool{ - "playbooks": true, "com.mattermost.plugin-incident-response": true, "com.mattermost.plugin-incident-management": true, } diff --git a/webapp/Makefile b/webapp/Makefile index 62f136717e7..2be54e8ae9d 100644 --- a/webapp/Makefile +++ b/webapp/Makefile @@ -51,18 +51,11 @@ check-types: node_modules ## Checks TS file for TypeScript confirmity npm run check-types -.PHONY: build -build: node_modules ## Builds all web app packages - @echo Building Web App and related packages - - npm run build - .PHONY: dist -dist: build ## Builds all web app packages and copies Playbooks files into Channels dist folder +dist: node_modules ## Builds all web app packages @echo Packaging Mattermost Web App - mkdir -p channels/dist/products/playbooks - cp -R playbooks/dist/* channels/dist/products/playbooks + npm run build node_modules: package.json $(wildcard package-lock.json) @echo Getting dependencies using npm diff --git a/webapp/channels/src/plugins/products.ts b/webapp/channels/src/plugins/products.ts index dc7c277374d..311c459e29e 100644 --- a/webapp/channels/src/plugins/products.ts +++ b/webapp/channels/src/plugins/products.ts @@ -3,7 +3,6 @@ import {Store} from 'redux'; -import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import store from 'stores/redux_store'; @@ -33,8 +32,8 @@ function configureClient() { function loadRemoteModules() { /* eslint-disable no-console */ - return async (dispatch: DispatchFunc, getState: GetStateFunc) => { - const config = getConfig(getState()); + return async (/*dispatch: DispatchFunc, getState: GetStateFunc*/) => { + // const config = getConfig(getState()); /** * products contains a map of product IDs to a function that will load all of their parts. Calling that @@ -43,17 +42,7 @@ function loadRemoteModules() { * Note that these import paths must be statically defined or else they won't be found at runtime. They * can't be constructed based on the name of a product at runtime. */ - let products = [ - { - id: 'playbooks', - load: () => ({ - index: import('playbooks'), - }), - }, - ]; - if (config.EnablePlaybooks !== 'true') { - products = products.filter((p) => p.id !== 'playbooks'); - } + const products: any[] = [ ]; await Promise.all(products.map(async (product) => { if (!REMOTE_CONTAINERS[product.id]) { diff --git a/webapp/channels/webpack.config.js b/webapp/channels/webpack.config.js index 3608f903152..f127f151249 100644 --- a/webapp/channels/webpack.config.js +++ b/webapp/channels/webpack.config.js @@ -275,23 +275,6 @@ var config = { ], }; -if (DEV) { - config.plugins.push({ - apply: (compiler) => { - compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => { - const playbooksDist = path.resolve(__dirname, '../playbooks/dist'); - const playbooksSymlink = './dist/products/playbooks'; - - fs.mkdir('./dist/products', () => { - if (!fs.existsSync(playbooksSymlink)) { - fs.symlinkSync(playbooksDist, playbooksSymlink, 'dir'); - } - }); - }); - }, - }); -} - function generateCSP() { let csp = 'script-src \'self\' cdn.rudderlabs.com/ js.stripe.com/v3'; @@ -328,9 +311,7 @@ async function initializeModuleFederation() { } async function getRemoteContainers() { - const products = [ - {name: 'playbooks'}, - ]; + const products = []; const remotes = {}; for (const product of products) { diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 76881df7875..bb6c0cbc580 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -11,8 +11,7 @@ "channels", "platform/client", "platform/components", - "platform/types", - "playbooks" + "platform/types" ], "dependencies": { "react-intl": "6.3.2" @@ -847,235 +846,6 @@ "node": ">=6.0.0" } }, - "node_modules/@apollo/client": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.3.tgz", - "integrity": "sha512-nzZ6d6a4flLpm3pZOGpuAUxLlp9heob7QcCkyIqZlCLvciUibgufRfYTwfkWCc4NaGHGSZyodzvfr79H6oUwGQ==", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "@wry/context": "^0.7.0", - "@wry/equality": "^0.5.0", - "@wry/trie": "^0.3.0", - "graphql-tag": "^2.12.6", - "hoist-non-react-statics": "^3.3.2", - "optimism": "^0.16.1", - "prop-types": "^15.7.2", - "response-iterator": "^0.2.6", - "symbol-observable": "^4.0.0", - "ts-invariant": "^0.10.3", - "tslib": "^2.3.0", - "zen-observable-ts": "^1.2.5" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" - }, - "peerDependenciesMeta": { - "graphql-ws": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "subscriptions-transport-ws": { - "optional": true - } - } - }, - "node_modules/@ardatan/relay-compiler": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", - "integrity": "sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/runtime": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "fbjs": "^3.0.0", - "glob": "^7.1.1", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "relay-compiler": "bin/relay-compiler" - }, - "peerDependencies": { - "graphql": "*" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/immutable": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", - "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "node_modules/@ardatan/relay-compiler/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@ardatan/relay-compiler/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@ardatan/sync-fetch": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz", - "integrity": "sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.1" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/@babel/code-frame": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", @@ -1937,21 +1707,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", - "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", @@ -2286,22 +2041,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz", - "integrity": "sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-flow": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", @@ -3005,28 +2744,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@deanwhillier/jest-matchmedia-mock": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@deanwhillier/jest-matchmedia-mock/-/jest-matchmedia-mock-1.2.0.tgz", @@ -3048,64 +2765,6 @@ "node": ">=10.0.0" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.1.3" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@emotion/cache": { "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", @@ -3239,86 +2898,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" }, - "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/is-prop-valid/node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, "node_modules/@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, - "node_modules/@emotion/react": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", - "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/react/node_modules/@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", - "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" - } - }, - "node_modules/@emotion/react/node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" - }, - "node_modules/@emotion/react/node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" - }, - "node_modules/@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", - "csstype": "^3.0.2" - } - }, "node_modules/@emotion/sheet": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", @@ -3329,19 +2913,6 @@ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" }, - "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", @@ -3390,94 +2961,6 @@ "react-dom": ">=16.8.0" } }, - "node_modules/@formatjs/cli": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-4.7.0.tgz", - "integrity": "sha512-XYy8rDLW64G4191rXdPjhxW8QrSc3vG3IfV6wfxJfWj5bwNEnsC1YixV4hnuiFs1OMT5I9MF9xQ0V7dauRqyUQ==", - "dev": true, - "dependencies": { - "@formatjs/icu-messageformat-parser": "2.0.16", - "@formatjs/ts-transformer": "3.8.1", - "@types/estree": "^0.0.50", - "@types/fs-extra": "^9.0.1", - "@types/json-stable-stringify": "^1.0.32", - "@types/node": "14", - "@vue/compiler-core": "^3.2.23", - "chalk": "^4.0.0", - "commander": "8", - "fast-glob": "^3.2.7", - "fs-extra": "10", - "json-stable-stringify": "^1.0.1", - "loud-rejection": "^2.2.0", - "tslib": "^2.1.0", - "typescript": "^4.5", - "vue": "^3.2.23" - }, - "bin": { - "formatjs": "bin/formatjs" - } - }, - "node_modules/@formatjs/cli/node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.1.tgz", - "integrity": "sha512-tgtNODZUGuUI6PAcnvaLZpGrZLVkXnnAvgzOiueYMzFdOdcOw4iH1WKhCe3+r6VR8rHKToJ2HksUGNCB+zt/bg==", - "dev": true, - "dependencies": { - "@formatjs/intl-localematcher": "0.2.22", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/cli/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.16.tgz", - "integrity": "sha512-sYg0ImXsAqBbjU/LotoCD9yKC5nUpWVy3s4DwWerHXD4sm62FcjMF8mekwudRk3eZLHqSO+M21MpFUUjDQ+Q5Q==", - "dev": true, - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.1", - "@formatjs/icu-skeleton-parser": "1.3.3", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/cli/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.3.tgz", - "integrity": "sha512-ifWnzjmHPHUF89UpCvClTP66sXYFc8W/qg7Qt+qtTUB9BqRWlFeUsevAzaMYDJsRiOy4S2WJFrJoZgRKUFfPGQ==", - "dev": true, - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.1", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/cli/node_modules/@formatjs/intl-localematcher": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.22.tgz", - "integrity": "sha512-z+TvbHW8Q/g2l7/PnfUl0mV9gWxV4d0HT6GQyzkO5QI6QjCvCZGiztnmLX7zoyS16uSMvZ2PoMDfSK9xvZkRRA==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/cli/node_modules/@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "node_modules/@formatjs/cli/node_modules/@types/node": { - "version": "14.18.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.37.tgz", - "integrity": "sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==", - "dev": true - }, - "node_modules/@formatjs/cli/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true, - "engines": { - "node": ">= 12" - } - }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz", @@ -3564,1013 +3047,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@formatjs/ts-transformer": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.8.1.tgz", - "integrity": "sha512-emndJkdURyan9i9KkZN1Oa+xWG0y5Y17PLFz76rE21mO4OOx02nEJ9wX12PWfxdlmzt9sjN0O7WlRUw10HUaFA==", - "dev": true, - "dependencies": { - "@formatjs/icu-messageformat-parser": "2.0.16", - "@types/node": "14 || 16", - "chalk": "^4.0.0", - "tslib": "^2.1.0", - "typescript": "^4.5" - }, - "peerDependencies": { - "ts-jest": "27" - }, - "peerDependenciesMeta": { - "ts-jest": { - "optional": true - } - } - }, - "node_modules/@formatjs/ts-transformer/node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.1.tgz", - "integrity": "sha512-tgtNODZUGuUI6PAcnvaLZpGrZLVkXnnAvgzOiueYMzFdOdcOw4iH1WKhCe3+r6VR8rHKToJ2HksUGNCB+zt/bg==", - "dev": true, - "dependencies": { - "@formatjs/intl-localematcher": "0.2.22", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/ts-transformer/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.16.tgz", - "integrity": "sha512-sYg0ImXsAqBbjU/LotoCD9yKC5nUpWVy3s4DwWerHXD4sm62FcjMF8mekwudRk3eZLHqSO+M21MpFUUjDQ+Q5Q==", - "dev": true, - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.1", - "@formatjs/icu-skeleton-parser": "1.3.3", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/ts-transformer/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.3.tgz", - "integrity": "sha512-ifWnzjmHPHUF89UpCvClTP66sXYFc8W/qg7Qt+qtTUB9BqRWlFeUsevAzaMYDJsRiOy4S2WJFrJoZgRKUFfPGQ==", - "dev": true, - "dependencies": { - "@formatjs/ecma402-abstract": "1.11.1", - "tslib": "^2.1.0" - } - }, - "node_modules/@formatjs/ts-transformer/node_modules/@formatjs/intl-localematcher": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.22.tgz", - "integrity": "sha512-z+TvbHW8Q/g2l7/PnfUl0mV9gWxV4d0HT6GQyzkO5QI6QjCvCZGiztnmLX7zoyS16uSMvZ2PoMDfSK9xvZkRRA==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@graphql-codegen/add": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-3.2.3.tgz", - "integrity": "sha512-sQOnWpMko4JLeykwyjFTxnhqjd/3NOG2OyMuvK76Wnnwh8DRrNf2VEs2kmSvLl7MndMlOj7Kh5U154dVcvhmKQ==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.1", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/cli": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.16.3.tgz", - "integrity": "sha512-dyRt4nvbpLmWSq+fNsYhQo5tDJyFdlEIX+detR6biOur+kjI9e8djMVa5XSojoDkRIQCifu++6nUHxeROXN8iw==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.18.13", - "@babel/template": "^7.18.10", - "@babel/types": "^7.18.13", - "@graphql-codegen/core": "2.6.8", - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/apollo-engine-loader": "^7.3.6", - "@graphql-tools/code-file-loader": "^7.3.13", - "@graphql-tools/git-loader": "^7.2.13", - "@graphql-tools/github-loader": "^7.3.20", - "@graphql-tools/graphql-file-loader": "^7.5.0", - "@graphql-tools/json-file-loader": "^7.4.1", - "@graphql-tools/load": "7.8.0", - "@graphql-tools/prisma-loader": "^7.2.49", - "@graphql-tools/url-loader": "^7.13.2", - "@graphql-tools/utils": "^9.0.0", - "@whatwg-node/fetch": "^0.5.0", - "chalk": "^4.1.0", - "chokidar": "^3.5.2", - "cosmiconfig": "^7.0.0", - "cosmiconfig-typescript-loader": "4.3.0", - "debounce": "^1.2.0", - "detect-indent": "^6.0.0", - "graphql-config": "4.3.6", - "inquirer": "^8.0.0", - "is-glob": "^4.0.1", - "json-to-pretty-yaml": "^1.2.2", - "listr2": "^4.0.5", - "log-symbols": "^4.0.0", - "shell-quote": "^1.7.3", - "string-env-interpolation": "^1.0.1", - "ts-log": "^2.2.3", - "tslib": "^2.4.0", - "yaml": "^1.10.0", - "yargs": "^17.0.0" - }, - "bin": { - "gql-gen": "cjs/bin.js", - "graphql-code-generator": "cjs/bin.js", - "graphql-codegen": "cjs/bin.js", - "graphql-codegen-esm": "esm/bin.js" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", - "ts-node": ">=10" - } - }, - "node_modules/@graphql-codegen/cli/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@graphql-codegen/cli/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@graphql-codegen/cli/node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-codegen/cli/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@graphql-codegen/cli/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@graphql-codegen/client-preset": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-1.2.5.tgz", - "integrity": "sha512-gACm+XkH9aukgYLURWlryWcG0bdXfxf/tiywpJWCy3QvNWky3zyvJoWK3Dh1UBryXFY5ktN2PSvNFbXwpyuz8g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/template": "^7.15.4", - "@graphql-codegen/add": "^3.2.3", - "@graphql-codegen/gql-tag-operations": "1.6.0", - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/typed-document-node": "^2.3.12", - "@graphql-codegen/typescript": "^2.8.7", - "@graphql-codegen/typescript-operations": "^2.5.12", - "@graphql-codegen/visitor-plugin-common": "^2.13.7", - "@graphql-tools/utils": "^9.0.0", - "@graphql-typed-document-node/core": "3.1.1", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/client-preset/node_modules/@graphql-typed-document-node/core": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "dev": true, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/core": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-2.6.8.tgz", - "integrity": "sha512-JKllNIipPrheRgl+/Hm/xuWMw9++xNQ12XJR/OHHgFopOg4zmN3TdlRSyYcv/K90hCFkkIwhlHFUQTfKrm8rxQ==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.1", - "@graphql-tools/schema": "^9.0.0", - "@graphql-tools/utils": "^9.1.1", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/gql-tag-operations": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-1.6.0.tgz", - "integrity": "sha512-wllGNBrYWuxA/E6NK0SQLWSbFyVv7zb6TIUGdjViohCgd1z5Bpn9Ohm1YBJXofxweAnyLyB5KCSPBwYkh439Zw==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/visitor-plugin-common": "2.13.7", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/gql-tag-operations/node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "2.13.7", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.7.tgz", - "integrity": "sha512-xE6iLDhr9sFM1qwCGJcCXRu5MyA0moapG2HVejwyAXXLubYKYwWnoiEigLH2b5iauh6xsl6XP8hh9D1T1dn5Cw==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/plugin-helpers": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", - "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/schema-ast": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-2.6.1.tgz", - "integrity": "sha512-5TNW3b1IHJjCh07D2yQNGDQzUpUl2AD+GVe1Dzjqyx/d2Fn0TPMxLsHsKPS4Plg4saO8FK/QO70wLsP7fdbQ1w==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/utils": "^9.0.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typed-document-node": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-2.3.13.tgz", - "integrity": "sha512-vt1hvBAbYTYUCXblks9KYwR5Ho16hWQljid5xgx77jeVufj5PjnWrOjJfEFKFx17VOM4CKHP8ryoeT4NyjYNWw==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.8.8.tgz", - "integrity": "sha512-A0oUi3Oy6+DormOlrTC4orxT9OBZkIglhbJBcDmk34jAKKUgesukXRd4yOhmTrnbchpXz2T8IAOFB3FWIaK4Rw==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/schema-ast": "^2.6.1", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/typescript-operations": { - "version": "2.5.13", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-2.5.13.tgz", - "integrity": "sha512-3vfR6Rx6iZU0JRt29GBkFlrSNTM6t+MSLF86ChvL4d/Jfo/JYAGuB3zNzPhirHYzJPCvLOAx2gy9ID1ltrpYiw==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/typescript": "^2.8.8", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-codegen/visitor-plugin-common": { - "version": "2.13.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.8.tgz", - "integrity": "sha512-IQWu99YV4wt8hGxIbBQPtqRuaWZhkQRG2IZKbMoSvh0vGeWb3dB0n0hSgKaOOxDY+tljtOf9MTcUYvJslQucMQ==", - "dev": true, - "dependencies": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - }, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/@graphql-tools/apollo-engine-loader": { - "version": "7.3.26", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-7.3.26.tgz", - "integrity": "sha512-h1vfhdJFjnCYn9b5EY1Z91JTF0KB3hHVJNQIsiUV2mpQXZdeOXQoaWeYEKaiI5R6kwBw5PP9B0fv3jfUIG8LyQ==", - "dev": true, - "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/utils": "^9.2.1", - "@whatwg-node/fetch": "^0.8.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/apollo-engine-loader/node_modules/@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "dependencies": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - }, - "node_modules/@graphql-tools/batch-execute": { - "version": "8.5.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.18.tgz", - "integrity": "sha512-mNv5bpZMLLwhkmPA6+RP81A6u3KF4CSKLf3VX9hbomOkQR4db8pNs8BOvpZU54wKsUzMzdlws/2g/Dabyb2Vsg==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "dataloader": "2.2.2", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/code-file-loader": { - "version": "7.3.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-7.3.21.tgz", - "integrity": "sha512-dj+OLnz1b8SYkXcuiy0CUQ25DWnOEyandDlOcdBqU3WVwh5EEVbn0oXUYm90fDlq2/uut00OrtC5Wpyhi3tAvA==", - "dev": true, - "dependencies": { - "@graphql-tools/graphql-tag-pluck": "7.5.0", - "@graphql-tools/utils": "9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/delegate": { - "version": "9.0.28", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-9.0.28.tgz", - "integrity": "sha512-8j23JCs2mgXqnp+5K0v4J3QBQU/5sXd9miaLvMfRf/6963DznOXTECyS9Gcvj1VEeR5CXIw6+aX/BvRDKDdN1g==", - "dev": true, - "dependencies": { - "@graphql-tools/batch-execute": "^8.5.18", - "@graphql-tools/executor": "^0.0.15", - "@graphql-tools/schema": "^9.0.16", - "@graphql-tools/utils": "^9.2.1", - "dataloader": "^2.2.2", - "tslib": "^2.5.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/delegate/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - }, - "node_modules/@graphql-tools/executor": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.15.tgz", - "integrity": "sha512-6U7QLZT8cEUxAMXDP4xXVplLi6RBwx7ih7TevlBto66A/qFp3PDb6o/VFo07yBKozr8PGMZ4jMfEWBGxmbGdxA==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "@graphql-typed-document-node/core": "3.1.2", - "@repeaterjs/repeater": "3.0.4", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-graphql-ws": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.12.tgz", - "integrity": "sha512-aFD79i9l282Ob5dOZ7JsyhhXXP1o8eQh0prYkSSVo/OU2ndzWigfANz4DJgWgS3LwBjLDlMcmaXPZZeXt3m4Tg==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "@repeaterjs/repeater": "3.0.4", - "@types/ws": "^8.0.0", - "graphql-ws": "5.12.0", - "isomorphic-ws": "5.0.0", - "tslib": "^2.4.0", - "ws": "8.12.1" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-graphql-ws/node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@graphql-tools/executor-http": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-0.1.9.tgz", - "integrity": "sha512-tNzMt5qc1ptlHKfpSv9wVBVKCZ7gks6Yb/JcYJluxZIT4qRV+TtOFjpptfBU63usgrGVOVcGjzWc/mt7KhmmpQ==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "^9.2.1", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/fetch": "^0.8.1", - "dset": "^3.1.2", - "extract-files": "^11.0.0", - "meros": "^1.2.1", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-http/node_modules/@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "dependencies": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - }, - "node_modules/@graphql-tools/executor-legacy-ws": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.9.tgz", - "integrity": "sha512-L7oDv7R5yoXzMH+KLKDB2WHVijfVW4dB2H+Ae1RdW3MFvwbYjhnIB6QzHqKEqksjp/FndtxZkbuTIuAOsYGTYw==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "@types/ws": "^8.0.0", - "isomorphic-ws": "5.0.0", - "tslib": "^2.4.0", - "ws": "8.12.1" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/executor-legacy-ws/node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@graphql-tools/git-loader": { - "version": "7.2.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.2.20.tgz", - "integrity": "sha512-D/3uwTzlXxG50HI8BEixqirT4xiUp6AesTdfotRXAs2d4CT9wC6yuIWOHkSBqgI1cwKWZb6KXZr467YPS5ob1w==", - "dev": true, - "dependencies": { - "@graphql-tools/graphql-tag-pluck": "7.5.0", - "@graphql-tools/utils": "9.2.1", - "is-glob": "4.0.3", - "micromatch": "^4.0.4", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/github-loader": { - "version": "7.3.27", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-7.3.27.tgz", - "integrity": "sha512-fFFC35qenyhjb8pfcYXKknAt0CXP5CkQYtLfJXgTXSgBjIsfAVMrqxQ/Y0ejeM19XNF/C3VWJ7rE308yOX6ywA==", - "dev": true, - "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/graphql-tag-pluck": "^7.4.6", - "@graphql-tools/utils": "^9.2.1", - "@whatwg-node/fetch": "^0.8.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/github-loader/node_modules/@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "dependencies": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - }, - "node_modules/@graphql-tools/graphql-file-loader": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.16.tgz", - "integrity": "sha512-lK1N3Y2I634FS12nd4bu7oAJbai3bUc28yeX+boT+C83KTO4ujGHm+6hPC8X/FRGwhKOnZBxUM7I5nvb3HiUxw==", - "dev": true, - "dependencies": { - "@graphql-tools/import": "6.7.17", - "@graphql-tools/utils": "9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/graphql-tag-pluck": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.0.tgz", - "integrity": "sha512-76SYzhSlH50ZWkhWH6OI94qrxa8Ww1ZeOU04MdtpSeQZVT2rjGWeTb3xM3kjTVWQJsr/YJBhDeNPGlwNUWfX4Q==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.16.8", - "@babel/plugin-syntax-import-assertions": "7.20.0", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/import": { - "version": "6.7.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.7.17.tgz", - "integrity": "sha512-bn9SgrECXq3WIasgNP7ful/uON51wBajPXtxdY+z/ce7jLWaFE6lzwTDB/GAgiZ+jo7nb0ravlxteSAz2qZmuA==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "resolve-from": "5.0.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/import/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@graphql-tools/json-file-loader": { - "version": "7.4.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.4.17.tgz", - "integrity": "sha512-KOSTP43nwjPfXgas90rLHAFgbcSep4nmiYyR9xRVz4ZAmw8VYHcKhOLTSGylCAzi7KUfyBXajoW+6Z7dQwdn3g==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/load": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-7.8.0.tgz", - "integrity": "sha512-l4FGgqMW0VOqo+NMYizwV8Zh+KtvVqOf93uaLo9wJ3sS3y/egPCgxPMDJJ/ufQZG3oZ/0oWeKt68qop3jY0yZg==", - "dev": true, - "dependencies": { - "@graphql-tools/schema": "9.0.4", - "@graphql-tools/utils": "8.12.0", - "p-limit": "3.1.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/load/node_modules/@graphql-tools/merge": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.6.tgz", - "integrity": "sha512-uUBokxXi89bj08P+iCvQk3Vew4vcfL5ZM6NTylWi8PIpoq4r5nJ625bRuN8h2uubEdRiH8ntN9M4xkd/j7AybQ==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "8.12.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/load/node_modules/@graphql-tools/schema": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.4.tgz", - "integrity": "sha512-B/b8ukjs18fq+/s7p97P8L1VMrwapYc3N2KvdG/uNThSazRRn8GsBK0Nr+FH+mVKiUfb4Dno79e3SumZVoHuOQ==", - "dev": true, - "dependencies": { - "@graphql-tools/merge": "8.3.6", - "@graphql-tools/utils": "8.12.0", - "tslib": "^2.4.0", - "value-or-promise": "1.0.11" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/load/node_modules/@graphql-tools/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-TeO+MJWGXjUTS52qfK4R8HiPoF/R7X+qmgtOYd8DTH0l6b+5Y/tlg5aGeUJefqImRq7nvi93Ms40k/Uz4D5CWw==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/load/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@graphql-tools/load/node_modules/value-or-promise": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", - "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@graphql-tools/merge": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.0.tgz", - "integrity": "sha512-3XYCWe0d3I4F1azNj1CdShlbHfTIfiDgj00R9uvFH8tHKh7i1IWN3F7QQYovcHKhayaR6zPok3YYMESYQcBoaA==", - "dev": true, - "dependencies": { - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/optimize": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.3.1.tgz", - "integrity": "sha512-5j5CZSRGWVobt4bgRRg7zhjPiSimk+/zIuColih8E8DxuFOaJ+t0qu7eZS5KXWBkjcd4BPNuhUPpNlEmHPqVRQ==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/prisma-loader": { - "version": "7.2.65", - "resolved": "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-7.2.65.tgz", - "integrity": "sha512-MuX4XrJEuOE6qYbjR9WhSInPX8iji07wA6K72hVgkvG2UBXBUAcVC/1H6iOFbs1d7rLLU2r6aWsYnD6zwzw+eA==", - "dev": true, - "dependencies": { - "@graphql-tools/url-loader": "7.17.14", - "@graphql-tools/utils": "9.2.1", - "@types/js-yaml": "^4.0.0", - "@types/json-stable-stringify": "^1.0.32", - "chalk": "^4.1.0", - "debug": "^4.3.1", - "dotenv": "^16.0.0", - "graphql-request": "^5.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "isomorphic-fetch": "^3.0.0", - "jose": "^4.11.4", - "js-yaml": "^4.0.0", - "json-stable-stringify": "^1.0.1", - "lodash": "^4.17.20", - "scuid": "^1.1.0", - "tslib": "^2.4.0", - "yaml-ast-parser": "^0.0.43" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@graphql-tools/prisma-loader/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/@graphql-tools/relay-operation-optimizer": { - "version": "6.5.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.17.tgz", - "integrity": "sha512-hHPEX6ccRF3+9kfVz0A3In//Dej7QrHOLGZEokBmPDMDqn9CS7qUjpjyGzclbOX0tRBtLfuFUZ68ABSac3P1nA==", - "dev": true, - "dependencies": { - "@ardatan/relay-compiler": "12.0.0", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/schema": { - "version": "9.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.17.tgz", - "integrity": "sha512-HVLq0ecbkuXhJlpZ50IHP5nlISqH2GbNgjBJhhRzHeXhfwlUOT4ISXGquWTmuq61K0xSaO0aCjMpxe4QYbKTng==", - "dev": true, - "dependencies": { - "@graphql-tools/merge": "8.4.0", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/url-loader": { - "version": "7.17.14", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.17.14.tgz", - "integrity": "sha512-7boEmrZlbViqQSSvu2VFCGi9YAY7E0BCVObiv1sLYbFR+62mo825As0haU5l7wlx1zCDyUlOleNz+X2jVvBbSQ==", - "dev": true, - "dependencies": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/delegate": "^9.0.27", - "@graphql-tools/executor-graphql-ws": "^0.0.12", - "@graphql-tools/executor-http": "^0.1.7", - "@graphql-tools/executor-legacy-ws": "^0.0.9", - "@graphql-tools/utils": "^9.2.1", - "@graphql-tools/wrap": "^9.3.8", - "@types/ws": "^8.0.0", - "@whatwg-node/fetch": "^0.8.0", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.11", - "ws": "^8.12.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/url-loader/node_modules/@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "dependencies": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - }, - "node_modules/@graphql-tools/url-loader/node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-tools/wrap": { - "version": "9.3.8", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.3.8.tgz", - "integrity": "sha512-MGsExYPiILMw4Qff7HcvE9MMSYdjb/tr5IQYJbxJIU4/TrBHox1/smne8HG+Bd7kmDlTTj7nU/Z8sxmoRd0hOQ==", - "dev": true, - "dependencies": { - "@graphql-tools/delegate": "9.0.28", - "@graphql-tools/schema": "9.0.17", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.2.tgz", - "integrity": "sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA==", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@guyplusplus/turndown-plugin-gfm": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@guyplusplus/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.7.tgz", @@ -4612,12 +3088,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", - "dev": true - }, "node_modules/@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -5632,16 +4102,6 @@ "resolved": "platform/types", "link": true }, - "node_modules/@mdi/js": { - "version": "6.9.96", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.9.96.tgz", - "integrity": "sha512-rK0/vLFaiItYS2W7uVmaKPKnhNQE4XVkylpk5njtVwENnp8elwY5uRL6qvdj2esuvUHG7DwygE4Qu3eKxxuJiQ==" - }, - "node_modules/@mdi/react": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.5.0.tgz", - "integrity": "sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w==" - }, "node_modules/@mdn/browser-compat-data": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz", @@ -5974,45 +4434,6 @@ "node": ">= 8" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz", - "integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==", - "dev": true, - "dependencies": { - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", - "dev": true, - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@peculiar/webcrypto": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", - "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", - "dev": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.3.0", - "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.1", - "webcrypto-core": "^1.7.4" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -6041,33 +4462,6 @@ "redux": "^3.1.0 || ^4.0.0" } }, - "node_modules/@repeaterjs/repeater": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", - "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", - "dev": true - }, - "node_modules/@restart/context": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", - "dev": true, - "peerDependencies": { - "react": ">=16.3.2" - } - }, - "node_modules/@restart/hooks": { - "version": "0.3.27", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", - "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", - "dev": true, - "dependencies": { - "dequal": "^2.0.2" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -6351,36 +4745,6 @@ "react-dom": "*" } }, - "node_modules/@testing-library/react-hooks": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.0.tgz", - "integrity": "sha512-uZqcgtcUUtw7Z9N32W13qQhVAD+Xki2hxbTR461MKax8T6Jr8nsUvZB+vcBTkzY2nFvsUet434CsgF0ncW2yFw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", - "react-test-renderer": "^16.9.0 || ^17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-test-renderer": { - "optional": true - } - } - }, "node_modules/@tippyjs/react": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", @@ -6412,42 +4776,6 @@ "node": ">=10.13.0" } }, - "node_modules/@ts-morph/common": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", - "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.7", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, "node_modules/@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -6572,12 +4900,6 @@ "integrity": "sha512-aCE6SP9WQJkq9kNFDRdxpUfWhazvreNSPtm1BQG1B4/xSpKbRwfweVAChYx6MMLyTguCKJExGZ1C52Z8DlFSGg==", "dev": true }, - "node_modules/@types/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", - "dev": true - }, "node_modules/@types/enzyme": { "version": "3.10.11", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.11.tgz", @@ -6638,15 +4960,6 @@ "@types/send": "*" } }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -6696,12 +5009,6 @@ "@types/node": "*" } }, - "node_modules/@types/invariant": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==", - "dev": true - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -6785,17 +5092,6 @@ "@types/sizzle": "*" } }, - "node_modules/@types/js-cookie": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", - "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" - }, - "node_modules/@types/js-yaml": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", - "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", - "dev": true - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -6981,15 +5277,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-infinite-scroller": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.3.tgz", - "integrity": "sha512-l60JckVoO+dxmKW2eEG7jbliEpITsTJvRPTe97GazjF5+ylagAuyYdXl8YY9DQsTP9QjhqGKZROknzgscGJy0A==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.2.tgz", @@ -7040,26 +5327,6 @@ "@types/react-router": "*" } }, - "node_modules/@types/react-router-hash-link": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@types/react-router-hash-link/-/react-router-hash-link-2.4.5.tgz", - "integrity": "sha512-YsiD8xCWtRBebzPqG6kXjDQCI35LCN9MhV/MbgYF8y0trOp7VSUNmSj8HdIGyH99WCfSOLZB2pIwUMN/IwIDQg==", - "dev": true, - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-dom": "*" - } - }, - "node_modules/@types/react-test-renderer": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", - "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -7234,12 +5501,6 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, - "node_modules/@types/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==", - "dev": true - }, "node_modules/@types/webpack-env": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", @@ -7339,142 +5600,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vue/compiler-core": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", - "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.47", - "estree-walker": "^2.0.2", - "source-map": "^0.6.1" - } - }, - "node_modules/@vue/compiler-core/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz", - "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==", - "dev": true, - "dependencies": { - "@vue/compiler-core": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz", - "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.47", - "@vue/compiler-dom": "3.2.47", - "@vue/compiler-ssr": "3.2.47", - "@vue/reactivity-transform": "3.2.47", - "@vue/shared": "3.2.47", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", - "postcss": "^8.1.10", - "source-map": "^0.6.1" - } - }, - "node_modules/@vue/compiler-sfc/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz", - "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==", - "dev": true, - "dependencies": { - "@vue/compiler-dom": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", - "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==", - "dev": true, - "dependencies": { - "@vue/shared": "3.2.47" - } - }, - "node_modules/@vue/reactivity-transform": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz", - "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.47", - "@vue/shared": "3.2.47", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz", - "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==", - "dev": true, - "dependencies": { - "@vue/reactivity": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz", - "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==", - "dev": true, - "dependencies": { - "@vue/runtime-core": "3.2.47", - "@vue/shared": "3.2.47", - "csstype": "^2.6.8" - } - }, - "node_modules/@vue/runtime-dom/node_modules/csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", - "dev": true - }, - "node_modules/@vue/server-renderer": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz", - "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==", - "dev": true, - "dependencies": { - "@vue/compiler-ssr": "3.2.47", - "@vue/shared": "3.2.47" - }, - "peerDependencies": { - "vue": "3.2.47" - } - }, - "node_modules/@vue/shared": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", - "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==", - "dev": true - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", @@ -7674,82 +5799,6 @@ "node": ">=8.0.0" } }, - "node_modules/@whatwg-node/events": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz", - "integrity": "sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==", - "dev": true - }, - "node_modules/@whatwg-node/fetch": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.5.4.tgz", - "integrity": "sha512-dR5PCzvOeS7OaW6dpIlPt+Ou3pak7IEG+ZVAV26ltcaiDB3+IpuvjqRdhsY6FKHcqBo1qD+S99WXY9Z6+9Rwnw==", - "dev": true, - "dependencies": { - "@peculiar/webcrypto": "^1.4.0", - "abort-controller": "^3.0.0", - "busboy": "^1.6.0", - "form-data-encoder": "^1.7.1", - "formdata-node": "^4.3.1", - "node-fetch": "^2.6.7", - "undici": "^5.12.0", - "web-streams-polyfill": "^3.2.0" - } - }, - "node_modules/@whatwg-node/node-fetch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.3.1.tgz", - "integrity": "sha512-/U4onp5eBkRHfFe/VL+ppyupqj7z6iBtjcuPSosQNH2/y+LxRn5lyFb7Vqhb5DokjrDMjssLcqiVYnx+UABFsw==", - "dev": true, - "dependencies": { - "@whatwg-node/events": "^0.0.2", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "fast-url-parser": "^1.1.3", - "tslib": "^2.3.1" - }, - "peerDependencies": { - "@types/node": "^18.0.6" - } - }, - "node_modules/@wry/context": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.0.tgz", - "integrity": "sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/equality": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.3.tgz", - "integrity": "sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/trie": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.2.tgz", - "integrity": "sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@xobotyi/scrollbar-width": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", - "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -7768,18 +5817,6 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7859,19 +5896,6 @@ "node": ">= 6.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/airbnb-prop-types": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", @@ -8079,12 +6103,6 @@ "node": ">=4" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -8129,15 +6147,6 @@ "integrity": "sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==", "dev": true }, - "node_modules/array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -8251,12 +6260,6 @@ "node": ">=0.10.0" } }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, "node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -8273,20 +6276,6 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, - "node_modules/asn1js": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", - "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", - "dev": true, - "dependencies": { - "pvtsutils": "^1.3.2", - "pvutils": "^1.1.3", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -8319,18 +6308,6 @@ "node": ">= 4.5.0" } }, - "node_modules/auto-bind": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", - "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/autoprefixer": { "version": "9.8.8", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", @@ -8524,12 +6501,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/babel-plugin-add-react-displayname": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", - "dev": true - }, "node_modules/babel-plugin-formatjs": { "version": "10.5.1", "resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.5.1.tgz", @@ -8725,12 +6696,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" }, - "node_modules/babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true - }, "node_modules/babel-plugin-typescript-to-proptypes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-typescript-to-proptypes/-/babel-plugin-typescript-to-proptypes-2.1.0.tgz", @@ -8773,44 +6738,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/babel-preset-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", @@ -9803,18 +7730,6 @@ "semver": "bin/semver.js" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -9864,6 +7779,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -9940,17 +7856,6 @@ } ] }, - "node_modules/capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, "node_modules/caw": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", @@ -10004,44 +7909,6 @@ "node": ">=8" } }, - "node_modules/change-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", - "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, - "dependencies": { - "camel-case": "^4.1.2", - "capital-case": "^1.0.4", - "constant-case": "^3.0.4", - "dot-case": "^3.0.4", - "header-case": "^2.0.4", - "no-case": "^3.0.4", - "param-case": "^3.0.4", - "pascal-case": "^3.1.2", - "path-case": "^3.0.4", - "sentence-case": "^3.0.4", - "snake-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/change-case-all": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.15.tgz", - "integrity": "sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==", - "dev": true, - "dependencies": { - "change-case": "^4.1.2", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", - "lower-case": "^2.0.2", - "lower-case-first": "^2.0.2", - "sponge-case": "^1.0.1", - "swap-case": "^2.0.2", - "title-case": "^3.0.3", - "upper-case": "^2.0.2", - "upper-case-first": "^2.0.2" - } - }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -10081,25 +7948,11 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, "node_modules/chart.js": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.2.tgz", "integrity": "sha512-7rqSlHWMUKFyBDOJvmFGW2lxULtcwaPLegDjX/Nu5j6QybY+GCiQkEY+6cqHw62S5tcwXMD8Y+H5OBGoR7d+ZQ==" }, - "node_modules/chartjs-plugin-annotation": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-2.1.2.tgz", - "integrity": "sha512-kmEp2WtpogwnKKnDPO3iO3mVwvVGtmG5BkZVtAEZm5YzJ9CYxojjYEgk7OTrFbJ5vU098b84UeJRe8kRfNcq5g==", - "peerDependencies": { - "chart.js": ">=3.7.0" - } - }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -10174,14 +8027,6 @@ "node": ">=6.0" } }, - "node_modules/chrono-node": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.3.5.tgz", - "integrity": "sha512-QIWEgXYVn55/Nsgdqbe6inqW+GoK3B6Qtga8AWdpq+nd+mOZVMxa+SGwPq/XjY+nKN+toQGu8KifCPwUkmz2sg==", - "dependencies": { - "dayjs": "^1.10.0" - } - }, "node_modules/ci-info": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.2.tgz", @@ -10211,78 +8056,6 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", - "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -10294,15 +8067,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -10357,12 +8121,6 @@ "node": ">= 0.12.0" } }, - "node_modules/code-block-writer": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", - "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", - "dev": true - }, "node_modules/collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -10422,15 +8180,6 @@ "node": ">= 0.8" } }, - "node_modules/common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -10626,17 +8375,6 @@ "node": ">=0.8" } }, - "node_modules/constant-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", - "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case": "^2.0.2" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -10678,14 +8416,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, "node_modules/copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -10850,31 +8580,6 @@ "node": ">=8" } }, - "node_modules/cosmiconfig-toml-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz", - "integrity": "sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA==", - "dev": true, - "dependencies": { - "@iarna/toml": "^2.2.5" - } - }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", - "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", - "dev": true, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=7", - "ts-node": ">=10", - "typescript": ">=3" - } - }, "node_modules/country-list": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/country-list/-/country-list-2.2.0.tgz", @@ -10919,12 +8624,6 @@ "sha.js": "^2.4.8" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -10943,15 +8642,6 @@ "yarn": ">=1" } }, - "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "dependencies": { - "node-fetch": "2.6.7" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -11041,14 +8731,6 @@ "node": "*" } }, - "node_modules/css-in-js-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", - "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", - "dependencies": { - "hyphenate-style-name": "^1.0.3" - } - }, "node_modules/css-loader": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", @@ -11105,6 +8787,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "optional": true, "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -11117,6 +8801,8 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -11217,18 +8903,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "node_modules/currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "dependencies": { - "array-find-index": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cwebp-bin": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cwebp-bin/-/cwebp-bin-7.0.1.tgz", @@ -11270,12 +8944,6 @@ "node": ">=10" } }, - "node_modules/dataloader": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", - "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", - "dev": true - }, "node_modules/date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", @@ -11288,16 +8956,6 @@ "url": "https://opencollective.com/date-fns" } }, - "node_modules/dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11674,18 +9332,6 @@ "node": ">=8" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -11728,24 +9374,6 @@ "node": ">= 0.8" } }, - "node_modules/dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -11789,15 +9417,6 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -12066,15 +9685,6 @@ "node": ">=4" } }, - "node_modules/dset": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", - "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -12375,14 +9985,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "dependencies": { - "stackframe": "^1.3.4" - } - }, "node_modules/es-abstract": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", @@ -12641,25 +10243,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/eslint-import-resolver-webpack": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.2.tgz", @@ -12802,21 +10385,6 @@ "eslint": ">=7.7.0" } }, - "node_modules/eslint-plugin-import-newlines": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.3.0.tgz", - "integrity": "sha512-8rokf6NvxC10ugA1VNmzEIO75CzId7IDF3Ai2GNXl0Xr4VORpb8u+bxsjRuE+2BS8MfDbrK/MHUQZI2G9qQyyA==", - "dev": true, - "bin": { - "import-linter": "lib/index.js" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", @@ -12873,12 +10441,6 @@ "jsx-ast-utils": "^3.3.3" } }, - "node_modules/eslint-plugin-no-relative-import-paths": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-relative-import-paths/-/eslint-plugin-no-relative-import-paths-1.5.0.tgz", - "integrity": "sha512-4if/PGGzpSP+DfOscGZ2nYd8XObc5rN9Ux5lhDzAVqLEaz8XD3ikzApSogXBdf+lnpTsTE5LJSqXFmFpNkZ3LQ==", - "dev": true - }, "node_modules/eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", @@ -12891,36 +10453,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, - "node_modules/eslint-plugin-unused-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", - "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", - "dev": true, - "dependencies": { - "eslint-rule-composer": "^0.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", - "eslint": "^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - } - } - }, - "node_modules/eslint-rule-composer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -13285,15 +10817,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -13716,20 +11239,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/external-remotes-plugin": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/external-remotes-plugin/-/external-remotes-plugin-1.0.0.tgz", @@ -13742,24 +11251,6 @@ "webpack": "^5" } }, - "node_modules/extract-files": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", - "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", - "dev": true, - "engines": { - "node": "^12.20 || >= 14.13" - }, - "funding": { - "url": "https://github.com/sponsors/jaydenseric" - } - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -13792,40 +11283,6 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, - "node_modules/fast-loops": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", - "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" - }, - "node_modules/fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", - "dev": true, - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-shallow-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", - "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" - }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "dev": true, - "dependencies": { - "punycode": "^1.3.2" - } - }, - "node_modules/fast-url-parser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - }, "node_modules/fast-xml-parser": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", @@ -13852,11 +11309,6 @@ "node": ">= 4.9.1" } }, - "node_modules/fastest-stable-stringify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", - "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" - }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -13887,46 +11339,6 @@ "bser": "2.1.1" } }, - "node_modules/fbjs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz", - "integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==", - "dev": true, - "dependencies": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.30" - } - }, - "node_modules/fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "dev": true - }, - "node_modules/fbjs/node_modules/ua-parser-js": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz", - "integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "engines": { - "node": "*" - } - }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -13937,21 +11349,6 @@ "pend": "~1.2.0" } }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -14200,34 +11597,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "dev": true - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dev": true, - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/formdata-node/node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "dev": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -14304,29 +11673,6 @@ "dev": true, "optional": true }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/fs-monkey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", @@ -14412,6 +11758,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -14790,148 +12137,6 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "node_modules/graphql": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", - "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==", - "engines": { - "node": "^12.22.0 || ^14.16.0 || >=16.0.0" - } - }, - "node_modules/graphql-config": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-4.3.6.tgz", - "integrity": "sha512-i7mAPwc0LAZPnYu2bI8B6yXU5820Wy/ArvmOseDLZIu0OU1UTULEuexHo6ZcHXeT9NvGGaUPQZm8NV3z79YydA==", - "dev": true, - "dependencies": { - "@graphql-tools/graphql-file-loader": "^7.3.7", - "@graphql-tools/json-file-loader": "^7.3.7", - "@graphql-tools/load": "^7.5.5", - "@graphql-tools/merge": "^8.2.6", - "@graphql-tools/url-loader": "^7.9.7", - "@graphql-tools/utils": "^8.6.5", - "cosmiconfig": "7.0.1", - "cosmiconfig-toml-loader": "1.0.0", - "cosmiconfig-typescript-loader": "^4.0.0", - "minimatch": "4.2.1", - "string-env-interpolation": "1.0.1", - "ts-node": "^10.8.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/graphql-config/node_modules/@graphql-tools/utils": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz", - "integrity": "sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, - "node_modules/graphql-config/node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/graphql-config/node_modules/minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/graphql-request": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-5.2.0.tgz", - "integrity": "sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ==", - "dev": true, - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "cross-fetch": "^3.1.5", - "extract-files": "^9.0.0", - "form-data": "^3.0.0" - }, - "peerDependencies": { - "graphql": "14 - 16" - } - }, - "node_modules/graphql-request/node_modules/extract-files": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", - "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==", - "dev": true, - "engines": { - "node": "^10.17.0 || ^12.0.0 || >= 13.7.0" - }, - "funding": { - "url": "https://github.com/sponsors/jaydenseric" - } - }, - "node_modules/graphql-request/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/graphql-tag": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" - } - }, - "node_modules/graphql-ws": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.0.tgz", - "integrity": "sha512-PA3ImUp8utrpEjoxBMhvxsjkStvFEdU0E1gEBREt8HZIWkxOUymwJBhFnBL7t/iHhUq1GVPeZevPinkZFENxTw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": ">=0.11 <=16" - } - }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -15022,6 +12227,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -15088,16 +12294,6 @@ "he": "bin/he" } }, - "node_modules/header-case": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", - "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, - "dependencies": { - "capital-case": "^1.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/helpertypes": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/helpertypes/-/helpertypes-0.0.18.tgz", @@ -15522,11 +12718,6 @@ "node": ">=10.17.0" } }, - "node_modules/hyphenate-style-name": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -15985,18 +13176,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", - "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", - "dev": true, - "engines": { - "node": ">=12.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/import-lazy": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", @@ -16065,46 +13244,11 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "node_modules/inline-style-prefixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", - "integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==", - "dependencies": { - "css-in-js-utils": "^3.1.0", - "fast-loops": "^1.1.3" - } - }, "node_modules/inobounce": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/inobounce/-/inobounce-0.2.1.tgz", "integrity": "sha512-dmKhRDbUS3zGD8HDGchsZBuxaXnfFM+2jXrZpnEnBToEWCgcs3lBfCQe0wzkbpIoJwU/lufaMquSyWoX8OXTRw==" }, - "node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -16170,19 +13314,6 @@ "node": ">= 10" } }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -16467,15 +13598,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-jpg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", @@ -16486,15 +13608,6 @@ "node": ">=6" } }, - "node_modules/is-lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", - "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -16643,18 +13756,6 @@ "node": ">=6" } }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "dependencies": { - "is-unc-path": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", @@ -16773,18 +13874,6 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -16797,15 +13886,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", - "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -16840,15 +13920,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -16901,15 +13972,6 @@ "whatwg-fetch": "^3.4.1" } }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "dev": true, - "peerDependencies": { - "ws": "*" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -17175,40 +14237,6 @@ "node": ">=8" } }, - "node_modules/jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, "node_modules/jest-config": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", @@ -18014,26 +15042,12 @@ "regenerator-runtime": "^0.13.3" } }, - "node_modules/jose": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.13.1.tgz", - "integrity": "sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "dev": true }, - "node_modules/js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" - }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -18049,11 +15063,6 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "node_modules/js-trim-multiline-string": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/js-trim-multiline-string/-/js-trim-multiline-string-1.0.8.tgz", - "integrity": "sha512-EFAZ/l2Pgt0hVA+tPgt8y2tpB1KiTJdkWMj9N4OrA9olkrJ01KthyX5Z/ieT2xT8tqPxu3PnwuoCky4mDwMmwg==" - }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -18204,19 +15213,6 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, - "node_modules/json-to-pretty-yaml": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", - "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", - "dev": true, - "dependencies": { - "remedial": "^1.0.7", - "remove-trailing-spaces": "^1.0.6" - }, - "engines": { - "node": ">= 0.2.0" - } - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -18229,27 +15225,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/jsonify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", @@ -18417,33 +15392,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/listr2": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", - "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", - "dev": true, - "dependencies": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.5", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "enquirer": ">= 2.3.0 < 3" - }, - "peerDependenciesMeta": { - "enquirer": { - "optional": true - } - } - }, "node_modules/load-bmfont": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", @@ -18649,38 +15597,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/longest-streak": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", @@ -18702,19 +15618,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loud-rejection": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-2.2.0.tgz", - "integrity": "sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==", - "dev": true, - "dependencies": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -18724,15 +15627,6 @@ "tslib": "^2.0.3" } }, - "node_modules/lower-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz", - "integrity": "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -18780,12 +15674,6 @@ "sourcemap-codec": "^1.4.8" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -18795,15 +15683,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -18906,7 +15785,9 @@ "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true, + "optional": true }, "node_modules/media-typer": { "version": "0.3.0", @@ -19173,23 +16054,6 @@ "node": ">= 8" } }, - "node_modules/meros": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", - "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", - "dev": true, - "engines": { - "node": ">=13" - }, - "peerDependencies": { - "@types/node": ">=13" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -19678,31 +16542,6 @@ "multicast-dns": "cli.js" } }, - "node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "node_modules/nano-css": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz", - "integrity": "sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==", - "dependencies": { - "css-tree": "^1.1.2", - "csstype": "^3.0.6", - "fastest-stable-stringify": "^2.0.2", - "inline-style-prefixer": "^6.0.0", - "rtl-css-js": "^1.14.0", - "sourcemap-codec": "^1.4.8", - "stacktrace-js": "^2.0.2", - "stylis": "^4.0.6" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, "node_modules/nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", @@ -19802,25 +16641,6 @@ "node": ">= 10.13" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -20049,12 +16869,6 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true - }, "node_modules/num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -20079,6 +16893,7 @@ "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -20279,15 +17094,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/optimism": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.2.tgz", - "integrity": "sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ==", - "dependencies": { - "@wry/context": "^0.7.0", - "@wry/trie": "^0.3.0" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -20323,64 +17129,6 @@ "node": ">=10" } }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/ora/node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/os-filter-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", @@ -20394,15 +17142,6 @@ "node": ">=4" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ow": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/ow/-/ow-0.17.0.tgz", @@ -20475,21 +17214,6 @@ "node": ">=4" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-map-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", @@ -20647,11 +17371,6 @@ "xml2js": "^0.4.5" } }, - "node_modules/parse-duration": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.2.tgz", - "integrity": "sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg==" - }, "node_modules/parse-entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", @@ -20670,20 +17389,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/parse-headers": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", @@ -20751,22 +17456,6 @@ "tslib": "^2.0.3" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", - "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -20790,27 +17479,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "dependencies": { - "path-root-regex": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -21035,10 +17703,6 @@ "node": ">=8" } }, - "node_modules/playbooks": { - "resolved": "playbooks", - "link": true - }, "node_modules/pngjs": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", @@ -21669,15 +18333,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "dependencies": { - "asap": "~2.0.3" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -21849,24 +18504,6 @@ "node": ">=6" } }, - "node_modules/pvtsutils": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", - "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", - "dev": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/pvutils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -22088,15 +18725,6 @@ "react-dom": ">=15.0.0" } }, - "node_modules/react-chartjs-2": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz", - "integrity": "sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==", - "peerDependencies": { - "chart.js": "^3.5.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", @@ -22168,22 +18796,6 @@ "react": "17.0.2" } }, - "node_modules/react-error-boundary": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", - "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, - "peerDependencies": { - "react": ">=16.13.1" - } - }, "node_modules/react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", @@ -22225,39 +18837,6 @@ "node": ">= 8" } }, - "node_modules/react-infinite-scroll-component": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", - "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", - "dependencies": { - "throttle-debounce": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/react-infinite-scroller": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", - "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", - "dependencies": { - "prop-types": "^15.5.8" - }, - "peerDependencies": { - "react": "^0.14.9 || ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-input-autosize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", - "integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==", - "dependencies": { - "prop-types": "^15.5.8" - }, - "peerDependencies": { - "react": "^16.3.0 || ^17.0.0" - } - }, "node_modules/react-intl": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.3.2.tgz", @@ -22434,18 +19013,6 @@ "prop-types": "^15.6.0" } }, - "node_modules/react-router-hash-link": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz", - "integrity": "sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==", - "dependencies": { - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": ">=15", - "react-router-dom": ">=4" - } - }, "node_modules/react-router/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -22503,48 +19070,6 @@ "csstype": "^3.0.2" } }, - "node_modules/react-universal-interface": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", - "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", - "peerDependencies": { - "react": "*", - "tslib": "*" - } - }, - "node_modules/react-use": { - "version": "17.3.2", - "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz", - "integrity": "sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw==", - "dependencies": { - "@types/js-cookie": "^2.2.6", - "@xobotyi/scrollbar-width": "^1.9.5", - "copy-to-clipboard": "^3.3.1", - "fast-deep-equal": "^3.1.3", - "fast-shallow-equal": "^1.0.0", - "js-cookie": "^2.2.1", - "nano-css": "^5.3.1", - "react-universal-interface": "^0.6.2", - "resize-observer-polyfill": "^1.5.1", - "screenfull": "^5.1.0", - "set-harmonic-interval": "^1.0.1", - "throttle-debounce": "^3.0.1", - "ts-easing": "^0.2.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "node_modules/react-use/node_modules/throttle-debounce": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", - "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", - "engines": { - "node": ">=10" - } - }, "node_modules/react-virtualized-auto-sizer": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz", @@ -22840,17 +19365,6 @@ "node": ">= 0.10" } }, - "node_modules/relay-runtime": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", - "integrity": "sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.0.0", - "fbjs": "^3.0.0", - "invariant": "^2.2.4" - } - }, "node_modules/remark": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", @@ -22892,27 +19406,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/remedial": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", - "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, - "node_modules/remove-trailing-spaces": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz", - "integrity": "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==", - "dev": true - }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -23049,12 +19542,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "node_modules/require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", @@ -23067,16 +19554,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, - "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" - }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -23136,14 +19613,6 @@ "node": ">=10" } }, - "node_modules/response-iterator": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", - "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==", - "engines": { - "node": ">=0.8" - } - }, "node_modules/responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -23154,19 +19623,6 @@ "lowercase-keys": "^1.0.0" } }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -23195,12 +19651,6 @@ "node": ">=0.10.0" } }, - "node_modules/rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -23393,29 +19843,12 @@ "nearley": "^2.7.10" } }, - "node_modules/rtl-css-js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", - "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", - "dependencies": { - "@babel/runtime": "^7.1.2" - } - }, "node_modules/rudder-sdk-js": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/rudder-sdk-js/-/rudder-sdk-js-1.0.16.tgz", "integrity": "sha512-LbxFZBRpgOKp83EDzwSSjfOavj66vwQwdUZ5+h6BYOLLtnWPxiXTKXegoEujNhej6hTLQMVFLktsMoIkzYojCQ==", "deprecated": "1.x.x versions of the SDK are deprecated. Please upgrade to the latest (2.x.x) version" }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -23592,17 +20025,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/scroll-into-view-if-needed": { "version": "2.2.29", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", @@ -23611,12 +20033,6 @@ "compute-scroll-into-view": "^1.0.17" } }, - "node_modules/scuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", - "integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==", - "dev": true - }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -23748,17 +20164,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/sentence-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", - "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, "node_modules/serialize-error": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.0.1.tgz", @@ -23886,26 +20291,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, - "node_modules/set-harmonic-interval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", - "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", - "engines": { - "node": ">=6.9" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -23980,6 +20365,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -23995,12 +20381,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "node_modules/signedsource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", - "integrity": "sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==", - "dev": true - }, "node_modules/sirv": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -24055,16 +20435,6 @@ "scroll-into-view-if-needed": "^2.2.28" } }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -24181,7 +20551,8 @@ "node_modules/sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true }, "node_modules/spawn-command": { "version": "0.0.2-1", @@ -24260,15 +20631,6 @@ "specificity": "bin/specificity" } }, - "node_modules/sponge-case": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz", - "integrity": "sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -24283,14 +20645,6 @@ "dev": true, "optional": true }, - "node_modules/stack-generator": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", - "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", - "dependencies": { - "stackframe": "^1.3.4" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -24312,38 +20666,6 @@ "node": ">=8" } }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "node_modules/stacktrace-gps": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", - "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", - "dependencies": { - "source-map": "0.5.6", - "stackframe": "^1.3.4" - } - }, - "node_modules/stacktrace-gps/node_modules/source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stacktrace-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", - "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", - "dependencies": { - "error-stack-parser": "^2.0.6", - "stack-generator": "^2.0.5", - "stacktrace-gps": "^3.0.4" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -24362,15 +20684,6 @@ "readable-stream": "^3.5.0" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -24389,12 +20702,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-env-interpolation": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", - "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", - "dev": true - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -24967,11 +21274,6 @@ "node": ">=0.10.0" } }, - "node_modules/stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" - }, "node_modules/sugarss": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", @@ -25180,23 +21482,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/swap-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz", - "integrity": "sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/symbol-observable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", - "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", - "engines": { - "node": ">=0.10" - } - }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -25386,19 +21671,12 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, - "node_modules/throttle-debounce": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", - "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "optional": true }, "node_modules/thunky": { "version": "1.1.0", @@ -25453,27 +21731,6 @@ "@popperjs/core": "^2.9.0" } }, - "node_modules/title-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", - "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -25528,11 +21785,6 @@ "to-no-case": "^1.0.0" } }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -25625,15 +21877,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/true-myth": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", - "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", - "dev": true, - "engines": { - "node": "10.* || >= 12.*" - } - }, "node_modules/ts-clone-node": { "version": "0.3.32", "resolved": "https://registry.npmjs.org/ts-clone-node/-/ts-clone-node-0.3.32.tgz", @@ -25653,144 +21896,6 @@ "typescript": "^3.x || ^4.x" } }, - "node_modules/ts-easing": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", - "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" - }, - "node_modules/ts-invariant": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", - "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ts-log": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.5.tgz", - "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==", - "dev": true - }, - "node_modules/ts-morph": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz", - "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", - "dev": true, - "dependencies": { - "@ts-morph/common": "~0.12.3", - "code-block-writer": "^11.0.0" - } - }, - "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ts-node/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ts-prune": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/ts-prune/-/ts-prune-0.10.3.tgz", - "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", - "dev": true, - "dependencies": { - "commander": "^6.2.1", - "cosmiconfig": "^7.0.1", - "json5": "^2.1.3", - "lodash": "^4.17.21", - "true-myth": "^4.1.0", - "ts-morph": "^13.0.1" - }, - "bin": { - "ts-prune": "lib/index.js" - } - }, - "node_modules/ts-prune/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ts-prune/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -26008,15 +22113,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/uncontrollable": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-5.1.0.tgz", @@ -26028,18 +22124,6 @@ "react": ">=15.0.0" } }, - "node_modules/undici": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", - "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", - "dev": true, - "dependencies": { - "busboy": "^1.6.0" - }, - "engines": { - "node": ">=12.18" - } - }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -26170,30 +22254,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unixify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", - "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", - "dev": true, - "dependencies": { - "normalize-path": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unixify/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -26229,24 +22289,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", - "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "dependencies": { - "tslib": "^2.0.3" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -26289,28 +22331,6 @@ "node": ">= 4" } }, - "node_modules/urlpattern-polyfill": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz", - "integrity": "sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==", - "dev": true, - "dependencies": { - "braces": "^3.0.2" - } - }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-memo-one": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", @@ -26363,12 +22383,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -26407,15 +22421,6 @@ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" }, - "node_modules/value-or-promise": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", - "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -26455,19 +22460,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/vue": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz", - "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==", - "dev": true, - "dependencies": { - "@vue/compiler-dom": "3.2.47", - "@vue/compiler-sfc": "3.2.47", - "@vue/runtime-dom": "3.2.47", - "@vue/server-renderer": "3.2.47", - "@vue/shared": "3.2.47" - } - }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -26529,15 +22521,6 @@ "minimalistic-assert": "^1.0.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", @@ -26546,19 +22529,6 @@ "node": ">= 8" } }, - "node_modules/webcrypto-core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.6.tgz", - "integrity": "sha512-TBPiewB4Buw+HI3EQW+Bexm19/W4cP/qZG/02QJCXN+iN+T5sl074vZ3rJcle/ZtDBQSgjkbsQO/1eFcxnSBUA==", - "dev": true, - "dependencies": { - "@peculiar/asn1-schema": "^2.1.6", - "@peculiar/json-schema": "^1.1.12", - "asn1js": "^3.0.1", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -27224,12 +23194,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", - "dev": true - }, "node_modules/which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", @@ -27410,12 +23374,6 @@ "node": ">= 6" } }, - "node_modules/yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", - "dev": true - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -27454,15 +23412,6 @@ "fd-slicer": "~1.1.0" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -27498,19 +23447,6 @@ "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.9.0.tgz", "integrity": "sha512-xBrvSw1htnb2VEYpgjG7etqztHSZ4KyKzJgFh/rucI7QzkbxD0rVNmIni6lia6KhWs7RBbBBLZksGKrCSaDAMQ==" }, - "node_modules/zen-observable-ts": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", - "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", - "dependencies": { - "zen-observable": "0.8.15" - } - }, - "node_modules/zen-observable-ts/node_modules/zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - }, "node_modules/zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", @@ -27695,6 +23631,7 @@ }, "playbooks": { "version": "8.0.0", + "extraneous": true, "dependencies": { "@apollo/client": "3.7.3", "@floating-ui/react-dom-interactions": "0.6.3", @@ -27778,831 +23715,6 @@ "redux-thunk": "2.4.1", "ts-prune": "0.10.3" } - }, - "playbooks/node_modules/@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", - "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" - } - }, - "playbooks/node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" - }, - "playbooks/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "playbooks/node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" - }, - "playbooks/node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "playbooks/node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "playbooks/node_modules/@floating-ui/core": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", - "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" - }, - "playbooks/node_modules/@floating-ui/dom": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", - "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", - "dependencies": { - "@floating-ui/core": "^0.7.3" - } - }, - "playbooks/node_modules/@floating-ui/react-dom": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", - "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", - "dependencies": { - "@floating-ui/dom": "^0.5.3", - "use-isomorphic-layout-effect": "^1.1.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "playbooks/node_modules/@floating-ui/react-dom-interactions": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.3.tgz", - "integrity": "sha512-xvbGEtBtA7JaEngnHQjROArv2onRp3oJIpb4+bEN5EGJf0hBYDY0vD8vFGPz/5TQwN++hb6icOB1QwdOnffMzw==", - "deprecated": "Package renamed to @floating-ui/react", - "dependencies": { - "@floating-ui/react-dom": "^0.7.1", - "aria-hidden": "^1.1.3", - "use-isomorphic-layout-effect": "^1.1.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "playbooks/node_modules/@mattermost/compass-icons": { - "version": "0.1.32", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.32.tgz", - "integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==" - }, - "playbooks/node_modules/@types/history": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", - "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", - "dev": true - }, - "playbooks/node_modules/@types/jest": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz", - "integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==", - "dev": true, - "dependencies": { - "jest-diff": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, - "playbooks/node_modules/@types/lodash": { - "version": "4.14.178", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", - "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", - "dev": true - }, - "playbooks/node_modules/@types/luxon": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.9.tgz", - "integrity": "sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA==", - "dev": true - }, - "playbooks/node_modules/@types/react-bootstrap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/react-bootstrap/-/react-bootstrap-1.0.1.tgz", - "integrity": "sha512-n68SeFrpOFWiSN2DCpyn17ZwTZzY8+mio8E9oCrl7abHytUz3r7ZYIKPxaETHYQxCi8oxF3NultpMAyBxFIjhg==", - "dev": true - }, - "playbooks/node_modules/@types/react-redux": { - "version": "7.1.21", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.21.tgz", - "integrity": "sha512-bLdglUiBSQNzWVVbmNPKGYYjrzp3/YDPwfOH3nLEz99I4awLlaRAPWjo6bZ2POpxztFWtDDXIPxBLVykXqBt+w==", - "dependencies": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "playbooks/node_modules/@types/react-select": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.1.2.tgz", - "integrity": "sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==", - "dev": true, - "dependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "@types/react-transition-group": "*" - } - }, - "playbooks/node_modules/@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, - "playbooks/node_modules/@types/styled-components": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.19.tgz", - "integrity": "sha512-hNj14Oamk7Jhb/fMMQG7TUkd3e8uMMgxsCTH+ueJNGdFo/PuhlGDQTPChqyilpZP0WttgBHkc2YyT5UG+yc6Yw==", - "dev": true, - "dependencies": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, - "playbooks/node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.1.tgz", - "integrity": "sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.57.1", - "@typescript-eslint/type-utils": "5.57.1", - "@typescript-eslint/utils": "5.57.1", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "playbooks/node_modules/@typescript-eslint/parser": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.1.tgz", - "integrity": "sha512-hlA0BLeVSA/wBPKdPGxoVr9Pp6GutGoY380FEhbVi0Ph4WNe8kLvqIRx76RSQt1lynZKfrXKs0/XeEk4zZycuA==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "5.57.1", - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/typescript-estree": "5.57.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "playbooks/node_modules/@typescript-eslint/scope-manager": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.57.1.tgz", - "integrity": "sha512-N/RrBwEUKMIYxSKl0oDK5sFVHd6VI7p9K5MyUlVYAY6dyNb/wHUqndkTd3XhpGlXgnQsBkRZuu4f9kAHghvgPw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/visitor-keys": "5.57.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "playbooks/node_modules/@typescript-eslint/type-utils": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.57.1.tgz", - "integrity": "sha512-/RIPQyx60Pt6ga86hKXesXkJ2WOS4UemFrmmq/7eOyiYjYv/MUSHPlkhU6k9T9W1ytnTJueqASW+wOmW4KrViw==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "5.57.1", - "@typescript-eslint/utils": "5.57.1", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "playbooks/node_modules/@typescript-eslint/types": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.57.1.tgz", - "integrity": "sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "playbooks/node_modules/@typescript-eslint/typescript-estree": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.1.tgz", - "integrity": "sha512-A2MZqD8gNT0qHKbk2wRspg7cHbCDCk2tcqt6ScCFLr5Ru8cn+TCfM786DjPhqwseiS+PrYwcXht5ztpEQ6TFTw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/visitor-keys": "5.57.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "playbooks/node_modules/@typescript-eslint/utils": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.57.1.tgz", - "integrity": "sha512-kN6vzzf9NkEtawECqze6v99LtmDiUJCVpvieTFA1uL7/jDghiJGubGZ5csicYHU1Xoqb3oH/R5cN5df6W41Nfg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.57.1", - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/typescript-estree": "5.57.1", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "playbooks/node_modules/@typescript-eslint/visitor-keys": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.1.tgz", - "integrity": "sha512-RjQrAniDU0CEk5r7iphkm731zKlFiUjvcBS2yHAg8WWqFMCaCrD0rKEVOMUyMMcbGPZ0bPp56srkGWrgfZqLRA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.57.1", - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "playbooks/node_modules/classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "dev": true - }, - "playbooks/node_modules/core-js": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.2.tgz", - "integrity": "sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "playbooks/node_modules/diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "playbooks/node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "playbooks/node_modules/eslint-plugin-import": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", - "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.2", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.12.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "playbooks/node_modules/eslint-plugin-import/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "playbooks/node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "playbooks/node_modules/eslint-plugin-react": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz", - "integrity": "sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "playbooks/node_modules/eslint-plugin-react-hooks": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" - } - }, - "playbooks/node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "playbooks/node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "playbooks/node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "playbooks/node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "playbooks/node_modules/jest": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz", - "integrity": "sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg==", - "dev": true, - "dependencies": { - "@jest/core": "^27.4.7", - "import-local": "^3.0.2", - "jest-cli": "^27.4.7" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "playbooks/node_modules/jest-canvas-mock": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz", - "integrity": "sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg==", - "dev": true, - "dependencies": { - "cssfontparser": "^1.2.1", - "moo-color": "^1.0.2" - } - }, - "playbooks/node_modules/jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "playbooks/node_modules/jest-junit": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-13.0.0.tgz", - "integrity": "sha512-JSHR+Dhb32FGJaiKkqsB7AR3OqWKtldLd6ZH2+FJ8D4tsweb8Id8zEVReU4+OlrRO1ZluqJLQEETm+Q6/KilBg==", - "dev": true, - "dependencies": { - "mkdirp": "^1.0.4", - "strip-ansi": "^6.0.1", - "uuid": "^8.3.2", - "xml": "^1.0.1" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "playbooks/node_modules/luxon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.3.1.tgz", - "integrity": "sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==", - "engines": { - "node": ">=12" - } - }, - "playbooks/node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, - "playbooks/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "playbooks/node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "playbooks/node_modules/qs": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.2.tgz", - "integrity": "sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "playbooks/node_modules/react-beautiful-dnd": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", - "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8.5 || ^17.0.0", - "react-dom": "^16.8.5 || ^17.0.0" - } - }, - "playbooks/node_modules/react-bootstrap": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.1.tgz", - "integrity": "sha512-ojEPQ6OtyIMdLg0Smofk+85PKN6MLKQX3bU0Vwmok/4yNa8DQ2vCGhO2IgHJvT+ERQZ4X+gAQcdn6msAHSwLBg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.14.0", - "@restart/context": "^2.1.4", - "@restart/hooks": "^0.3.26", - "@types/invariant": "^2.2.33", - "@types/prop-types": "^15.7.3", - "@types/react": ">=16.14.8", - "@types/react-transition-group": "^4.4.1", - "@types/warning": "^3.0.0", - "classnames": "^2.3.1", - "dom-helpers": "^5.2.1", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "prop-types-extra": "^1.1.0", - "react-overlays": "^5.0.1", - "react-transition-group": "^4.4.1", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "playbooks/node_modules/react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "playbooks/node_modules/react-overlays/node_modules/@restart/hooks": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", - "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", - "dev": true, - "dependencies": { - "dequal": "^2.0.2" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "playbooks/node_modules/react-redux": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", - "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", - "dependencies": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - }, - "peerDependencies": { - "react": "^16.8.3 || ^17" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, - "playbooks/node_modules/react-select": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz", - "integrity": "sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==", - "dependencies": { - "@babel/runtime": "^7.12.0", - "@emotion/cache": "^11.4.0", - "@emotion/react": "^11.1.1", - "memoize-one": "^5.0.0", - "prop-types": "^15.6.0", - "react-input-autosize": "^3.0.0", - "react-transition-group": "^4.3.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0", - "react-dom": "^16.8.0 || ^17.0.0" - } - }, - "playbooks/node_modules/redux": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", - "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, - "playbooks/node_modules/redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", - "dev": true, - "peerDependencies": { - "redux": "^4" - } - }, - "playbooks/node_modules/styled-components": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz", - "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "playbooks/node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, - "playbooks/node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, - "dependencies": { - "loose-envify": "^1.0.0" - } } }, "dependencies": { @@ -28634,174 +23746,6 @@ } } }, - "@apollo/client": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.7.3.tgz", - "integrity": "sha512-nzZ6d6a4flLpm3pZOGpuAUxLlp9heob7QcCkyIqZlCLvciUibgufRfYTwfkWCc4NaGHGSZyodzvfr79H6oUwGQ==", - "requires": { - "@graphql-typed-document-node/core": "^3.1.1", - "@wry/context": "^0.7.0", - "@wry/equality": "^0.5.0", - "@wry/trie": "^0.3.0", - "graphql-tag": "^2.12.6", - "hoist-non-react-statics": "^3.3.2", - "optimism": "^0.16.1", - "prop-types": "^15.7.2", - "response-iterator": "^0.2.6", - "symbol-observable": "^4.0.0", - "ts-invariant": "^0.10.3", - "tslib": "^2.3.0", - "zen-observable-ts": "^1.2.5" - } - }, - "@ardatan/relay-compiler": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", - "integrity": "sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==", - "dev": true, - "requires": { - "@babel/core": "^7.14.0", - "@babel/generator": "^7.14.0", - "@babel/parser": "^7.14.0", - "@babel/runtime": "^7.0.0", - "@babel/traverse": "^7.14.0", - "@babel/types": "^7.0.0", - "babel-preset-fbjs": "^3.4.0", - "chalk": "^4.0.0", - "fb-watchman": "^2.0.0", - "fbjs": "^3.0.0", - "glob": "^7.1.1", - "immutable": "~3.7.6", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "relay-runtime": "12.0.0", - "signedsource": "^1.0.0", - "yargs": "^15.3.1" - }, - "dependencies": { - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "immutable": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.7.6.tgz", - "integrity": "sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "@ardatan/sync-fetch": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz", - "integrity": "sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==", - "dev": true, - "requires": { - "node-fetch": "^2.6.1" - } - }, "@babel/code-frame": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", @@ -29420,15 +24364,6 @@ "@babel/helper-plugin-utils": "^7.8.3" } }, - "@babel/plugin-syntax-flow": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.18.6.tgz", - "integrity": "sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, "@babel/plugin-syntax-import-assertions": { "version": "7.20.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", @@ -29649,16 +24584,6 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, - "@babel/plugin-transform-flow-strip-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.21.0.tgz", - "integrity": "sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-flow": "^7.18.6" - } - }, "@babel/plugin-transform-for-of": { "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.5.tgz", @@ -30158,27 +25083,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } - } - }, "@deanwhillier/jest-matchmedia-mock": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@deanwhillier/jest-matchmedia-mock/-/jest-matchmedia-mock-1.2.0.tgz", @@ -30191,53 +25095,6 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, - "@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", - "requires": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.1.3" - }, - "dependencies": { - "babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "requires": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - } - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" - } - } - }, "@emotion/cache": { "version": "10.0.29", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", @@ -30374,82 +25231,11 @@ } } }, - "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" - }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "requires": { - "@emotion/memoize": "0.7.4" - }, - "dependencies": { - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - } - } - }, "@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" }, - "@emotion/react": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.6.tgz", - "integrity": "sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==", - "requires": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.6", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "hoist-non-react-statics": "^3.3.1" - }, - "dependencies": { - "@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", - "requires": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" - } - }, - "@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" - }, - "@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" - } - } - }, - "@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", - "requires": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", - "csstype": "^3.0.2" - } - }, "@emotion/sheet": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", @@ -30460,16 +25246,6 @@ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" }, - "@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" - }, - "@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==" - }, "@emotion/utils": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", @@ -30510,90 +25286,6 @@ "aria-hidden": "^1.1.3" } }, - "@formatjs/cli": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-4.7.0.tgz", - "integrity": "sha512-XYy8rDLW64G4191rXdPjhxW8QrSc3vG3IfV6wfxJfWj5bwNEnsC1YixV4hnuiFs1OMT5I9MF9xQ0V7dauRqyUQ==", - "dev": true, - "requires": { - "@formatjs/icu-messageformat-parser": "2.0.16", - "@formatjs/ts-transformer": "3.8.1", - "@types/estree": "^0.0.50", - "@types/fs-extra": "^9.0.1", - "@types/json-stable-stringify": "^1.0.32", - "@types/node": "14", - "@vue/compiler-core": "^3.2.23", - "chalk": "^4.0.0", - "commander": "8", - "fast-glob": "^3.2.7", - "fs-extra": "10", - "json-stable-stringify": "^1.0.1", - "loud-rejection": "^2.2.0", - "tslib": "^2.1.0", - "typescript": "^4.5", - "vue": "^3.2.23" - }, - "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.1.tgz", - "integrity": "sha512-tgtNODZUGuUI6PAcnvaLZpGrZLVkXnnAvgzOiueYMzFdOdcOw4iH1WKhCe3+r6VR8rHKToJ2HksUGNCB+zt/bg==", - "dev": true, - "requires": { - "@formatjs/intl-localematcher": "0.2.22", - "tslib": "^2.1.0" - } - }, - "@formatjs/icu-messageformat-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.16.tgz", - "integrity": "sha512-sYg0ImXsAqBbjU/LotoCD9yKC5nUpWVy3s4DwWerHXD4sm62FcjMF8mekwudRk3eZLHqSO+M21MpFUUjDQ+Q5Q==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.11.1", - "@formatjs/icu-skeleton-parser": "1.3.3", - "tslib": "^2.1.0" - } - }, - "@formatjs/icu-skeleton-parser": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.3.tgz", - "integrity": "sha512-ifWnzjmHPHUF89UpCvClTP66sXYFc8W/qg7Qt+qtTUB9BqRWlFeUsevAzaMYDJsRiOy4S2WJFrJoZgRKUFfPGQ==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.11.1", - "tslib": "^2.1.0" - } - }, - "@formatjs/intl-localematcher": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.22.tgz", - "integrity": "sha512-z+TvbHW8Q/g2l7/PnfUl0mV9gWxV4d0HT6GQyzkO5QI6QjCvCZGiztnmLX7zoyS16uSMvZ2PoMDfSK9xvZkRRA==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, - "@types/estree": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz", - "integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==", - "dev": true - }, - "@types/node": { - "version": "14.18.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.37.tgz", - "integrity": "sha512-7GgtHCs/QZrBrDzgIJnQtuSvhFSwhyYSI2uafSwZoNt1iOGhEN5fwNrQMjtONyHm9+/LoA4453jH0CMYcr06Pg==", - "dev": true - }, - "commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "dev": true - } - } - }, "@formatjs/ecma402-abstract": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz", @@ -30672,822 +25364,6 @@ "tslib": "^2.4.0" } }, - "@formatjs/ts-transformer": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.8.1.tgz", - "integrity": "sha512-emndJkdURyan9i9KkZN1Oa+xWG0y5Y17PLFz76rE21mO4OOx02nEJ9wX12PWfxdlmzt9sjN0O7WlRUw10HUaFA==", - "dev": true, - "requires": { - "@formatjs/icu-messageformat-parser": "2.0.16", - "@types/node": "14 || 16", - "chalk": "^4.0.0", - "tslib": "^2.1.0", - "typescript": "^4.5" - }, - "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.1.tgz", - "integrity": "sha512-tgtNODZUGuUI6PAcnvaLZpGrZLVkXnnAvgzOiueYMzFdOdcOw4iH1WKhCe3+r6VR8rHKToJ2HksUGNCB+zt/bg==", - "dev": true, - "requires": { - "@formatjs/intl-localematcher": "0.2.22", - "tslib": "^2.1.0" - } - }, - "@formatjs/icu-messageformat-parser": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.16.tgz", - "integrity": "sha512-sYg0ImXsAqBbjU/LotoCD9yKC5nUpWVy3s4DwWerHXD4sm62FcjMF8mekwudRk3eZLHqSO+M21MpFUUjDQ+Q5Q==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.11.1", - "@formatjs/icu-skeleton-parser": "1.3.3", - "tslib": "^2.1.0" - } - }, - "@formatjs/icu-skeleton-parser": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.3.tgz", - "integrity": "sha512-ifWnzjmHPHUF89UpCvClTP66sXYFc8W/qg7Qt+qtTUB9BqRWlFeUsevAzaMYDJsRiOy4S2WJFrJoZgRKUFfPGQ==", - "dev": true, - "requires": { - "@formatjs/ecma402-abstract": "1.11.1", - "tslib": "^2.1.0" - } - }, - "@formatjs/intl-localematcher": { - "version": "0.2.22", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.22.tgz", - "integrity": "sha512-z+TvbHW8Q/g2l7/PnfUl0mV9gWxV4d0HT6GQyzkO5QI6QjCvCZGiztnmLX7zoyS16uSMvZ2PoMDfSK9xvZkRRA==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - } - } - }, - "@graphql-codegen/add": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-3.2.3.tgz", - "integrity": "sha512-sQOnWpMko4JLeykwyjFTxnhqjd/3NOG2OyMuvK76Wnnwh8DRrNf2VEs2kmSvLl7MndMlOj7Kh5U154dVcvhmKQ==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.1", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/cli": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-2.16.3.tgz", - "integrity": "sha512-dyRt4nvbpLmWSq+fNsYhQo5tDJyFdlEIX+detR6biOur+kjI9e8djMVa5XSojoDkRIQCifu++6nUHxeROXN8iw==", - "dev": true, - "requires": { - "@babel/generator": "^7.18.13", - "@babel/template": "^7.18.10", - "@babel/types": "^7.18.13", - "@graphql-codegen/core": "2.6.8", - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/apollo-engine-loader": "^7.3.6", - "@graphql-tools/code-file-loader": "^7.3.13", - "@graphql-tools/git-loader": "^7.2.13", - "@graphql-tools/github-loader": "^7.3.20", - "@graphql-tools/graphql-file-loader": "^7.5.0", - "@graphql-tools/json-file-loader": "^7.4.1", - "@graphql-tools/load": "7.8.0", - "@graphql-tools/prisma-loader": "^7.2.49", - "@graphql-tools/url-loader": "^7.13.2", - "@graphql-tools/utils": "^9.0.0", - "@whatwg-node/fetch": "^0.5.0", - "chalk": "^4.1.0", - "chokidar": "^3.5.2", - "cosmiconfig": "^7.0.0", - "cosmiconfig-typescript-loader": "4.3.0", - "debounce": "^1.2.0", - "detect-indent": "^6.0.0", - "graphql-config": "4.3.6", - "inquirer": "^8.0.0", - "is-glob": "^4.0.1", - "json-to-pretty-yaml": "^1.2.2", - "listr2": "^4.0.5", - "log-symbols": "^4.0.0", - "shell-quote": "^1.7.3", - "string-env-interpolation": "^1.0.1", - "ts-log": "^2.2.3", - "tslib": "^2.4.0", - "yaml": "^1.10.0", - "yargs": "^17.0.0" - }, - "dependencies": { - "cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - } - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true - }, - "yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, - "requires": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - } - }, - "yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true - } - } - }, - "@graphql-codegen/client-preset": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@graphql-codegen/client-preset/-/client-preset-1.2.5.tgz", - "integrity": "sha512-gACm+XkH9aukgYLURWlryWcG0bdXfxf/tiywpJWCy3QvNWky3zyvJoWK3Dh1UBryXFY5ktN2PSvNFbXwpyuz8g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/template": "^7.15.4", - "@graphql-codegen/add": "^3.2.3", - "@graphql-codegen/gql-tag-operations": "1.6.0", - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/typed-document-node": "^2.3.12", - "@graphql-codegen/typescript": "^2.8.7", - "@graphql-codegen/typescript-operations": "^2.5.12", - "@graphql-codegen/visitor-plugin-common": "^2.13.7", - "@graphql-tools/utils": "^9.0.0", - "@graphql-typed-document-node/core": "3.1.1", - "tslib": "~2.4.0" - }, - "dependencies": { - "@graphql-typed-document-node/core": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "dev": true - } - } - }, - "@graphql-codegen/core": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-2.6.8.tgz", - "integrity": "sha512-JKllNIipPrheRgl+/Hm/xuWMw9++xNQ12XJR/OHHgFopOg4zmN3TdlRSyYcv/K90hCFkkIwhlHFUQTfKrm8rxQ==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.1", - "@graphql-tools/schema": "^9.0.0", - "@graphql-tools/utils": "^9.1.1", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/gql-tag-operations": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@graphql-codegen/gql-tag-operations/-/gql-tag-operations-1.6.0.tgz", - "integrity": "sha512-wllGNBrYWuxA/E6NK0SQLWSbFyVv7zb6TIUGdjViohCgd1z5Bpn9Ohm1YBJXofxweAnyLyB5KCSPBwYkh439Zw==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/visitor-plugin-common": "2.13.7", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "tslib": "~2.4.0" - }, - "dependencies": { - "@graphql-codegen/visitor-plugin-common": { - "version": "2.13.7", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.7.tgz", - "integrity": "sha512-xE6iLDhr9sFM1qwCGJcCXRu5MyA0moapG2HVejwyAXXLubYKYwWnoiEigLH2b5iauh6xsl6XP8hh9D1T1dn5Cw==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - } - } - } - }, - "@graphql-codegen/plugin-helpers": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-3.1.2.tgz", - "integrity": "sha512-emOQiHyIliVOIjKVKdsI5MXj312zmRDwmHpyUTZMjfpvxq/UVAHUJIVdVf+lnjjrI+LXBTgMlTWTgHQfmICxjg==", - "dev": true, - "requires": { - "@graphql-tools/utils": "^9.0.0", - "change-case-all": "1.0.15", - "common-tags": "1.8.2", - "import-from": "4.0.0", - "lodash": "~4.17.0", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/schema-ast": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@graphql-codegen/schema-ast/-/schema-ast-2.6.1.tgz", - "integrity": "sha512-5TNW3b1IHJjCh07D2yQNGDQzUpUl2AD+GVe1Dzjqyx/d2Fn0TPMxLsHsKPS4Plg4saO8FK/QO70wLsP7fdbQ1w==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/utils": "^9.0.0", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/typed-document-node": { - "version": "2.3.13", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typed-document-node/-/typed-document-node-2.3.13.tgz", - "integrity": "sha512-vt1hvBAbYTYUCXblks9KYwR5Ho16hWQljid5xgx77jeVufj5PjnWrOjJfEFKFx17VOM4CKHP8ryoeT4NyjYNWw==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/typescript": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-2.8.8.tgz", - "integrity": "sha512-A0oUi3Oy6+DormOlrTC4orxT9OBZkIglhbJBcDmk34jAKKUgesukXRd4yOhmTrnbchpXz2T8IAOFB3FWIaK4Rw==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/schema-ast": "^2.6.1", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/typescript-operations": { - "version": "2.5.13", - "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-2.5.13.tgz", - "integrity": "sha512-3vfR6Rx6iZU0JRt29GBkFlrSNTM6t+MSLF86ChvL4d/Jfo/JYAGuB3zNzPhirHYzJPCvLOAx2gy9ID1ltrpYiw==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-codegen/typescript": "^2.8.8", - "@graphql-codegen/visitor-plugin-common": "2.13.8", - "auto-bind": "~4.0.0", - "tslib": "~2.4.0" - } - }, - "@graphql-codegen/visitor-plugin-common": { - "version": "2.13.8", - "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-2.13.8.tgz", - "integrity": "sha512-IQWu99YV4wt8hGxIbBQPtqRuaWZhkQRG2IZKbMoSvh0vGeWb3dB0n0hSgKaOOxDY+tljtOf9MTcUYvJslQucMQ==", - "dev": true, - "requires": { - "@graphql-codegen/plugin-helpers": "^3.1.2", - "@graphql-tools/optimize": "^1.3.0", - "@graphql-tools/relay-operation-optimizer": "^6.5.0", - "@graphql-tools/utils": "^9.0.0", - "auto-bind": "~4.0.0", - "change-case-all": "1.0.15", - "dependency-graph": "^0.11.0", - "graphql-tag": "^2.11.0", - "parse-filepath": "^1.0.2", - "tslib": "~2.4.0" - } - }, - "@graphql-tools/apollo-engine-loader": { - "version": "7.3.26", - "resolved": "https://registry.npmjs.org/@graphql-tools/apollo-engine-loader/-/apollo-engine-loader-7.3.26.tgz", - "integrity": "sha512-h1vfhdJFjnCYn9b5EY1Z91JTF0KB3hHVJNQIsiUV2mpQXZdeOXQoaWeYEKaiI5R6kwBw5PP9B0fv3jfUIG8LyQ==", - "dev": true, - "requires": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/utils": "^9.2.1", - "@whatwg-node/fetch": "^0.8.0", - "tslib": "^2.4.0" - }, - "dependencies": { - "@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "requires": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - } - } - }, - "@graphql-tools/batch-execute": { - "version": "8.5.18", - "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.18.tgz", - "integrity": "sha512-mNv5bpZMLLwhkmPA6+RP81A6u3KF4CSKLf3VX9hbomOkQR4db8pNs8BOvpZU54wKsUzMzdlws/2g/Dabyb2Vsg==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "dataloader": "2.2.2", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - } - }, - "@graphql-tools/code-file-loader": { - "version": "7.3.21", - "resolved": "https://registry.npmjs.org/@graphql-tools/code-file-loader/-/code-file-loader-7.3.21.tgz", - "integrity": "sha512-dj+OLnz1b8SYkXcuiy0CUQ25DWnOEyandDlOcdBqU3WVwh5EEVbn0oXUYm90fDlq2/uut00OrtC5Wpyhi3tAvA==", - "dev": true, - "requires": { - "@graphql-tools/graphql-tag-pluck": "7.5.0", - "@graphql-tools/utils": "9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - } - }, - "@graphql-tools/delegate": { - "version": "9.0.28", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-9.0.28.tgz", - "integrity": "sha512-8j23JCs2mgXqnp+5K0v4J3QBQU/5sXd9miaLvMfRf/6963DznOXTECyS9Gcvj1VEeR5CXIw6+aX/BvRDKDdN1g==", - "dev": true, - "requires": { - "@graphql-tools/batch-execute": "^8.5.18", - "@graphql-tools/executor": "^0.0.15", - "@graphql-tools/schema": "^9.0.16", - "@graphql-tools/utils": "^9.2.1", - "dataloader": "^2.2.2", - "tslib": "^2.5.0", - "value-or-promise": "^1.0.12" - }, - "dependencies": { - "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==", - "dev": true - } - } - }, - "@graphql-tools/executor": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.15.tgz", - "integrity": "sha512-6U7QLZT8cEUxAMXDP4xXVplLi6RBwx7ih7TevlBto66A/qFp3PDb6o/VFo07yBKozr8PGMZ4jMfEWBGxmbGdxA==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "@graphql-typed-document-node/core": "3.1.2", - "@repeaterjs/repeater": "3.0.4", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - } - }, - "@graphql-tools/executor-graphql-ws": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.12.tgz", - "integrity": "sha512-aFD79i9l282Ob5dOZ7JsyhhXXP1o8eQh0prYkSSVo/OU2ndzWigfANz4DJgWgS3LwBjLDlMcmaXPZZeXt3m4Tg==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "@repeaterjs/repeater": "3.0.4", - "@types/ws": "^8.0.0", - "graphql-ws": "5.12.0", - "isomorphic-ws": "5.0.0", - "tslib": "^2.4.0", - "ws": "8.12.1" - }, - "dependencies": { - "ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true - } - } - }, - "@graphql-tools/executor-http": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-0.1.9.tgz", - "integrity": "sha512-tNzMt5qc1ptlHKfpSv9wVBVKCZ7gks6Yb/JcYJluxZIT4qRV+TtOFjpptfBU63usgrGVOVcGjzWc/mt7KhmmpQ==", - "dev": true, - "requires": { - "@graphql-tools/utils": "^9.2.1", - "@repeaterjs/repeater": "^3.0.4", - "@whatwg-node/fetch": "^0.8.1", - "dset": "^3.1.2", - "extract-files": "^11.0.0", - "meros": "^1.2.1", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.12" - }, - "dependencies": { - "@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "requires": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - } - } - }, - "@graphql-tools/executor-legacy-ws": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.9.tgz", - "integrity": "sha512-L7oDv7R5yoXzMH+KLKDB2WHVijfVW4dB2H+Ae1RdW3MFvwbYjhnIB6QzHqKEqksjp/FndtxZkbuTIuAOsYGTYw==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "@types/ws": "^8.0.0", - "isomorphic-ws": "5.0.0", - "tslib": "^2.4.0", - "ws": "8.12.1" - }, - "dependencies": { - "ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true - } - } - }, - "@graphql-tools/git-loader": { - "version": "7.2.20", - "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.2.20.tgz", - "integrity": "sha512-D/3uwTzlXxG50HI8BEixqirT4xiUp6AesTdfotRXAs2d4CT9wC6yuIWOHkSBqgI1cwKWZb6KXZr467YPS5ob1w==", - "dev": true, - "requires": { - "@graphql-tools/graphql-tag-pluck": "7.5.0", - "@graphql-tools/utils": "9.2.1", - "is-glob": "4.0.3", - "micromatch": "^4.0.4", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - } - }, - "@graphql-tools/github-loader": { - "version": "7.3.27", - "resolved": "https://registry.npmjs.org/@graphql-tools/github-loader/-/github-loader-7.3.27.tgz", - "integrity": "sha512-fFFC35qenyhjb8pfcYXKknAt0CXP5CkQYtLfJXgTXSgBjIsfAVMrqxQ/Y0ejeM19XNF/C3VWJ7rE308yOX6ywA==", - "dev": true, - "requires": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/graphql-tag-pluck": "^7.4.6", - "@graphql-tools/utils": "^9.2.1", - "@whatwg-node/fetch": "^0.8.0", - "tslib": "^2.4.0" - }, - "dependencies": { - "@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "requires": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - } - } - }, - "@graphql-tools/graphql-file-loader": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.16.tgz", - "integrity": "sha512-lK1N3Y2I634FS12nd4bu7oAJbai3bUc28yeX+boT+C83KTO4ujGHm+6hPC8X/FRGwhKOnZBxUM7I5nvb3HiUxw==", - "dev": true, - "requires": { - "@graphql-tools/import": "6.7.17", - "@graphql-tools/utils": "9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - } - }, - "@graphql-tools/graphql-tag-pluck": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-tag-pluck/-/graphql-tag-pluck-7.5.0.tgz", - "integrity": "sha512-76SYzhSlH50ZWkhWH6OI94qrxa8Ww1ZeOU04MdtpSeQZVT2rjGWeTb3xM3kjTVWQJsr/YJBhDeNPGlwNUWfX4Q==", - "dev": true, - "requires": { - "@babel/parser": "^7.16.8", - "@babel/plugin-syntax-import-assertions": "7.20.0", - "@babel/traverse": "^7.16.8", - "@babel/types": "^7.16.8", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0" - } - }, - "@graphql-tools/import": { - "version": "6.7.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.7.17.tgz", - "integrity": "sha512-bn9SgrECXq3WIasgNP7ful/uON51wBajPXtxdY+z/ce7jLWaFE6lzwTDB/GAgiZ+jo7nb0ravlxteSAz2qZmuA==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "resolve-from": "5.0.0", - "tslib": "^2.4.0" - }, - "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } - } - }, - "@graphql-tools/json-file-loader": { - "version": "7.4.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.4.17.tgz", - "integrity": "sha512-KOSTP43nwjPfXgas90rLHAFgbcSep4nmiYyR9xRVz4ZAmw8VYHcKhOLTSGylCAzi7KUfyBXajoW+6Z7dQwdn3g==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "globby": "^11.0.3", - "tslib": "^2.4.0", - "unixify": "^1.0.0" - } - }, - "@graphql-tools/load": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-7.8.0.tgz", - "integrity": "sha512-l4FGgqMW0VOqo+NMYizwV8Zh+KtvVqOf93uaLo9wJ3sS3y/egPCgxPMDJJ/ufQZG3oZ/0oWeKt68qop3jY0yZg==", - "dev": true, - "requires": { - "@graphql-tools/schema": "9.0.4", - "@graphql-tools/utils": "8.12.0", - "p-limit": "3.1.0", - "tslib": "^2.4.0" - }, - "dependencies": { - "@graphql-tools/merge": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.6.tgz", - "integrity": "sha512-uUBokxXi89bj08P+iCvQk3Vew4vcfL5ZM6NTylWi8PIpoq4r5nJ625bRuN8h2uubEdRiH8ntN9M4xkd/j7AybQ==", - "dev": true, - "requires": { - "@graphql-tools/utils": "8.12.0", - "tslib": "^2.4.0" - } - }, - "@graphql-tools/schema": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.4.tgz", - "integrity": "sha512-B/b8ukjs18fq+/s7p97P8L1VMrwapYc3N2KvdG/uNThSazRRn8GsBK0Nr+FH+mVKiUfb4Dno79e3SumZVoHuOQ==", - "dev": true, - "requires": { - "@graphql-tools/merge": "8.3.6", - "@graphql-tools/utils": "8.12.0", - "tslib": "^2.4.0", - "value-or-promise": "1.0.11" - } - }, - "@graphql-tools/utils": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.12.0.tgz", - "integrity": "sha512-TeO+MJWGXjUTS52qfK4R8HiPoF/R7X+qmgtOYd8DTH0l6b+5Y/tlg5aGeUJefqImRq7nvi93Ms40k/Uz4D5CWw==", - "dev": true, - "requires": { - "tslib": "^2.4.0" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "value-or-promise": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", - "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", - "dev": true - } - } - }, - "@graphql-tools/merge": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.0.tgz", - "integrity": "sha512-3XYCWe0d3I4F1azNj1CdShlbHfTIfiDgj00R9uvFH8tHKh7i1IWN3F7QQYovcHKhayaR6zPok3YYMESYQcBoaA==", - "dev": true, - "requires": { - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0" - } - }, - "@graphql-tools/optimize": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/optimize/-/optimize-1.3.1.tgz", - "integrity": "sha512-5j5CZSRGWVobt4bgRRg7zhjPiSimk+/zIuColih8E8DxuFOaJ+t0qu7eZS5KXWBkjcd4BPNuhUPpNlEmHPqVRQ==", - "dev": true, - "requires": { - "tslib": "^2.4.0" - } - }, - "@graphql-tools/prisma-loader": { - "version": "7.2.65", - "resolved": "https://registry.npmjs.org/@graphql-tools/prisma-loader/-/prisma-loader-7.2.65.tgz", - "integrity": "sha512-MuX4XrJEuOE6qYbjR9WhSInPX8iji07wA6K72hVgkvG2UBXBUAcVC/1H6iOFbs1d7rLLU2r6aWsYnD6zwzw+eA==", - "dev": true, - "requires": { - "@graphql-tools/url-loader": "7.17.14", - "@graphql-tools/utils": "9.2.1", - "@types/js-yaml": "^4.0.0", - "@types/json-stable-stringify": "^1.0.32", - "chalk": "^4.1.0", - "debug": "^4.3.1", - "dotenv": "^16.0.0", - "graphql-request": "^5.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "isomorphic-fetch": "^3.0.0", - "jose": "^4.11.4", - "js-yaml": "^4.0.0", - "json-stable-stringify": "^1.0.1", - "lodash": "^4.17.20", - "scuid": "^1.1.0", - "tslib": "^2.4.0", - "yaml-ast-parser": "^0.0.43" - }, - "dependencies": { - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "dev": true - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - } - } - }, - "@graphql-tools/relay-operation-optimizer": { - "version": "6.5.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/relay-operation-optimizer/-/relay-operation-optimizer-6.5.17.tgz", - "integrity": "sha512-hHPEX6ccRF3+9kfVz0A3In//Dej7QrHOLGZEokBmPDMDqn9CS7qUjpjyGzclbOX0tRBtLfuFUZ68ABSac3P1nA==", - "dev": true, - "requires": { - "@ardatan/relay-compiler": "12.0.0", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0" - } - }, - "@graphql-tools/schema": { - "version": "9.0.17", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.17.tgz", - "integrity": "sha512-HVLq0ecbkuXhJlpZ50IHP5nlISqH2GbNgjBJhhRzHeXhfwlUOT4ISXGquWTmuq61K0xSaO0aCjMpxe4QYbKTng==", - "dev": true, - "requires": { - "@graphql-tools/merge": "8.4.0", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - } - }, - "@graphql-tools/url-loader": { - "version": "7.17.14", - "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.17.14.tgz", - "integrity": "sha512-7boEmrZlbViqQSSvu2VFCGi9YAY7E0BCVObiv1sLYbFR+62mo825As0haU5l7wlx1zCDyUlOleNz+X2jVvBbSQ==", - "dev": true, - "requires": { - "@ardatan/sync-fetch": "^0.0.1", - "@graphql-tools/delegate": "^9.0.27", - "@graphql-tools/executor-graphql-ws": "^0.0.12", - "@graphql-tools/executor-http": "^0.1.7", - "@graphql-tools/executor-legacy-ws": "^0.0.9", - "@graphql-tools/utils": "^9.2.1", - "@graphql-tools/wrap": "^9.3.8", - "@types/ws": "^8.0.0", - "@whatwg-node/fetch": "^0.8.0", - "isomorphic-ws": "^5.0.0", - "tslib": "^2.4.0", - "value-or-promise": "^1.0.11", - "ws": "^8.12.0" - }, - "dependencies": { - "@whatwg-node/fetch": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.2.tgz", - "integrity": "sha512-6u1xGzFZvskJpQXhWreR9s1/4nsuY4iFRsTb4BC3NiDHmzgj/Hu1Ovt4iHs5KAjLzbnsjaQOI5f5bQPucqvPsQ==", - "dev": true, - "requires": { - "@peculiar/webcrypto": "^1.4.0", - "@whatwg-node/node-fetch": "^0.3.1", - "busboy": "^1.6.0", - "urlpattern-polyfill": "^6.0.2", - "web-streams-polyfill": "^3.2.1" - } - }, - "ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", - "dev": true - } - } - }, - "@graphql-tools/utils": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", - "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", - "dev": true, - "requires": { - "@graphql-typed-document-node/core": "^3.1.1", - "tslib": "^2.4.0" - } - }, - "@graphql-tools/wrap": { - "version": "9.3.8", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.3.8.tgz", - "integrity": "sha512-MGsExYPiILMw4Qff7HcvE9MMSYdjb/tr5IQYJbxJIU4/TrBHox1/smne8HG+Bd7kmDlTTj7nU/Z8sxmoRd0hOQ==", - "dev": true, - "requires": { - "@graphql-tools/delegate": "9.0.28", - "@graphql-tools/schema": "9.0.17", - "@graphql-tools/utils": "9.2.1", - "tslib": "^2.4.0", - "value-or-promise": "1.0.12" - } - }, - "@graphql-typed-document-node/core": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.2.tgz", - "integrity": "sha512-9anpBMM9mEgZN4wr2v8wHJI2/u5TnnggewRN6OlvXTTnuVyoY19X6rOv9XTqKRw6dcGKwZsBi8n0kDE2I5i4VA==" - }, "@guyplusplus/turndown-plugin-gfm": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@guyplusplus/turndown-plugin-gfm/-/turndown-plugin-gfm-1.0.7.tgz", @@ -31519,12 +25395,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@iarna/toml": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", - "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", - "dev": true - }, "@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -32348,16 +26218,6 @@ "@mattermost/types": { "version": "file:platform/types" }, - "@mdi/js": { - "version": "6.9.96", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.9.96.tgz", - "integrity": "sha512-rK0/vLFaiItYS2W7uVmaKPKnhNQE4XVkylpk5njtVwENnp8elwY5uRL6qvdj2esuvUHG7DwygE4Qu3eKxxuJiQ==" - }, - "@mdi/react": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.5.0.tgz", - "integrity": "sha512-NztRgUxSYD+ImaKN94Tg66VVVqXj4SmlDGzZoz48H9riJ+Awha56sfXH2fegw819NWo7KI3oeS1Es0lNQqwr0w==" - }, "@mdn/browser-compat-data": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz", @@ -32549,39 +26409,6 @@ "fastq": "^1.6.0" } }, - "@peculiar/asn1-schema": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz", - "integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==", - "dev": true, - "requires": { - "asn1js": "^3.0.5", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, - "@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", - "dev": true, - "requires": { - "tslib": "^2.0.0" - } - }, - "@peculiar/webcrypto": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", - "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", - "dev": true, - "requires": { - "@peculiar/asn1-schema": "^2.3.0", - "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.1", - "webcrypto-core": "^1.7.4" - } - }, "@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -32603,27 +26430,6 @@ "immutable": "^4.0.0" } }, - "@repeaterjs/repeater": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", - "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", - "dev": true - }, - "@restart/context": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", - "integrity": "sha512-INJYZQJP7g+IoDUh/475NlGiTeMfwTXUEr3tmRneckHIxNolGOW9CTq83S8cxq0CgJwwcMzMJFchxvlwe7Rk8Q==", - "dev": true - }, - "@restart/hooks": { - "version": "0.3.27", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.27.tgz", - "integrity": "sha512-s984xV/EapUIfkjlf8wz9weP2O9TNKR96C68FfMEy2bE69+H4cNv3RD4Mf97lW7Htt7PjZrYTjSC8f3SB9VCXw==", - "dev": true, - "requires": { - "dequal": "^2.0.2" - } - }, "@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -32826,16 +26632,6 @@ "@types/react-dom": "*" } }, - "@testing-library/react-hooks": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-8.0.0.tgz", - "integrity": "sha512-uZqcgtcUUtw7Z9N32W13qQhVAD+Xki2hxbTR461MKax8T6Jr8nsUvZB+vcBTkzY2nFvsUet434CsgF0ncW2yFw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - } - }, "@tippyjs/react": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", @@ -32857,42 +26653,6 @@ "dev": true, "optional": true }, - "@ts-morph/common": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.12.3.tgz", - "integrity": "sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==", - "dev": true, - "requires": { - "fast-glob": "^3.2.7", - "minimatch": "^3.0.4", - "mkdirp": "^1.0.4", - "path-browserify": "^1.0.1" - } - }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, "@types/aria-query": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", @@ -33017,12 +26777,6 @@ "integrity": "sha512-aCE6SP9WQJkq9kNFDRdxpUfWhazvreNSPtm1BQG1B4/xSpKbRwfweVAChYx6MMLyTguCKJExGZ1C52Z8DlFSGg==", "dev": true }, - "@types/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", - "dev": true - }, "@types/enzyme": { "version": "3.10.11", "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.11.tgz", @@ -33083,15 +26837,6 @@ "@types/send": "*" } }, - "@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -33141,12 +26886,6 @@ "@types/node": "*" } }, - "@types/invariant": { - "version": "2.2.35", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==", - "dev": true - }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -33226,17 +26965,6 @@ "@types/sizzle": "*" } }, - "@types/js-cookie": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", - "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==" - }, - "@types/js-yaml": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", - "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==", - "dev": true - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -33422,15 +27150,6 @@ "@types/react": "*" } }, - "@types/react-infinite-scroller": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@types/react-infinite-scroller/-/react-infinite-scroller-1.2.3.tgz", - "integrity": "sha512-l60JckVoO+dxmKW2eEG7jbliEpITsTJvRPTe97GazjF5+ylagAuyYdXl8YY9DQsTP9QjhqGKZROknzgscGJy0A==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, "@types/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.2.tgz", @@ -33481,26 +27200,6 @@ "@types/react-router": "*" } }, - "@types/react-router-hash-link": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@types/react-router-hash-link/-/react-router-hash-link-2.4.5.tgz", - "integrity": "sha512-YsiD8xCWtRBebzPqG6kXjDQCI35LCN9MhV/MbgYF8y0trOp7VSUNmSj8HdIGyH99WCfSOLZB2pIwUMN/IwIDQg==", - "dev": true, - "requires": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-dom": "*" - } - }, - "@types/react-test-renderer": { - "version": "17.0.1", - "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz", - "integrity": "sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, "@types/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -33675,12 +27374,6 @@ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", "dev": true }, - "@types/warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz", - "integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==", - "dev": true - }, "@types/webpack-env": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.1.tgz", @@ -33750,139 +27443,6 @@ } } }, - "@vue/compiler-core": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz", - "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==", - "dev": true, - "requires": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.47", - "estree-walker": "^2.0.2", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@vue/compiler-dom": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz", - "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==", - "dev": true, - "requires": { - "@vue/compiler-core": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "@vue/compiler-sfc": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz", - "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==", - "dev": true, - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.47", - "@vue/compiler-dom": "3.2.47", - "@vue/compiler-ssr": "3.2.47", - "@vue/reactivity-transform": "3.2.47", - "@vue/shared": "3.2.47", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", - "postcss": "^8.1.10", - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@vue/compiler-ssr": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz", - "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==", - "dev": true, - "requires": { - "@vue/compiler-dom": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "@vue/reactivity": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz", - "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==", - "dev": true, - "requires": { - "@vue/shared": "3.2.47" - } - }, - "@vue/reactivity-transform": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz", - "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==", - "dev": true, - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.47", - "@vue/shared": "3.2.47", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" - } - }, - "@vue/runtime-core": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz", - "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==", - "dev": true, - "requires": { - "@vue/reactivity": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "@vue/runtime-dom": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz", - "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==", - "dev": true, - "requires": { - "@vue/runtime-core": "3.2.47", - "@vue/shared": "3.2.47", - "csstype": "^2.6.8" - }, - "dependencies": { - "csstype": { - "version": "2.6.21", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", - "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==", - "dev": true - } - } - }, - "@vue/server-renderer": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz", - "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==", - "dev": true, - "requires": { - "@vue/compiler-ssr": "3.2.47", - "@vue/shared": "3.2.47" - } - }, - "@vue/shared": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz", - "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==", - "dev": true - }, "@webassemblyjs/ast": { "version": "1.11.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", @@ -34053,70 +27613,6 @@ "integrity": "sha512-9AZHVXWlpN8Cn9k5BC/O0Dzb9E9xfEMXzYrNunwvkUTvuK7xgQPVRZpLo+jWCOZ5r8oBa8NIrHuPEu1hzbb6bg==", "dev": true }, - "@whatwg-node/events": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz", - "integrity": "sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==", - "dev": true - }, - "@whatwg-node/fetch": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.5.4.tgz", - "integrity": "sha512-dR5PCzvOeS7OaW6dpIlPt+Ou3pak7IEG+ZVAV26ltcaiDB3+IpuvjqRdhsY6FKHcqBo1qD+S99WXY9Z6+9Rwnw==", - "dev": true, - "requires": { - "@peculiar/webcrypto": "^1.4.0", - "abort-controller": "^3.0.0", - "busboy": "^1.6.0", - "form-data-encoder": "^1.7.1", - "formdata-node": "^4.3.1", - "node-fetch": "^2.6.7", - "undici": "^5.12.0", - "web-streams-polyfill": "^3.2.0" - } - }, - "@whatwg-node/node-fetch": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.3.1.tgz", - "integrity": "sha512-/U4onp5eBkRHfFe/VL+ppyupqj7z6iBtjcuPSosQNH2/y+LxRn5lyFb7Vqhb5DokjrDMjssLcqiVYnx+UABFsw==", - "dev": true, - "requires": { - "@whatwg-node/events": "^0.0.2", - "busboy": "^1.6.0", - "fast-querystring": "^1.1.1", - "fast-url-parser": "^1.1.3", - "tslib": "^2.3.1" - } - }, - "@wry/context": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.0.tgz", - "integrity": "sha512-LcDAiYWRtwAoSOArfk7cuYvFXytxfVrdX7yxoUmK7pPITLk5jYh2F8knCwS7LjgYL8u1eidPlKKV6Ikqq0ODqQ==", - "requires": { - "tslib": "^2.3.0" - } - }, - "@wry/equality": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.3.tgz", - "integrity": "sha512-avR+UXdSrsF2v8vIqIgmeTY0UR91UT+IyablCyKe/uk22uOJ8fusKZnH9JH9e1/EtLeNJBtagNmL3eJdnOV53g==", - "requires": { - "tslib": "^2.3.0" - } - }, - "@wry/trie": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.3.2.tgz", - "integrity": "sha512-yRTyhWSls2OY/pYLfwff867r8ekooZ4UI+/gxot5Wj8EFwSf2rG+n+Mo/6LoLQm1TKA4GRj2+LCpbfS937dClQ==", - "requires": { - "tslib": "^2.3.0" - } - }, - "@xobotyi/scrollbar-width": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", - "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" - }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -34135,15 +27631,6 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, - "requires": { - "event-target-shim": "^5.0.0" - } - }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -34202,16 +27689,6 @@ "debug": "4" } }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, "airbnb-prop-types": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", @@ -34356,12 +27833,6 @@ } } }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -34394,12 +27865,6 @@ "integrity": "sha512-kO/vVCacW9mnpn3WPWbTVlEnOabK2L7LWi2HViURtCM46y1zb6I8UMjx4LgbiqadTgHnLInUronwn3ampNTJtQ==", "dev": true }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", - "dev": true - }, "array-flatten": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", @@ -34480,12 +27945,6 @@ "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true - }, "asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -34504,17 +27963,6 @@ } } }, - "asn1js": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", - "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", - "dev": true, - "requires": { - "pvtsutils": "^1.3.2", - "pvutils": "^1.1.3", - "tslib": "^2.4.0" - } - }, "ast-types-flow": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", @@ -34538,12 +27986,6 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, - "auto-bind": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-4.0.0.tgz", - "integrity": "sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==", - "dev": true - }, "autoprefixer": { "version": "9.8.8", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", @@ -34687,12 +28129,6 @@ } } }, - "babel-plugin-add-react-displayname": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/babel-plugin-add-react-displayname/-/babel-plugin-add-react-displayname-0.0.5.tgz", - "integrity": "sha512-LY3+Y0XVDYcShHHorshrDbt4KFWL4bSeniCtl4SYZbask+Syngk1uMPCeN9+nSiZo6zX5s0RTq/J9Pnaaf/KHw==", - "dev": true - }, "babel-plugin-formatjs": { "version": "10.5.1", "resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.5.1.tgz", @@ -34863,12 +28299,6 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" }, - "babel-plugin-syntax-trailing-function-commas": { - "version": "7.0.0-beta.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz", - "integrity": "sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==", - "dev": true - }, "babel-plugin-typescript-to-proptypes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-typescript-to-proptypes/-/babel-plugin-typescript-to-proptypes-2.1.0.tgz", @@ -34900,41 +28330,6 @@ "@babel/plugin-syntax-top-level-await": "^7.8.3" } }, - "babel-preset-fbjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.4.0.tgz", - "integrity": "sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==", - "dev": true, - "requires": { - "@babel/plugin-proposal-class-properties": "^7.0.0", - "@babel/plugin-proposal-object-rest-spread": "^7.0.0", - "@babel/plugin-syntax-class-properties": "^7.0.0", - "@babel/plugin-syntax-flow": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "@babel/plugin-transform-arrow-functions": "^7.0.0", - "@babel/plugin-transform-block-scoped-functions": "^7.0.0", - "@babel/plugin-transform-block-scoping": "^7.0.0", - "@babel/plugin-transform-classes": "^7.0.0", - "@babel/plugin-transform-computed-properties": "^7.0.0", - "@babel/plugin-transform-destructuring": "^7.0.0", - "@babel/plugin-transform-flow-strip-types": "^7.0.0", - "@babel/plugin-transform-for-of": "^7.0.0", - "@babel/plugin-transform-function-name": "^7.0.0", - "@babel/plugin-transform-literals": "^7.0.0", - "@babel/plugin-transform-member-expression-literals": "^7.0.0", - "@babel/plugin-transform-modules-commonjs": "^7.0.0", - "@babel/plugin-transform-object-super": "^7.0.0", - "@babel/plugin-transform-parameters": "^7.0.0", - "@babel/plugin-transform-property-literals": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0", - "@babel/plugin-transform-spread": "^7.0.0", - "@babel/plugin-transform-template-literals": "^7.0.0", - "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" - } - }, "babel-preset-jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", @@ -35745,15 +29140,6 @@ } } }, - "busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "requires": { - "streamsearch": "^1.1.0" - } - }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -35796,6 +29182,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -35844,17 +29231,6 @@ "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", "dev": true }, - "capital-case": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", - "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, "caw": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", @@ -35895,44 +29271,6 @@ } } }, - "change-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", - "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, - "requires": { - "camel-case": "^4.1.2", - "capital-case": "^1.0.4", - "constant-case": "^3.0.4", - "dot-case": "^3.0.4", - "header-case": "^2.0.4", - "no-case": "^3.0.4", - "param-case": "^3.0.4", - "pascal-case": "^3.1.2", - "path-case": "^3.0.4", - "sentence-case": "^3.0.4", - "snake-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "change-case-all": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/change-case-all/-/change-case-all-1.0.15.tgz", - "integrity": "sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==", - "dev": true, - "requires": { - "change-case": "^4.1.2", - "is-lower-case": "^2.0.2", - "is-upper-case": "^2.0.2", - "lower-case": "^2.0.2", - "lower-case-first": "^2.0.2", - "sponge-case": "^1.0.1", - "swap-case": "^2.0.2", - "title-case": "^3.0.3", - "upper-case": "^2.0.2", - "upper-case-first": "^2.0.2" - } - }, "char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -35957,22 +29295,11 @@ "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "dev": true }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, "chart.js": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.8.2.tgz", "integrity": "sha512-7rqSlHWMUKFyBDOJvmFGW2lxULtcwaPLegDjX/Nu5j6QybY+GCiQkEY+6cqHw62S5tcwXMD8Y+H5OBGoR7d+ZQ==" }, - "chartjs-plugin-annotation": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-2.1.2.tgz", - "integrity": "sha512-kmEp2WtpogwnKKnDPO3iO3mVwvVGtmG5BkZVtAEZm5YzJ9CYxojjYEgk7OTrFbJ5vU098b84UeJRe8kRfNcq5g==" - }, "cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -36024,14 +29351,6 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, - "chrono-node": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-2.3.5.tgz", - "integrity": "sha512-QIWEgXYVn55/Nsgdqbe6inqW+GoK3B6Qtga8AWdpq+nd+mOZVMxa+SGwPq/XjY+nKN+toQGu8KifCPwUkmz2sg==", - "requires": { - "dayjs": "^1.10.0" - } - }, "ci-info": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.2.tgz", @@ -36058,56 +29377,6 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-spinners": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz", - "integrity": "sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==", - "dev": true - }, - "cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "requires": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "dependencies": { - "slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - } - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -36119,12 +29388,6 @@ "wrap-ansi": "^7.0.0" } }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true - }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -36166,12 +29429,6 @@ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true }, - "code-block-writer": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", - "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", - "dev": true - }, "collect-v8-coverage": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", @@ -36222,12 +29479,6 @@ "delayed-stream": "~1.0.0" } }, - "common-tags": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", - "dev": true - }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -36384,17 +29635,6 @@ "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true }, - "constant-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", - "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case": "^2.0.2" - } - }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -36427,14 +29667,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, - "copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "requires": { - "toggle-selection": "^1.0.6" - } - }, "copy-webpack-plugin": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", @@ -36550,21 +29782,6 @@ "yaml": "^1.7.2" } }, - "cosmiconfig-toml-loader": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-toml-loader/-/cosmiconfig-toml-loader-1.0.0.tgz", - "integrity": "sha512-H/2gurFWVi7xXvCyvsWRLCMekl4tITJcX0QEsDMpzxtuxDyM59xLatYNg4s/k9AA/HdtCYfj2su8mgA0GSDLDA==", - "dev": true, - "requires": { - "@iarna/toml": "^2.2.5" - } - }, - "cosmiconfig-typescript-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz", - "integrity": "sha512-NTxV1MFfZDLPiBMjxbHRwSh5LaLcPMwNdCutmnHJCKoVnlvldPWlllonKwrsRJ5pYZBIBGRWWU2tfvzxgeSW5Q==", - "dev": true - }, "country-list": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/country-list/-/country-list-2.2.0.tgz", @@ -36611,12 +29828,6 @@ "sha.js": "^2.4.8" } }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -36626,15 +29837,6 @@ "cross-spawn": "^7.0.1" } }, - "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", - "dev": true, - "requires": { - "node-fetch": "2.6.7" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -36719,14 +29921,6 @@ "integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==", "dev": true }, - "css-in-js-utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", - "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", - "requires": { - "hyphenate-style-name": "^1.0.3" - } - }, "css-loader": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", @@ -36770,6 +29964,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "optional": true, "requires": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -36778,7 +29974,9 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true } } }, @@ -36853,15 +30051,6 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, "cwebp-bin": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cwebp-bin/-/cwebp-bin-7.0.1.tgz", @@ -36890,27 +30079,11 @@ "whatwg-url": "^8.0.0" } }, - "dataloader": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", - "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", - "dev": true - }, "date-fns": { "version": "2.29.3", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" }, - "dayjs": { - "version": "1.11.7", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" - }, - "debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -37205,15 +30378,6 @@ } } }, - "defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "requires": { - "clone": "^1.0.2" - } - }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -37241,18 +30405,6 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, - "dependency-graph": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", - "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", - "dev": true - }, - "dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true - }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -37286,12 +30438,6 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, "diff-sequences": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", @@ -37515,12 +30661,6 @@ } } }, - "dset": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", - "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==", - "dev": true - }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -37768,14 +30908,6 @@ "is-arrayish": "^0.2.1" } }, - "error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "requires": { - "stackframe": "^1.3.4" - } - }, "es-abstract": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", @@ -38154,27 +31286,6 @@ } } }, - "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, "eslint-import-resolver-webpack": { "version": "0.13.2", "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.2.tgz", @@ -38288,12 +31399,6 @@ "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", "dev": true }, - "eslint-plugin-import-newlines": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.3.0.tgz", - "integrity": "sha512-8rokf6NvxC10ugA1VNmzEIO75CzId7IDF3Ai2GNXl0Xr4VORpb8u+bxsjRuE+2BS8MfDbrK/MHUQZI2G9qQyyA==", - "dev": true - }, "eslint-plugin-jsx-a11y": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz", @@ -38342,33 +31447,12 @@ "jsx-ast-utils": "^3.3.3" } }, - "eslint-plugin-no-relative-import-paths": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-no-relative-import-paths/-/eslint-plugin-no-relative-import-paths-1.5.0.tgz", - "integrity": "sha512-4if/PGGzpSP+DfOscGZ2nYd8XObc5rN9Ux5lhDzAVqLEaz8XD3ikzApSogXBdf+lnpTsTE5LJSqXFmFpNkZ3LQ==", - "dev": true - }, "eslint-plugin-react-hooks": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "dev": true }, - "eslint-plugin-unused-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", - "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", - "dev": true, - "requires": { - "eslint-rule-composer": "^0.3.0" - } - }, - "eslint-rule-composer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", - "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", - "dev": true - }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -38441,12 +31525,6 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true - }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -38805,17 +31883,6 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, "external-remotes-plugin": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/external-remotes-plugin/-/external-remotes-plugin-1.0.0.tgz", @@ -38825,18 +31892,6 @@ "webpack-sources": "^2.2.0" } }, - "extract-files": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", - "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", - "dev": true - }, - "fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -38866,42 +31921,6 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, - "fast-loops": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", - "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==" - }, - "fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", - "dev": true, - "requires": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "fast-shallow-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", - "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" - }, - "fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "dev": true, - "requires": { - "punycode": "^1.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - } - } - }, "fast-xml-parser": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", @@ -38918,11 +31937,6 @@ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, - "fastest-stable-stringify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", - "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==" - }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -38950,35 +31964,6 @@ "bser": "2.1.1" } }, - "fbjs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.4.tgz", - "integrity": "sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ==", - "dev": true, - "requires": { - "cross-fetch": "^3.1.5", - "fbjs-css-vars": "^1.0.0", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.30" - }, - "dependencies": { - "ua-parser-js": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.34.tgz", - "integrity": "sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==", - "dev": true - } - } - }, - "fbjs-css-vars": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", - "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", - "dev": true - }, "fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -38989,15 +31974,6 @@ "pend": "~1.2.0" } }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -39191,30 +32167,6 @@ "mime-types": "^2.1.12" } }, - "form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "dev": true - }, - "formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "dev": true, - "requires": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "dependencies": { - "web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "dev": true - } - } - }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -39287,25 +32239,6 @@ "dev": true, "optional": true }, - "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, "fs-monkey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", @@ -39369,6 +32302,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -39655,110 +32589,6 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "graphql": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", - "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==" - }, - "graphql-config": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-4.3.6.tgz", - "integrity": "sha512-i7mAPwc0LAZPnYu2bI8B6yXU5820Wy/ArvmOseDLZIu0OU1UTULEuexHo6ZcHXeT9NvGGaUPQZm8NV3z79YydA==", - "dev": true, - "requires": { - "@graphql-tools/graphql-file-loader": "^7.3.7", - "@graphql-tools/json-file-loader": "^7.3.7", - "@graphql-tools/load": "^7.5.5", - "@graphql-tools/merge": "^8.2.6", - "@graphql-tools/url-loader": "^7.9.7", - "@graphql-tools/utils": "^8.6.5", - "cosmiconfig": "7.0.1", - "cosmiconfig-toml-loader": "1.0.0", - "cosmiconfig-typescript-loader": "^4.0.0", - "minimatch": "4.2.1", - "string-env-interpolation": "1.0.1", - "ts-node": "^10.8.1", - "tslib": "^2.4.0" - }, - "dependencies": { - "@graphql-tools/utils": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.13.1.tgz", - "integrity": "sha512-qIh9yYpdUFmctVqovwMdheVNJqFh+DQNWIhX87FJStfXYnmweBUDATok9fWPleKeFwxnW8IapKmY8m8toJEkAw==", - "dev": true, - "requires": { - "tslib": "^2.4.0" - } - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "graphql-request": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-5.2.0.tgz", - "integrity": "sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ==", - "dev": true, - "requires": { - "@graphql-typed-document-node/core": "^3.1.1", - "cross-fetch": "^3.1.5", - "extract-files": "^9.0.0", - "form-data": "^3.0.0" - }, - "dependencies": { - "extract-files": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz", - "integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==", - "dev": true - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, - "graphql-tag": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", - "requires": { - "tslib": "^2.1.0" - } - }, - "graphql-ws": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.0.tgz", - "integrity": "sha512-PA3ImUp8utrpEjoxBMhvxsjkStvFEdU0E1gEBREt8HZIWkxOUymwJBhFnBL7t/iHhUq1GVPeZevPinkZFENxTw==", - "dev": true - }, "gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -39824,7 +32654,8 @@ "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true }, "has-to-string-tag-x": { "version": "1.4.1", @@ -39870,16 +32701,6 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, - "header-case": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", - "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, - "requires": { - "capital-case": "^1.0.4", - "tslib": "^2.0.3" - } - }, "helpertypes": { "version": "0.0.18", "resolved": "https://registry.npmjs.org/helpertypes/-/helpertypes-0.0.18.tgz", @@ -40217,11 +33038,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "hyphenate-style-name": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", - "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -40560,12 +33376,6 @@ "resolve-from": "^4.0.0" } }, - "import-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", - "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", - "dev": true - }, "import-lazy": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", @@ -40616,43 +33426,11 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "inline-style-prefixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", - "integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==", - "requires": { - "css-in-js-utils": "^3.1.0", - "fast-loops": "^1.1.3" - } - }, "inobounce": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/inobounce/-/inobounce-0.2.1.tgz", "integrity": "sha512-dmKhRDbUS3zGD8HDGchsZBuxaXnfFM+2jXrZpnEnBToEWCgcs3lBfCQe0wzkbpIoJwU/lufaMquSyWoX8OXTRw==" }, - "inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.1", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.21", - "mute-stream": "0.0.8", - "ora": "^5.4.1", - "run-async": "^2.4.0", - "rxjs": "^7.5.5", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - } - }, "internal-slot": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", @@ -40706,16 +33484,6 @@ "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", "dev": true }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, "is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -40902,12 +33670,6 @@ "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "dev": true }, - "is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true - }, "is-jpg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", @@ -40915,15 +33677,6 @@ "dev": true, "optional": true }, - "is-lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-lower-case/-/is-lower-case-2.0.2.tgz", - "integrity": "sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -41030,15 +33783,6 @@ "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", "dev": true }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, "is-retry-allowed": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", @@ -41121,30 +33865,12 @@ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "dev": true }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, - "is-upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-upper-case/-/is-upper-case-2.0.2.tgz", - "integrity": "sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "is-weakmap": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", @@ -41170,12 +33896,6 @@ "get-intrinsic": "^1.1.1" } }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -41219,12 +33939,6 @@ "whatwg-fetch": "^3.4.1" } }, - "isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "dev": true - }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -41431,26 +34145,6 @@ } } }, - "jest-cli": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", - "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", - "dev": true, - "requires": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" - } - }, "jest-config": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", @@ -42092,23 +34786,12 @@ "regenerator-runtime": "^0.13.3" } }, - "jose": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.13.1.tgz", - "integrity": "sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==", - "dev": true - }, "jpeg-js": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "dev": true }, - "js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" - }, "js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -42120,11 +34803,6 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "js-trim-multiline-string": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/js-trim-multiline-string/-/js-trim-multiline-string-1.0.8.tgz", - "integrity": "sha512-EFAZ/l2Pgt0hVA+tPgt8y2tpB1KiTJdkWMj9N4OrA9olkrJ01KthyX5Z/ieT2xT8tqPxu3PnwuoCky4mDwMmwg==" - }, "js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", @@ -42245,40 +34923,12 @@ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", "dev": true }, - "json-to-pretty-yaml": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/json-to-pretty-yaml/-/json-to-pretty-yaml-1.2.2.tgz", - "integrity": "sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==", - "dev": true, - "requires": { - "remedial": "^1.0.7", - "remove-trailing-spaces": "^1.0.6" - } - }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - }, - "dependencies": { - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - } - } - }, "jsonify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", @@ -42414,22 +35064,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "listr2": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz", - "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==", - "dev": true, - "requires": { - "cli-truncate": "^2.1.0", - "colorette": "^2.0.16", - "log-update": "^4.0.0", - "p-map": "^4.0.0", - "rfdc": "^1.3.0", - "rxjs": "^7.5.5", - "through": "^2.3.8", - "wrap-ansi": "^7.0.0" - } - }, "load-bmfont": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.1.tgz", @@ -42614,31 +35248,6 @@ "is-unicode-supported": "^0.1.0" } }, - "log-update": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", - "dev": true, - "requires": { - "ansi-escapes": "^4.3.0", - "cli-cursor": "^3.1.0", - "slice-ansi": "^4.0.0", - "wrap-ansi": "^6.2.0" - }, - "dependencies": { - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - } - } - }, "longest-streak": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", @@ -42653,16 +35262,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "loud-rejection": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-2.2.0.tgz", - "integrity": "sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.2" - } - }, "lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -42672,15 +35271,6 @@ "tslib": "^2.0.3" } }, - "lower-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case-first/-/lower-case-first-2.0.2.tgz", - "integrity": "sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -42716,12 +35306,6 @@ "sourcemap-codec": "^1.4.8" } }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -42731,12 +35315,6 @@ "tmpl": "1.0.5" } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true - }, "map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -43386,7 +35964,9 @@ "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true, + "optional": true }, "media-typer": { "version": "0.3.0", @@ -43593,12 +36173,6 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, - "meros": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.1.tgz", - "integrity": "sha512-R2f/jxYqCAGI19KhAvaxSOxALBMkaXWH2a7rOyqQw+ZmizX5bKkEYWLzdhC+U82ZVVPVp6MCXe3EkVligh+12g==", - "dev": true - }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -43935,27 +36509,6 @@ "thunky": "^1.0.2" } }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, - "nano-css": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.3.5.tgz", - "integrity": "sha512-vSB9X12bbNu4ALBu7nigJgRViZ6ja3OU7CeuiV1zMIbXOdmkLahgtPmh3GBOlDxbKY0CitqlPdOReGlBLSp+yg==", - "requires": { - "css-tree": "^1.1.2", - "csstype": "^3.0.6", - "fastest-stable-stringify": "^2.0.2", - "inline-style-prefixer": "^6.0.0", - "rtl-css-js": "^1.14.0", - "sourcemap-codec": "^1.4.8", - "stacktrace-js": "^2.0.2", - "stylis": "^4.0.6" - } - }, "nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", @@ -44035,12 +36588,6 @@ "propagate": "^2.0.0" } }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "dev": true - }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -44231,12 +36778,6 @@ "boolbase": "^1.0.0" } }, - "nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true - }, "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", @@ -44257,7 +36798,8 @@ "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true }, "object-is": { "version": "1.1.5", @@ -44398,15 +36940,6 @@ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", "dev": true }, - "optimism": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.2.tgz", - "integrity": "sha512-zWNbgWj+3vLEjZNIh/okkY2EUfX+vB9TJopzIZwT1xxaMqC5hRLLraePod4c5n4He08xuXNH+zhKFFCu390wiQ==", - "requires": { - "@wry/context": "^0.7.0", - "@wry/trie": "^0.3.0" - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -44432,46 +36965,6 @@ "bin-wrapper": "^4.0.0" } }, - "ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "requires": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "dependencies": { - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - } - } - }, "os-filter-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", @@ -44482,12 +36975,6 @@ "arch": "^2.1.0" } }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true - }, "ow": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/ow/-/ow-0.17.0.tgz", @@ -44538,15 +37025,6 @@ "dev": true, "optional": true }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, "p-map-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", @@ -44672,11 +37150,6 @@ "xml2js": "^0.4.5" } }, - "parse-duration": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.0.2.tgz", - "integrity": "sha512-Dg27N6mfok+ow1a2rj/nRjtCfaKrHUZV2SJpEn/s8GaVUSlf4GGRCRP1c13Hj+wfPKVMrFDqLMLITkYKgKxyyg==" - }, "parse-entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", @@ -44691,17 +37164,6 @@ "is-hexadecimal": "^1.0.0" } }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, "parse-headers": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.5.tgz", @@ -44754,22 +37216,6 @@ "tslib": "^2.0.3" } }, - "path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "path-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", - "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -44787,21 +37233,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", - "dev": true - }, "path-to-regexp": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", @@ -44971,694 +37402,6 @@ } } }, - "playbooks": { - "version": "file:playbooks", - "requires": { - "@apollo/client": "3.7.3", - "@floating-ui/react-dom-interactions": "0.6.3", - "@formatjs/cli": "4.7.0", - "@graphql-codegen/cli": "2.16.3", - "@graphql-codegen/client-preset": "1.2.5", - "@mattermost/client": "*", - "@mattermost/compass-icons": "0.1.32", - "@mattermost/types": "*", - "@mdi/js": "^6.5.95", - "@mdi/react": "1.5.0", - "@testing-library/react-hooks": "8.0.0", - "@tippyjs/react": "4.2.6", - "@types/debounce": "1.2.1", - "@types/history": "4.7.8", - "@types/jest": "27.4.0", - "@types/lodash": "4.14.178", - "@types/luxon": "2.0.9", - "@types/qs": "6.9.7", - "@types/react": "^17.0.2", - "@types/react-beautiful-dnd": "13.1.2", - "@types/react-bootstrap": "1.0.1", - "@types/react-custom-scrollbars": "4.0.10", - "@types/react-dom": "^17.0.2", - "@types/react-infinite-scroller": "1.2.3", - "@types/react-redux": "7.1.21", - "@types/react-router-dom": "5.3.3", - "@types/react-router-hash-link": "2.4.5", - "@types/react-select": "3.1.2", - "@types/react-test-renderer": "17.0.1", - "@types/redux-mock-store": "1.0.3", - "@types/shallow-equals": "1.0.0", - "@types/styled-components": "5.1.19", - "@typescript-eslint/eslint-plugin": "5.57.1", - "@typescript-eslint/parser": "5.57.1", - "babel-plugin-add-react-displayname": "0.0.5", - "chart.js": "3.8.2", - "chartjs-plugin-annotation": "2.1.2", - "chrono-node": "2.3.5", - "classnames": "2.3.1", - "core-js": "3.20.2", - "debounce": "1.2.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-import-newlines": "1.3.0", - "eslint-plugin-no-relative-import-paths": "1.5.0", - "eslint-plugin-react": "7.28.0", - "eslint-plugin-react-hooks": "4.3.0", - "eslint-plugin-unused-imports": "2.0.0", - "graphql": "16.3.0", - "identity-obj-proxy": "3.0.0", - "jest": "27.4.7", - "jest-canvas-mock": "2.3.1", - "jest-junit": "13.0.0", - "js-trim-multiline-string": "^1.0.8", - "lodash": "4.17.21", - "luxon": "2.3.1", - "mattermost-webapp": "*", - "parse-duration": "1.0.2", - "qs": "6.10.2", - "react": "^17.0.2", - "react-beautiful-dnd": "13.1.0", - "react-bootstrap": "1.6.1", - "react-chartjs-2": "4.3.1", - "react-dom": "^17.0.2", - "react-infinite-scroll-component": "^6.1.0", - "react-infinite-scroller": "1.2.6", - "react-intl": "*", - "react-redux": "7.2.6", - "react-router-dom": "5.3.4", - "react-router-hash-link": "2.4.3", - "react-select": "4.3.1", - "react-test-renderer": "17.0.2", - "react-use": "17.3.2", - "redux": "4.1.2", - "redux-mock-store": "1.5.4", - "redux-thunk": "2.4.1", - "reselect": "4.1.8", - "styled-components": "5.3.3", - "ts-prune": "0.10.3", - "typescript": "4.7.4" - }, - "dependencies": { - "@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", - "requires": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" - } - }, - "@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" - }, - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" - }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", - "dev": true - }, - "@floating-ui/core": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", - "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" - }, - "@floating-ui/dom": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", - "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", - "requires": { - "@floating-ui/core": "^0.7.3" - } - }, - "@floating-ui/react-dom": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", - "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", - "requires": { - "@floating-ui/dom": "^0.5.3", - "use-isomorphic-layout-effect": "^1.1.1" - } - }, - "@floating-ui/react-dom-interactions": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.6.3.tgz", - "integrity": "sha512-xvbGEtBtA7JaEngnHQjROArv2onRp3oJIpb4+bEN5EGJf0hBYDY0vD8vFGPz/5TQwN++hb6icOB1QwdOnffMzw==", - "requires": { - "@floating-ui/react-dom": "^0.7.1", - "aria-hidden": "^1.1.3", - "use-isomorphic-layout-effect": "^1.1.1" - } - }, - "@mattermost/compass-icons": { - "version": "0.1.32", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.32.tgz", - "integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==" - }, - "@types/history": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz", - "integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==", - "dev": true - }, - "@types/jest": { - "version": "27.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz", - "integrity": "sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==", - "dev": true, - "requires": { - "jest-diff": "^27.0.0", - "pretty-format": "^27.0.0" - } - }, - "@types/lodash": { - "version": "4.14.178", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", - "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", - "dev": true - }, - "@types/luxon": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.9.tgz", - "integrity": "sha512-ZuzIc7aN+i2ZDMWIiSmMdubR9EMMSTdEzF6R+FckP4p6xdnOYKqknTo/k+xXQvciSXlNGIwA4OPU5X7JIFzYdA==", - "dev": true - }, - "@types/react-bootstrap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/react-bootstrap/-/react-bootstrap-1.0.1.tgz", - "integrity": "sha512-n68SeFrpOFWiSN2DCpyn17ZwTZzY8+mio8E9oCrl7abHytUz3r7ZYIKPxaETHYQxCi8oxF3NultpMAyBxFIjhg==", - "dev": true - }, - "@types/react-redux": { - "version": "7.1.21", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.21.tgz", - "integrity": "sha512-bLdglUiBSQNzWVVbmNPKGYYjrzp3/YDPwfOH3nLEz99I4awLlaRAPWjo6bZ2POpxztFWtDDXIPxBLVykXqBt+w==", - "requires": { - "@types/hoist-non-react-statics": "^3.3.0", - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0", - "redux": "^4.0.0" - } - }, - "@types/react-select": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@types/react-select/-/react-select-3.1.2.tgz", - "integrity": "sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==", - "dev": true, - "requires": { - "@types/react": "*", - "@types/react-dom": "*", - "@types/react-transition-group": "*" - } - }, - "@types/semver": { - "version": "7.3.13", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", - "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", - "dev": true - }, - "@types/styled-components": { - "version": "5.1.19", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.19.tgz", - "integrity": "sha512-hNj14Oamk7Jhb/fMMQG7TUkd3e8uMMgxsCTH+ueJNGdFo/PuhlGDQTPChqyilpZP0WttgBHkc2YyT5UG+yc6Yw==", - "dev": true, - "requires": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, - "@typescript-eslint/eslint-plugin": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.57.1.tgz", - "integrity": "sha512-1MeobQkQ9tztuleT3v72XmY0XuKXVXusAhryoLuU5YZ+mXoYKZP9SQ7Flulh1NX4DTjpGTc2b/eMu4u7M7dhnQ==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.57.1", - "@typescript-eslint/type-utils": "5.57.1", - "@typescript-eslint/utils": "5.57.1", - "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/parser": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.57.1.tgz", - "integrity": "sha512-hlA0BLeVSA/wBPKdPGxoVr9Pp6GutGoY380FEhbVi0Ph4WNe8kLvqIRx76RSQt1lynZKfrXKs0/XeEk4zZycuA==", - "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "5.57.1", - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/typescript-estree": "5.57.1", - "debug": "^4.3.4" - } - }, - "@typescript-eslint/scope-manager": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.57.1.tgz", - "integrity": "sha512-N/RrBwEUKMIYxSKl0oDK5sFVHd6VI7p9K5MyUlVYAY6dyNb/wHUqndkTd3XhpGlXgnQsBkRZuu4f9kAHghvgPw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/visitor-keys": "5.57.1" - } - }, - "@typescript-eslint/type-utils": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.57.1.tgz", - "integrity": "sha512-/RIPQyx60Pt6ga86hKXesXkJ2WOS4UemFrmmq/7eOyiYjYv/MUSHPlkhU6k9T9W1ytnTJueqASW+wOmW4KrViw==", - "dev": true, - "requires": { - "@typescript-eslint/typescript-estree": "5.57.1", - "@typescript-eslint/utils": "5.57.1", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/types": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.57.1.tgz", - "integrity": "sha512-bSs4LOgyV3bJ08F5RDqO2KXqg3WAdwHCu06zOqcQ6vqbTJizyBhuh1o1ImC69X4bV2g1OJxbH71PJqiO7Y1RuA==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.1.tgz", - "integrity": "sha512-A2MZqD8gNT0qHKbk2wRspg7cHbCDCk2tcqt6ScCFLr5Ru8cn+TCfM786DjPhqwseiS+PrYwcXht5ztpEQ6TFTw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/visitor-keys": "5.57.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, - "@typescript-eslint/utils": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.57.1.tgz", - "integrity": "sha512-kN6vzzf9NkEtawECqze6v99LtmDiUJCVpvieTFA1uL7/jDghiJGubGZ5csicYHU1Xoqb3oH/R5cN5df6W41Nfg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.57.1", - "@typescript-eslint/types": "5.57.1", - "@typescript-eslint/typescript-estree": "5.57.1", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "5.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.1.tgz", - "integrity": "sha512-RjQrAniDU0CEk5r7iphkm731zKlFiUjvcBS2yHAg8WWqFMCaCrD0rKEVOMUyMMcbGPZ0bPp56srkGWrgfZqLRA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.57.1", - "eslint-visitor-keys": "^3.3.0" - } - }, - "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "dev": true - }, - "core-js": { - "version": "3.20.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.2.tgz", - "integrity": "sha512-nuqhq11DcOAbFBV4zCbKeGbKQsUDRqTX0oqx7AttUBuqe3h20ixsE039QHelbL6P4h+9kytVqyEtyZ6gsiwEYw==" - }, - "diff-sequences": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", - "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", - "dev": true - }, - "dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "eslint-plugin-import": { - "version": "2.25.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", - "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flat": "^1.2.5", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.7.2", - "has": "^1.0.3", - "is-core-module": "^2.8.0", - "is-glob": "^4.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.5", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.12.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - } - } - }, - "eslint-plugin-react": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz", - "integrity": "sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw==", - "dev": true, - "requires": { - "array-includes": "^3.1.4", - "array.prototype.flatmap": "^1.2.5", - "doctrine": "^2.1.0", - "estraverse": "^5.3.0", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.5", - "object.fromentries": "^2.0.5", - "object.hasown": "^1.1.0", - "object.values": "^1.1.5", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "semver": "^6.3.0", - "string.prototype.matchall": "^4.0.6" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "resolve": { - "version": "2.0.0-next.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", - "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-plugin-react-hooks": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz", - "integrity": "sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true - }, - "jest": { - "version": "27.4.7", - "resolved": "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz", - "integrity": "sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg==", - "dev": true, - "requires": { - "@jest/core": "^27.4.7", - "import-local": "^3.0.2", - "jest-cli": "^27.4.7" - } - }, - "jest-canvas-mock": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz", - "integrity": "sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg==", - "dev": true, - "requires": { - "cssfontparser": "^1.2.1", - "moo-color": "^1.0.2" - } - }, - "jest-diff": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", - "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", - "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^27.5.1", - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" - } - }, - "jest-junit": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-13.0.0.tgz", - "integrity": "sha512-JSHR+Dhb32FGJaiKkqsB7AR3OqWKtldLd6ZH2+FJ8D4tsweb8Id8zEVReU4+OlrRO1ZluqJLQEETm+Q6/KilBg==", - "dev": true, - "requires": { - "mkdirp": "^1.0.4", - "strip-ansi": "^6.0.1", - "uuid": "^8.3.2", - "xml": "^1.0.1" - } - }, - "luxon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.3.1.tgz", - "integrity": "sha512-I8vnjOmhXsMSlNMZlMkSOvgrxKJl0uOsEzdGgGNZuZPaS9KlefpE9KV95QFftlJSC+1UyCC9/I69R02cz/zcCA==" - }, - "memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, - "qs": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.2.tgz", - "integrity": "sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==", - "requires": { - "side-channel": "^1.0.4" - } - }, - "react-beautiful-dnd": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", - "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.9.2", - "css-box-model": "^1.2.0", - "memoize-one": "^5.1.1", - "raf-schd": "^4.0.2", - "react-redux": "^7.2.0", - "redux": "^4.0.4", - "use-memo-one": "^1.1.1" - } - }, - "react-bootstrap": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-1.6.1.tgz", - "integrity": "sha512-ojEPQ6OtyIMdLg0Smofk+85PKN6MLKQX3bU0Vwmok/4yNa8DQ2vCGhO2IgHJvT+ERQZ4X+gAQcdn6msAHSwLBg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.14.0", - "@restart/context": "^2.1.4", - "@restart/hooks": "^0.3.26", - "@types/invariant": "^2.2.33", - "@types/prop-types": "^15.7.3", - "@types/react": ">=16.14.8", - "@types/react-transition-group": "^4.4.1", - "@types/warning": "^3.0.0", - "classnames": "^2.3.1", - "dom-helpers": "^5.2.1", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "prop-types-extra": "^1.1.0", - "react-overlays": "^5.0.1", - "react-transition-group": "^4.4.1", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - } - }, - "react-overlays": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", - "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.8", - "@popperjs/core": "^2.11.6", - "@restart/hooks": "^0.4.7", - "@types/warning": "^3.0.0", - "dom-helpers": "^5.2.0", - "prop-types": "^15.7.2", - "uncontrollable": "^7.2.1", - "warning": "^4.0.3" - }, - "dependencies": { - "@restart/hooks": { - "version": "0.4.9", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz", - "integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==", - "dev": true, - "requires": { - "dequal": "^2.0.2" - } - } - } - }, - "react-redux": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz", - "integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==", - "requires": { - "@babel/runtime": "^7.15.4", - "@types/react-redux": "^7.1.20", - "hoist-non-react-statics": "^3.3.2", - "loose-envify": "^1.4.0", - "prop-types": "^15.7.2", - "react-is": "^17.0.2" - } - }, - "react-select": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-4.3.1.tgz", - "integrity": "sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==", - "requires": { - "@babel/runtime": "^7.12.0", - "@emotion/cache": "^11.4.0", - "@emotion/react": "^11.1.1", - "memoize-one": "^5.0.0", - "prop-types": "^15.6.0", - "react-input-autosize": "^3.0.0", - "react-transition-group": "^4.3.0" - } - }, - "redux": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", - "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "requires": { - "@babel/runtime": "^7.9.2" - } - }, - "redux-thunk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", - "dev": true - }, - "styled-components": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.3.tgz", - "integrity": "sha512-++4iHwBM7ZN+x6DtPPWkCI4vdtwumQ+inA/DdAsqYd4SVgUKJie5vXyzotA00ttcFdQkCng7zc6grwlfIfw+lw==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^0.8.8", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - } - }, - "uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - } - }, - "warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - } - } - }, "pngjs": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", @@ -46126,15 +37869,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "promise": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", - "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", - "dev": true, - "requires": { - "asap": "~2.0.3" - } - }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -46296,21 +38030,6 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "pvtsutils": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", - "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", - "dev": true, - "requires": { - "tslib": "^2.4.0" - } - }, - "pvutils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", - "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", - "dev": true - }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -46484,11 +38203,6 @@ } } }, - "react-chartjs-2": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz", - "integrity": "sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==" - }, "react-color": { "version": "2.19.3", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz", @@ -46537,15 +38251,6 @@ "scheduler": "^0.20.2" } }, - "react-error-boundary": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", - "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.12.5" - } - }, "react-fast-compare": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", @@ -46573,30 +38278,6 @@ } } }, - "react-infinite-scroll-component": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", - "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", - "requires": { - "throttle-debounce": "^2.1.0" - } - }, - "react-infinite-scroller": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz", - "integrity": "sha512-mGdMyOD00YArJ1S1F3TVU9y4fGSfVVl6p5gh/Vt4u99CJOptfVu/q5V/Wlle72TMgYlBwIhbxK5wF0C/R33PXQ==", - "requires": { - "prop-types": "^15.5.8" - } - }, - "react-input-autosize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", - "integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==", - "requires": { - "prop-types": "^15.5.8" - } - }, "react-intl": { "version": "6.3.2", "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.3.2.tgz", @@ -46744,14 +38425,6 @@ "prop-types": "^15.6.0" } }, - "react-router-hash-link": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/react-router-hash-link/-/react-router-hash-link-2.4.3.tgz", - "integrity": "sha512-NU7GWc265m92xh/aYD79Vr1W+zAIXDWp3L2YZOYP4rCqPnJ6LI6vh3+rKgkidtYijozHclaEQTAHaAaMWPVI4A==", - "requires": { - "prop-types": "^15.7.2" - } - }, "react-shallow-renderer": { "version": "16.15.0", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", @@ -46796,39 +38469,6 @@ } } }, - "react-universal-interface": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", - "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==" - }, - "react-use": { - "version": "17.3.2", - "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.3.2.tgz", - "integrity": "sha512-bj7OD0/1wL03KyWmzFXAFe425zziuTf7q8olwCYBfOeFHY1qfO1FAMjROQLsLZYwG4Rx63xAfb7XAbBrJsZmEw==", - "requires": { - "@types/js-cookie": "^2.2.6", - "@xobotyi/scrollbar-width": "^1.9.5", - "copy-to-clipboard": "^3.3.1", - "fast-deep-equal": "^3.1.3", - "fast-shallow-equal": "^1.0.0", - "js-cookie": "^2.2.1", - "nano-css": "^5.3.1", - "react-universal-interface": "^0.6.2", - "resize-observer-polyfill": "^1.5.1", - "screenfull": "^5.1.0", - "set-harmonic-interval": "^1.0.1", - "throttle-debounce": "^3.0.1", - "ts-easing": "^0.2.0", - "tslib": "^2.1.0" - }, - "dependencies": { - "throttle-debounce": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", - "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==" - } - } - }, "react-virtualized-auto-sizer": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz", @@ -47058,17 +38698,6 @@ "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", "dev": true }, - "relay-runtime": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-12.0.0.tgz", - "integrity": "sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==", - "dev": true, - "requires": { - "@babel/runtime": "^7.0.0", - "fbjs": "^3.0.0", - "invariant": "^2.2.4" - } - }, "remark": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", @@ -47098,24 +38727,6 @@ "mdast-util-to-markdown": "^0.6.0" } }, - "remedial": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/remedial/-/remedial-1.0.8.tgz", - "integrity": "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==", - "dev": true - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, - "remove-trailing-spaces": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/remove-trailing-spaces/-/remove-trailing-spaces-1.0.8.tgz", - "integrity": "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==", - "dev": true - }, "renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -47217,12 +38828,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "require-package-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", @@ -47235,16 +38840,6 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, - "reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" - }, - "resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" - }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -47288,11 +38883,6 @@ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, - "response-iterator": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/response-iterator/-/response-iterator-0.2.6.tgz", - "integrity": "sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==" - }, "responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", @@ -47303,16 +38893,6 @@ "lowercase-keys": "^1.0.0" } }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -47331,12 +38911,6 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -47465,25 +39039,11 @@ "nearley": "^2.7.10" } }, - "rtl-css-js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", - "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", - "requires": { - "@babel/runtime": "^7.1.2" - } - }, "rudder-sdk-js": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/rudder-sdk-js/-/rudder-sdk-js-1.0.16.tgz", "integrity": "sha512-LbxFZBRpgOKp83EDzwSSjfOavj66vwQwdUZ5+h6BYOLLtnWPxiXTKXegoEujNhej6hTLQMVFLktsMoIkzYojCQ==" }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -47585,11 +39145,6 @@ "ajv-keywords": "^3.5.2" } }, - "screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==" - }, "scroll-into-view-if-needed": { "version": "2.2.29", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz", @@ -47598,12 +39153,6 @@ "compute-scroll-into-view": "^1.0.17" } }, - "scuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/scuid/-/scuid-1.1.0.tgz", - "integrity": "sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==", - "dev": true - }, "seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -47718,17 +39267,6 @@ } } }, - "sentence-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", - "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, - "requires": { - "no-case": "^3.0.4", - "tslib": "^2.0.3", - "upper-case-first": "^2.0.2" - } - }, "serialize-error": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.0.1.tgz", @@ -47833,23 +39371,6 @@ "send": "0.18.0" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, - "set-harmonic-interval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", - "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==" - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -47909,6 +39430,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, "requires": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -47921,12 +39443,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "signedsource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/signedsource/-/signedsource-1.0.0.tgz", - "integrity": "sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==", - "dev": true - }, "sirv": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz", @@ -47969,16 +39485,6 @@ "scroll-into-view-if-needed": "^2.2.28" } }, - "snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, - "requires": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, "sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -48077,7 +39583,8 @@ "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true }, "spawn-command": { "version": "0.0.2-1", @@ -48150,15 +39657,6 @@ "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", "dev": true }, - "sponge-case": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sponge-case/-/sponge-case-1.0.1.tgz", - "integrity": "sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -48172,14 +39670,6 @@ "dev": true, "optional": true }, - "stack-generator": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", - "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", - "requires": { - "stackframe": "^1.3.4" - } - }, "stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -48197,37 +39687,6 @@ } } }, - "stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "stacktrace-gps": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", - "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", - "requires": { - "source-map": "0.5.6", - "stackframe": "^1.3.4" - }, - "dependencies": { - "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==" - } - } - }, - "stacktrace-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", - "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", - "requires": { - "error-stack-parser": "^2.0.6", - "stack-generator": "^2.0.5", - "stacktrace-gps": "^3.0.4" - } - }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -48243,12 +39702,6 @@ "readable-stream": "^3.5.0" } }, - "streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true - }, "strict-uri-encode": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", @@ -48264,12 +39717,6 @@ "safe-buffer": "~5.2.0" } }, - "string-env-interpolation": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", - "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", - "dev": true - }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -48703,11 +40150,6 @@ "postcss-value-parser": "^4.1.0" } }, - "stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" - }, "sugarss": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", @@ -48867,20 +40309,6 @@ } } }, - "swap-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/swap-case/-/swap-case-2.0.2.tgz", - "integrity": "sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "symbol-observable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", - "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==" - }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -49044,16 +40472,12 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, - "throttle-debounce": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", - "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==" - }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "optional": true }, "thunky": { "version": "1.1.0", @@ -49102,24 +40526,6 @@ "@popperjs/core": "^2.9.0" } }, - "title-case": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", - "integrity": "sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -49168,11 +40574,6 @@ "to-no-case": "^1.0.0" } }, - "toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" - }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -49240,12 +40641,6 @@ "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", "dev": true }, - "true-myth": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/true-myth/-/true-myth-4.1.1.tgz", - "integrity": "sha512-rqy30BSpxPznbbTcAcci90oZ1YR4DqvKcNXNerG5gQBU2v4jk0cygheiul5J6ExIMrgDVuanv/MkGfqZbKrNNg==", - "dev": true - }, "ts-clone-node": { "version": "0.3.32", "resolved": "https://registry.npmjs.org/ts-clone-node/-/ts-clone-node-0.3.32.tgz", @@ -49255,105 +40650,6 @@ "compatfactory": "^0.0.13" } }, - "ts-easing": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", - "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" - }, - "ts-invariant": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", - "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", - "requires": { - "tslib": "^2.1.0" - } - }, - "ts-log": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.5.tgz", - "integrity": "sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==", - "dev": true - }, - "ts-morph": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz", - "integrity": "sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==", - "dev": true, - "requires": { - "@ts-morph/common": "~0.12.3", - "code-block-writer": "^11.0.0" - } - }, - "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "dependencies": { - "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - } - } - }, - "ts-prune": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/ts-prune/-/ts-prune-0.10.3.tgz", - "integrity": "sha512-iS47YTbdIcvN8Nh/1BFyziyUqmjXz7GVzWu02RaZXqb+e/3Qe1B7IQ4860krOeCGUeJmterAlaM2FRH0Ue0hjw==", - "dev": true, - "requires": { - "commander": "^6.2.1", - "cosmiconfig": "^7.0.1", - "json5": "^2.1.3", - "lodash": "^4.17.21", - "true-myth": "^4.1.0", - "ts-morph": "^13.0.1" - }, - "dependencies": { - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true - }, - "cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - } - } - }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -49510,12 +40806,6 @@ } } }, - "unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "dev": true - }, "uncontrollable": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-5.1.0.tgz", @@ -49524,15 +40814,6 @@ "invariant": "^2.2.4" } }, - "undici": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.20.0.tgz", - "integrity": "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g==", - "dev": true, - "requires": { - "busboy": "^1.6.0" - } - }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -49631,26 +40912,6 @@ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true }, - "unixify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", - "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", - "dev": true, - "requires": { - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -49667,24 +40928,6 @@ "picocolors": "^1.0.0" } }, - "upper-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", - "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, - "upper-case-first": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", - "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, - "requires": { - "tslib": "^2.0.3" - } - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -49721,20 +40964,6 @@ "dev": true, "optional": true }, - "urlpattern-polyfill": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz", - "integrity": "sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==", - "dev": true, - "requires": { - "braces": "^3.0.2" - } - }, - "use-isomorphic-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" - }, "use-memo-one": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", @@ -49778,12 +41007,6 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -49818,12 +41041,6 @@ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" }, - "value-or-promise": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", - "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", - "dev": true - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -49852,19 +41069,6 @@ "unist-util-stringify-position": "^2.0.0" } }, - "vue": { - "version": "3.2.47", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz", - "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==", - "dev": true, - "requires": { - "@vue/compiler-dom": "3.2.47", - "@vue/compiler-sfc": "3.2.47", - "@vue/runtime-dom": "3.2.47", - "@vue/server-renderer": "3.2.47", - "@vue/shared": "3.2.47" - } - }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -49919,33 +41123,11 @@ "minimalistic-assert": "^1.0.0" } }, - "wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "requires": { - "defaults": "^1.0.3" - } - }, "web-streams-polyfill": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" }, - "webcrypto-core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.6.tgz", - "integrity": "sha512-TBPiewB4Buw+HI3EQW+Bexm19/W4cP/qZG/02QJCXN+iN+T5sl074vZ3rJcle/ZtDBQSgjkbsQO/1eFcxnSBUA==", - "dev": true, - "requires": { - "@peculiar/asn1-schema": "^2.1.6", - "@peculiar/json-schema": "^1.1.12", - "asn1js": "^3.0.1", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" - } - }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -50397,12 +41579,6 @@ "is-weakset": "^2.0.1" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", - "dev": true - }, "which-typed-array": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", @@ -50538,12 +41714,6 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, - "yaml-ast-parser": { - "version": "0.0.43", - "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", - "dev": true - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -50576,12 +41746,6 @@ "fd-slicer": "~1.1.0" } }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -50608,21 +41772,6 @@ "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.9.0.tgz", "integrity": "sha512-xBrvSw1htnb2VEYpgjG7etqztHSZ4KyKzJgFh/rucI7QzkbxD0rVNmIni6lia6KhWs7RBbBBLZksGKrCSaDAMQ==" }, - "zen-observable-ts": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", - "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", - "requires": { - "zen-observable": "0.8.15" - }, - "dependencies": { - "zen-observable": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" - } - } - }, "zwitch": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", diff --git a/webapp/package.json b/webapp/package.json index c94b224c2c2..5ead703227b 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -51,7 +51,6 @@ "channels", "platform/client", "platform/components", - "platform/types", - "playbooks" + "platform/types" ] } diff --git a/webapp/platform/types/src/config.ts b/webapp/platform/types/src/config.ts index 1b8d8749caf..982c5256758 100644 --- a/webapp/platform/types/src/config.ts +++ b/webapp/platform/types/src/config.ts @@ -82,7 +82,6 @@ export type ClientConfig = { EnableOAuthServiceProvider: string; EnableOpenServer: string; EnableOutgoingWebhooks: string; - EnablePlaybooks: string; EnablePostIconOverride: string; EnablePostUsernameOverride: string; EnablePreviewFeatures: string; @@ -819,7 +818,6 @@ export type JobSettings = { }; export type ProductSettings = { - EnablePlaybooks: boolean; }; export type PluginSettings = { diff --git a/webapp/playbooks/.eslintrc.json b/webapp/playbooks/.eslintrc.json deleted file mode 100644 index 4ddac0c3e60..00000000000 --- a/webapp/playbooks/.eslintrc.json +++ /dev/null @@ -1,688 +0,0 @@ -{ - "extends": [ - "eslint:recommended", - "plugin:react-hooks/recommended" - - ], - "parser": "@typescript-eslint/parser", - "plugins": [ - "react", - "import", - "formatjs", - "@typescript-eslint", - "unused-imports", - "no-relative-import-paths", - "import-newlines" - ], - "env": { - "browser": true, - "node": true, - "jquery": true, - "es6": true, - "jest": true - }, - "globals": { - "jest": true, - "describe": true, - "it": true, - "expect": true, - "before": true, - "after": true, - "beforeEach": true - }, - "settings": { - "import/resolver": "webpack", - "react": { - "version": "detect" - } - }, - "rules": { - "array-bracket-spacing": [ - 2, - "never" - ], - "array-callback-return": 2, - "arrow-body-style": 0, - "arrow-parens": [ - 2, - "always" - ], - "arrow-spacing": [ - 2, - { - "before": true, - "after": true - } - ], - "block-scoped-var": 2, - "brace-style": [ - 2, - "1tbs", - { - "allowSingleLine": false - } - ], - "capitalized-comments": 0, - "class-methods-use-this": 0, - "comma-dangle": [ - 2, - "always-multiline" - ], - "comma-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "comma-style": [ - 2, - "last" - ], - "complexity": [ - 0, - 10 - ], - "computed-property-spacing": [ - 2, - "never" - ], - "consistent-return": 2, - "consistent-this": [ - 2, - "self" - ], - "constructor-super": 2, - "curly": [ - 2, - "all" - ], - "dot-location": [ - 2, - "property" - ], - "dot-notation": 2, - "eqeqeq": [ - 2, - "smart" - ], - "func-call-spacing": [ - 2, - "never" - ], - "func-name-matching": 0, - "func-names": 2, - "func-style": [ - 2, - "declaration", - { - "allowArrowFunctions": true - } - ], - "generator-star-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "global-require": 2, - "guard-for-in": 2, - "id-blacklist": 0, - "import/no-unresolved": 0, // ts handles this better - "import/order": [ - "error", - { - "newlines-between": "always-and-inside-groups", - "groups": [ - "builtin", - "external", - [ - "internal", - "parent" - ], - "sibling", - "index" - ] - } - ], - "import-newlines/enforce": [ - 2, - 3 - ], - "indent": 0, // ts handles this - "jsx-quotes": [ - 2, - "prefer-single" - ], - "key-spacing": [ - 2, - { - "beforeColon": false, - "afterColon": true, - "mode": "strict" - } - ], - "keyword-spacing": [ - 2, - { - "before": true, - "after": true, - "overrides": {} - } - ], - "line-comment-position": 0, - "linebreak-style": 2, - "lines-around-comment": [ - 2, - { - "beforeBlockComment": true, - "beforeLineComment": true, - "allowBlockStart": true, - "allowBlockEnd": true - } - ], - "max-lines": [ - 0, - { - "max": 450, - "skipBlankLines": true, - "skipComments": false - } - ], - "max-nested-callbacks": [ - 2, - { - "max": 8 - } - ], - "max-statements-per-line": [ - 2, - { - "max": 1 - } - ], - "multiline-ternary": [ - 1, - "never" - ], - "new-cap": 2, - "new-parens": 2, - "newline-before-return": 0, - "newline-per-chained-call": 0, - "no-alert": 2, - "no-array-constructor": 2, - "no-await-in-loop": 2, - "no-caller": 2, - "no-case-declarations": 2, - "no-class-assign": 2, - "no-compare-neg-zero": 2, - "no-cond-assign": [ - 2, - "except-parens" - ], - "no-confusing-arrow": 2, - "no-console": 2, - "no-const-assign": 2, - "no-constant-condition": 2, - "no-debugger": 2, - "no-div-regex": 2, - "no-dupe-args": 2, - "no-dupe-class-members": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-duplicate-imports": [ - 2, - { - "includeExports": true - } - ], - "no-else-return": 2, - "no-empty": 2, - "no-empty-function": 2, - "no-empty-pattern": 2, - "no-eval": 2, - "no-ex-assign": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-label": 2, - "no-extra-parens": 0, - "no-extra-semi": 2, - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-func-assign": 2, - "no-global-assign": 2, - "no-implicit-coercion": 2, - "no-implicit-globals": 0, - "no-implied-eval": 2, - "no-inner-declarations": 0, - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-lonely-if": 2, - "no-loop-func": 2, - "no-magic-numbers": [ - 0, - { - "ignore": [ - -1, - 0, - 1, - 2 - ], - "enforceConst": true, - "detectObjects": true - } - ], - "no-mixed-operators": [ - 2, - { - "allowSamePrecedence": false - } - ], - "no-mixed-spaces-and-tabs": 2, - "no-multi-assign": 2, - "no-multi-spaces": [ - 2, - { - "exceptions": { - "Property": false - } - } - ], - "no-multi-str": 0, - "no-multiple-empty-lines": [ - 2, - { - "max": 1 - } - ], - "no-native-reassign": 2, - "no-negated-condition": 2, - "no-nested-ternary": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-symbol": 2, - "no-new-wrappers": 2, - "no-octal-escape": 2, - "no-param-reassign": 2, - "no-process-env": 2, - "no-process-exit": 2, - "no-proto": 2, - "no-redeclare": 2, - "no-return-assign": [ - 2, - "always" - ], - "no-return-await": 2, - "no-script-url": 2, - "no-self-assign": [ - 2, - { - "props": true - } - ], - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow": 0, - "no-shadow-restricted-names": 2, - "no-spaced-func": 2, - "no-tabs": 0, - "no-template-curly-in-string": 2, - "no-ternary": 0, - "no-this-before-super": 2, - "no-throw-literal": 2, - "no-trailing-spaces": [ - 2, - { - "skipBlankLines": false - } - ], - "no-undef-init": 2, - "no-undefined": 0, - "no-underscore-dangle": 2, - "no-unexpected-multiline": 2, - "no-unmodified-loop-condition": 2, - "no-unneeded-ternary": [ - 2, - { - "defaultAssignment": false - } - ], - "no-unreachable": 2, - "no-unsafe-finally": 2, - "no-unsafe-negation": 2, - "no-unused-expressions": 2, - "no-unused-vars": [ - 2, - { - "vars": "all", - "args": "after-used" - } - ], - "no-use-before-define": 0, - "no-useless-computed-key": 2, - "no-useless-concat": 2, - "no-useless-constructor": 2, - "no-useless-escape": 2, - "no-useless-rename": 2, - "no-useless-return": 2, - "no-var": 0, - "no-void": 2, - "no-warning-comments": 1, - "no-whitespace-before-property": 2, - "no-with": 2, - "object-curly-newline": 0, - "object-curly-spacing": [ - 2, - "never" - ], - "object-property-newline": [ - 2, - { - "allowMultiplePropertiesPerLine": true - } - ], - "object-shorthand": [ - 2, - "always" - ], - "one-var": [ - 2, - "never" - ], - "one-var-declaration-per-line": 0, - "operator-assignment": [ - 2, - "always" - ], - "operator-linebreak": [ - 2, - "after" - ], - "padded-blocks": [ - 2, - "never" - ], - "prefer-arrow-callback": 2, - "prefer-const": 2, - "prefer-destructuring": 0, - "prefer-numeric-literals": 2, - "prefer-promise-reject-errors": 2, - "prefer-rest-params": 2, - "prefer-spread": 2, - "prefer-template": 0, - "quote-props": [ - 2, - "as-needed" - ], - "quotes": [ - 2, - "single", - "avoid-escape" - ], - "radix": 2, - "react/display-name": [ - 0, - { - "ignoreTranspilerName": false - } - ], - "react/forbid-component-props": 0, - "react/forbid-elements": [ - 2, - { - "forbid": [ - "embed" - ] - } - ], - "react/jsx-boolean-value": [ - 2, - "always" - ], - "react/jsx-closing-bracket-location": [ - 2, - { - "location": "tag-aligned" - } - ], - "react/jsx-curly-spacing": [ - 2, - "never" - ], - "react/jsx-equals-spacing": [ - 2, - "never" - ], - "react/jsx-filename-extension": 2, - "react/jsx-first-prop-new-line": [ - 2, - "multiline" - ], - "react/jsx-handler-names": 0, - "react/jsx-indent": [ - 2, - 4 - ], - "react/jsx-indent-props": [ - 2, - 4 - ], - "react/jsx-key": 2, - "react/jsx-max-props-per-line": [ - 2, - { - "maximum": 1 - } - ], - "react/jsx-no-bind": 0, - "react/jsx-no-comment-textnodes": 2, - "react/jsx-no-duplicate-props": [ - 2, - { - "ignoreCase": false - } - ], - "react/jsx-no-literals": 2, - "react/jsx-no-target-blank": 2, - "react/jsx-no-undef": 2, - "react/jsx-pascal-case": 2, - "react/jsx-tag-spacing": [ - 2, - { - "closingSlash": "never", - "beforeSelfClosing": "never", - "afterOpening": "never" - } - ], - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - "react/jsx-wrap-multilines": 2, - "react/no-array-index-key": 1, - "react/no-children-prop": 2, - "react/no-danger": 0, - "react/no-danger-with-children": 2, - "react/no-deprecated": 1, - "react/no-did-mount-set-state": 2, - "react/no-did-update-set-state": 2, - "react/no-direct-mutation-state": 2, - "react/no-find-dom-node": 1, - "react/no-is-mounted": 2, - "react/no-multi-comp": [ - 2, - { - "ignoreStateless": true - } - ], - "react/no-render-return-value": 2, - "react/no-set-state": 0, - "react/no-string-refs": 0, - "react/no-unescaped-entities": 2, - "react/no-unknown-property": 2, - "react/no-unused-prop-types": [ - 1, - { - "skipShapeProps": true - } - ], - "react/prefer-es6-class": 2, - "react/prefer-stateless-function": 2, - "react/prop-types": [ - 2, - { - "ignore": [ - "location", - "history", - "component", - "className" - ] - } - ], - "react/require-default-props": 0, - "react/require-optimization": 1, - "react/require-render-return": 2, - "react/self-closing-comp": 2, - "react/sort-comp": 0, - "react/style-prop-object": [ - 2, - { - "allow": [ - "FormattedNumber", - "FormattedDuration", - "FormattedRelativeTime", - "Timestamp" - ] - } - ], - "require-yield": 2, - "rest-spread-spacing": [ - 2, - "never" - ], - "semi": [ - 2, - "always" - ], - "semi-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "sort-imports": [ - 2, - { - "ignoreDeclarationSort": true - } - ], - "sort-keys": 0, - "space-before-blocks": [ - 2, - "always" - ], - "space-before-function-paren": [ - 2, - { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - } - ], - "space-in-parens": [ - 2, - "never" - ], - "space-infix-ops": 2, - "space-unary-ops": [ - 2, - { - "words": true, - "nonwords": false - } - ], - "symbol-description": 2, - "template-curly-spacing": [ - 2, - "never" - ], - "valid-typeof": [ - 2, - { - "requireStringLiterals": false - } - ], - "vars-on-top": 0, - "wrap-iife": [ - 2, - "outside" - ], - "wrap-regex": 2, - "yoda": [ - 2, - "never", - { - "exceptRange": false, - "onlyEquality": false - } - ], - "formatjs/no-multiple-whitespaces": 2, - "formatjs/enforce-default-message": 2, - "formatjs/no-multiple-plurals": 2, - "formatjs/enforce-placeholders": 2, - "formatjs/no-literal-string-in-jsx": 2, - "unused-imports/no-unused-imports": 2, - "no-relative-import-paths/no-relative-import-paths": [ - "error", - { "allowSameFolder": true } - ] - }, - "overrides": [ - { - "files": ["**/*.tsx", "**/*.ts"], - "extends": "plugin:@typescript-eslint/recommended", - "rules": { - "@typescript-eslint/ban-ts-ignore": 0, - "@typescript-eslint/ban-types": 1, - "@typescript-eslint/ban-ts-comment": 0, - "@typescript-eslint/no-unused-vars": [ - 2, - { - "vars": "all", - "args": "after-used" - } - ], - "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/prefer-interface": 0, - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/explicit-module-boundary-types": 0, - "@typescript-eslint/no-shadow": 2, - "@typescript-eslint/indent": [ - 2, - 4, - { - "SwitchCase": 0 - } - ], - "@typescript-eslint/no-use-before-define": [ - 2, - { - "classes": false, - "functions": false, - "variables": false - } - ], - "react/jsx-filename-extension": [ - 1, - { - "extensions": [".jsx", ".tsx"] - } - ] - } - } - ] -} diff --git a/webapp/playbooks/.npmrc b/webapp/playbooks/.npmrc deleted file mode 100644 index cffe8cdef13..00000000000 --- a/webapp/playbooks/.npmrc +++ /dev/null @@ -1 +0,0 @@ -save-exact=true diff --git a/webapp/playbooks/babel.config.js b/webapp/playbooks/babel.config.js deleted file mode 100644 index 90d71570dc6..00000000000 --- a/webapp/playbooks/babel.config.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -const config = { - presets: [ - ['@babel/preset-env', { - targets: { - chrome: 110, - firefox: 102, - edge: 110, - safari: 16, - }, - corejs: 3, - useBuiltIns: 'usage', - shippedProposals: true, - }], - ['@babel/preset-react', { - useBuiltIns: true, - }], - ['@babel/typescript', { - allExtensions: true, - isTSX: true, - }], - ], - plugins: [ - 'babel-plugin-typescript-to-proptypes', - 'babel-plugin-add-react-displayname', - [ - 'babel-plugin-styled-components', - { - ssr: false, - fileName: false, - }, - ], - [ - 'formatjs', - { - idInterpolationPattern: '[sha512:contenthash:base64:6]', - ast: true, - }, - ], - ], -}; - -config.env = { - test: { - presets: config.presets, - plugins: config.plugins, - }, - production: { - presets: config.presets, - plugins: config.plugins.filter((plugin) => plugin !== 'babel-plugin-typescript-to-proptypes'), - }, -}; - -module.exports = config; diff --git a/webapp/playbooks/graphql_gen.ts b/webapp/playbooks/graphql_gen.ts deleted file mode 100644 index 9165a1ff396..00000000000 --- a/webapp/playbooks/graphql_gen.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {CodegenConfig} from '@graphql-codegen/cli'; - -const config: CodegenConfig = { - overwrite: true, - schema: '../server/api/schema.graphqls', - documents: ['src/graphql/*.graphql', 'src/**/*.tsx', '!src/graphql/generated/**/*'], - generates: { - 'src/graphql/generated/': { - preset: 'client', - plugins: [], - presetConfig: { - fragmentMasking: {unmaskFunctionName: 'getFragmentData'}, - }, - }, - }, - hooks: { - afterAllFileWrite: 'eslint --fix', - }, -}; - -// ts-prune-ignore-next -export default config; diff --git a/webapp/playbooks/i18n/cs.json b/webapp/playbooks/i18n/cs.json deleted file mode 100644 index fd9de1f9b04..00000000000 --- a/webapp/playbooks/i18n/cs.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "1I48bs": "Vzor Retrospektivy", - "1GOpgL": "Přiřazující...", - "15jbT0": "Přidat více do Vaší časové osy", - "0tznw6": "Převést na soukromý playbook", - "0oLj/t": "Rozšířit", - "0oL1zz": "Zkopírováno!", - "0boT49": "Jste si jistí, že chcete ukončit běh pro všechny účastníky?", - "0Xt1ea": "Stále budete moct přistoupit k historickým datům pro tuto metriku.", - "0Vvpht": "Udělat člena Playbooku", - "0RlzlZ": "Zaslat dočasnou vítací zprávu uživateli", - "0QD99o": "Zažádat o přidání do kanálu", - "0HT+Ib": "Archivované", - "0Azlrb": "Spravovat", - "03oqA2": "Aktivní Běhy", - "/urtZ8": "Vaše Playbooky", - "/qDObA": "Procházet Běhy", - "/jUtaM": "AKTIVNÍ BĚHY za den během posledních 14 dní", - "/gbqA6": "{duration} než běh začal", - "/YZ/sw": "Začít zkušení verzi", - "/RnCQb": "Poslat odchozí webhook", - "/MaJux": "Zahájit retrospektivu", - "/1FEJW": "AKTIVNÍ ÚČASTNÍCI za den během posledních 14 dnů", - "/GCoTA": "Vyčistit", - "//o1Nu": "Vypnout aktualizace", - "/+8SGX": "Zobrazuji {filteredNum} z {totalNum} událostí", - "+qDKgW": "Zobrazit všechny aktualizace", - "+hddg7": "Přidat do časové osy běhu", - "+Tmpup": "Automaticky dostanete aktualizace když bude tento playbook spuštěn.", - "+8G9qr": "Výchozí text pro retrospektivu.", - "+/x2FM": "Vyberte playbook", - "/HtNUp": "Vyberte nebo zadejte {mode, select, DurationValue {časový rozsah (\"4 hodiny\", \"7 dní\"...)} DateTimeValue {čas (\"za 4 hodiny\", \"1. května\", \"Zítra ve 13:00\"...)} jiný {čas nebo časové rozpětí}}" -} diff --git a/webapp/playbooks/i18n/de.json b/webapp/playbooks/i18n/de.json deleted file mode 100644 index ae8050659c8..00000000000 --- a/webapp/playbooks/i18n/de.json +++ /dev/null @@ -1,853 +0,0 @@ -{ - "z5RMPO": "Nur Sie haben Zugang zu diesem Playbook", - "waVyVY": "Derzeit aktive Teilnehmer", - "wEQDC6": "Bearbeiten", - "v3+TmO": "{members, plural, =0 {Keine Person kann} =1 {Eine Person kann} other {# Personen können}} auf das Playbook zugreifen", - "t6SiGO": "Derzeit aktive Läufe", - "soePYH": "{num_checklists, plural, =0 {keine Check-Listen} one {# Check-Liste} other {# Check-Listen}}", - "sQu1rA": "{numTotalRuns, plural, =0 {keine Läufe gestartet} =1 {# Lauf gestartet} other {# Läufe gestartet}}", - "s3jjqi": "{num_actions, plural, =0 {keine Aktionen} one {# Aktion} other {# Aktionen}}", - "lZwZi+": "Tag: {date}", - "ebkl6I": "Jeder in diesem Team kann auf dieses Playbook zugreifen", - "c8hxKk": "Woche von {date}", - "bPLen5": "In den letzten 30 Tagen beendete Läufe", - "avPeEI": "Aktualisieren Sie, um die Trends für die Gesamtanzahl der Läufe, die aktiven Läufe und die an den Läufen dieses Playbook beteiligten Teilnehmer anzuzeigen.", - "YDuW/T": "{num_runs, plural, =0 {Keine Läufe} one {# Lauf} other {# Läufe insgesamt}}", - "XmUdvV": "Alle Statistiken, die Sie brauchen", - "UbTsGY": "Läufe, die zwischen {start} und {end} gestartet wurden", - "TSSNg/": "GESAMTE LÄUFE pro Woche in den letzten 12 Wochen", - "RoGxij": "Läuft aktiv am {date}", - "Q7aZO4": "{numParticipants, plural, =0 {keine aktive Teilnehmer} =1 {# aktiver Teilnehmer} other {# aktive Teilnehmer}}", - "KiXNvz": "Ausführen", - "AF9wda": "Diese Aktualisierung wird auf der Übersichtsseite{hasBroadcast, select, true { gespeichert und an {broadcastChannelCount, plural, =1 {ein Kanal gesendet} other {{broadcastChannelCount, number} Kanäle gesendet}}} other {}}.", - "6Lwe7T": "Jeder in {team} kann auf dieses Playbook zugreifen", - "1MQ3XZ": "{numActiveRuns, plural, =0 {Kein aktiver Lauf} =1 {# aktiver Lauf} other {# aktive Läufe}}", - "/jUtaM": "AKTIVE LÄUFE pro Tag über die letzten 14 Tagen", - "/HtNUp": "Wählen Sie oder geben Sie einen {mode, select, DurationValue {time span (\"4 Stunden\", \"7 Tage\"...)} DateTimeValue {time (\"in 4 Stunden\", \"1 Mai\", \"Morgen um 13:00\"...)} other {Zeit oder Zeitdauer}}", - "/1FEJW": "AKTIVE TEILNEHMER pro Tag über die letzten 14 Tagen", - "zINlao": "Besitzer", - "recCg9": "Aktualisierungen", - "jXT2++": "Zum Kanal gehen", - "hXIYHG": "Installieren und aktivieren Sie das Kanal-Export-Plugin, um den Export des Kanals zu unterstützen", - "gy/Kkr": "(bearbeitet)", - "g5pX+a": "Über", - "dcV/DJ": "{timestamp}", - "d9epHh": "Kanalprotokoll exportieren", - "b40Pr7": "Reporter", - "VmpFFw": "Es ist keine Beschreibung verfügbar.", - "T7Ry38": "Nachricht", - "RthEJt": "Retrospektive", - "R+JQaJ": "Kanalmitglieder", - "Q8Qw5B": "Beschreibung", - "IuFETn": "Dauer", - "EC5MJD": "Es sind keine Aktualisierungen verfügbar.", - "BD66u6": "Herunterladen einer CSV-Datei mit allen Nachrichten aus dem Kanal", - "AS5kar": "Teilnehmer ({participants})", - "9uOFF3": "Übersicht", - "5FRgqE": "Herunterladen des Kanalprotokolls", - "uJ3bRR": "Diese Vorlage hilft bei der Standardisierung des Formats für eine prägnante Beschreibung, die jeden Lauf für die Beteiligten erläutert.", - "oS0w4E": "Standard-Update-Timer", - "lbhO3D": "kursiv", - "jIIWN+": "vorformatiert", - "hzt6l8": "Verwenden Sie Markdown, um eine Vorlage zu erstellen.", - "eiPBw7": "Erinnerungsintervall für Retrospektive", - "eHAvFf": "fett", - "bLK+Kr": "Erinnert den Kanal in einem bestimmten Intervall daran, die Retrospektive auszufüllen.", - "TZYiF/": "Durchgestrichen", - "TJo5E6": "Vorschau", - "T5rX+W": "Wie oft soll eine Aktualisierung veröffentlicht werden?", - "SENRqu": "Hilfe", - "QiKcO7": "Retrospektive Vorlage eingeben", - "JCGvY/": "Diese Vorlage hilft bei der Standardisierung des Formats für wiederkehrende Aktualisierungen, die während jedes Laufs stattfinden, um sie beizubehalten.", - "HhLp57": "Zitat", - "DCl7Vv": "Inline-Code", - "A3ptul": "Vorlagen", - "3rCdDw": "Status-Updates", - "1I48bs": "Vorlage Retrospektive", - "+8G9qr": "Standardtext für die Retrospektive.", - "rX08cW": "Das Datum muss in der Zukunft liegen.", - "wbwhbH": "Name der Aufgabe", - "jvo0vs": "Speichern", - "QnZAit": "Optionale Beschreibung hinzufügen", - "ObmjTB": "Slash-Befehle", - "ICqy9/": "Checklisten", - "5A46pW": "Slash-Befehl hinzufügen", - "47FYwb": "Abbrechen", - "wbsq7O": "Verwendung", - "viXE32": "Privat", - "uhu5aG": "Öffentlich", - "jq4eWU": "Zugang zum Playbook", - "djXM+y": "Nur ausgewählte Benutzer können darauf zugreifen.", - "SFuk1v": "Berechtigungen", - "NE1OeI": "Alle Mitglieder des Teams ({team}) haben Zugang.", - "DnBhRg": "Benutzer hinzufügen", - "CL5OZP": "Nur die von Ihnen ausgewählten Benutzer können dieses Playbook bearbeiten oder ausführen.", - "5Ot7cd": "Bestimmen Sie den Typ des Channels, den dieses Playbook erstellt.", - "+ZIXOR": "Zugang zum Kanal", - "X3DLGJ": "Jeder in diesem Arbeitsbereich kann Playbooks erstellen.", - "TyrY2b": "Playbook-Erstellung", - "D3idYv": "Einstellungen", - "AT2QBo": "Nur ausgewählte Benutzer können Playbooks erstellen.", - "zy3cJT": "Aufforderung zur Ausführung dieses Playbooks, wenn ein Benutzer eine Nachricht mit den Schlüsselwörtern sendet", - "wL7VAE": "Aktion", - "usa8vQ": "Senden Sie eine Willkommensnachricht", - "nvy0pS": "Wenn ein Lauf beendet ist, exportieren Sie den Kanal", - "lxfpbh": "Der Eigentümer des Kanals wird {reminderEnabled, select, true {jede zu einer Statusaktualisierung aufgefordert} other {nicht zu einer Statusaktualisierung aufgefordert}}", - "jS/UOn": "Vorlage aktualisieren", - "hO9EdA": "Laden Sie {numInvitedUsers, plural, =0 {kein Mitglied} =1 {ein Mitglied} other {# Mitglieder}} in den Kanal", - "bGhCLX": "Wenn eine Aktualisierung veröffentlicht wird", - "b5FaCc": "Den Kanal in die Seitenleistekategorie hinzufügen", - "Z/hwEf": "Der Kanal wird {reminderEnabled, select, true {jeden} other {}} erinnert die Retrospektive durchzuführen", - "Ui6GK/": "Wenn ein neues Mitglied dem Kanal beitritt", - "SDSqfA": "Wenn ein Lauf beginnt", - "OINwWS": "Einen {isPublic, select, true {öffentlichen} other {privaten}} Kanal erstellen", - "LRFvqz": "Verkünden in {oneChannel, plural, one {Kanal} other {Kanäle}}", - "KUr+sG": "Zusammenfassung aktualisieren", - "Hzwzgs": "Broadcast-Updates in {oneChannel, plural, one {Kanal} other {Kanäle}}", - "CjNrqO": "Vorlage für einen retrospektiven Bericht", - "8hDbW6": "Ausgehenden Webhook senden", - "+QgvjN": "Weisen Sie die Eigentümerrolle zu", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {Aufgabe} other {Aufgaben}}", - "zWkvNO": "Zeitleiste", - "zELxbG": "Gespeicherte Nachrichten", - "yhzuSC": "Zeit: {time}", - "x5Tz6M": "Bericht", - "w7tf2z": "Veröffentlicht", - "vndQuC": "Slash-Befehl ausgeführt", - "vOFN0m": "Statusmeldung gelöscht:", - "v1SpKO": "Änderungen der Rolle", - "v1DNMW": "Rückwirkender Bericht von {name} veröffentlicht", - "syEQFE": "Veröffentlichen", - "pKLw8O": "Sind Sie sicher, dass Sie dieses Ereignis löschen möchten? Gelöschte Ereignisse werden dauerhaft aus der Zeitleiste entfernt.", - "o2eHmz": "Durchlauf von {name} beendet", - "jnmORb": "In diesem Playbook", - "fXGjhC": "Besitzer geändert von {summary}", - "fUEpLA": "Es gibt keine Timeline-Ereignisse, die diesen Filtern entsprechen.", - "egvJrY": "Verantwortlicher geändert", - "aACJNp": "Lauf gestartet von {name}", - "ZwlIYH": "{activeRuns, number} {activeRuns, plural, one {aktiver Lauf} other {aktive Läufe}}", - "W9j0FJ": "{date}", - "TvihSy": "Neu veröffentlichen", - "OsDomv": "Alle Ereignisse", - "OcpRSQ": "Eintrag löschen", - "N1U/QR": "Änderung des Aufgabenstatus", - "MvEydR": "{name} hat ein Status-Update gepostet", - "LmhSmU": "Bestätige Löschung", - "JeqL8w": "Retrospektive von {name} abgebrochen", - "I2zEie": "Feiern Sie Erfolge und lernen Sie aus Fehlern mit retrospektiven Berichten. Filtern Sie Zeitplan-Ereignisse für Prozessüberprüfung, Stakeholder-Engagement und Auditing-Zwecke.", - "FEGywG": "Bitte geben Sie ein zukünftiges Datum/Uhrzeit für die Aktualisierungserinnerung an.", - "DXACD6": "Retrospektive veröffentlichen und auf die Zeitleiste zugreifen", - "ArpdYl": "Ereignisse in der Zeitleiste werden hier angezeigt, sobald sie auftreten. Bewegen Sie den Mauszeiger über ein Ereignis, um es zu entfernen.", - "AML4RW": "Zugewiesene Aufgaben", - "9Obw6C": "Filter", - "4Hrh5B": "{name} hat den Status von {summary} geändert", - "3/wF0G": "Slash-Befehle", - "izWS4J": "Nich mehr folgen", - "ieGrWo": "Folgen", - "9TTfXU": "Ihr Systemadministrator wurde benachrichtigt.", - "9PXW6Q": "Dauer / Begonnen am", - "91Hr5f": "Ziehen Sie mich zum neuanordnen", - "9+Ddtu": "Weiter", - "6uhSSw": "Kanal auswählen", - "6n0XDG": "Sind Sie sicher, dass Sie die Checkliste entfernen möchten? Alle Aufgaben werden entfernt.", - "6jDabx": "Feedback geben", - "6CGo3o": "Status / Letzte Aktualisierung", - "5wqhGy": "Laufdetails umschalten", - "5qBEKB": "Was sind Playbookläufe?", - "5CI3KH": "Support kontaktieren", - "4ltHYh": "Zum Playbook gehen", - "42qmJ5": "Sie haben nicht die Erlaubnis, eine Aktualisierung zu veröffentlichen.", - "3Psa+5": "Schlüsselwörter hinzufügen", - "2VrVHu": "Suche nach Laufname", - "2Qq4YX": "Sind Sie sicher, dass Sie Ihre Änderungen verwerfen wollen?", - "2QkJ4s": "Speichern Sie wichtige Nachrichten, um ein vollständiges Bild zu erhalten, welche die Retrospektiven vereinfacht.", - "2PNrBQ": "Exportieren Sie den Kanal Ihres Playbook-Laufs und speichern Sie ihn zur späteren Analyse.", - "15jbT0": "Mehr zu Ihrer Zeitleiste hinzufügen", - "0wJ7N+": "Aufgabe", - "0oLj/t": "Erweitern", - "/YZ/sw": "Testzeitraum starten", - "/MaJux": "Beginn der Retrospektive", - "+hddg7": "Zur Zeitleiste hinzufügen", - "zx0myy": "Teilnehmer", - "z3A0LP": "Letzter Durchlauf war vor {relativeTime}", - "yxguVq": "Änderungen verwerfen", - "yqpcOa": "Verwenden", - "yhU1et": "Aufgaben", - "xmcVZ0": "Suchen", - "x8cvBr": "Durchlaufübersicht anzeigen", - "wsUmh9": "Team", - "wcWpGs": "Ungültige Webhook URLs", - "wZ83YL": "Nicht jetzt", - "w0muFd": "Sende ausgehenden Webhook (Einer pro Zeile)", - "vir0m9": "Ungültiger Kategoriename.", - "vNiZXF": "Aktuell sind keine Durchläufe im Gange. Führen Sie ein Playbook aus, um mit der Orchestrierung von Workflows für Ihr Team und Ihre Tools zu beginnen.", - "v8ZnNc": "Team auswählen", - "uny3Zy": "Playbooks", - "uBLF+D": "Was ist ein Playbook?", - "u4MwUB": "Speichern Sie den Verlauf Ihrer Playbook-Durchläufe", - "tzMNF3": "Status", - "twieZh": "Zur Durchlaufübersicht wechseln", - "sqNmlF": "Retrospektive überspringen", - "scYyVv": "Möchten Sie eine Retrospektive durchführen?", - "sVlNlY": "Die Struktur jedes Teams ist unterschiedlich. Sie können verwalten, welche Benutzer im Team Playbooks erstellen können.", - "sIX63S": "Ihr Systemadministrator wurde benachrichtigt", - "sGJpuF": "Beschreibung hinzufügen…", - "ryrP8K": "Verwalten Sie die Berechtigungen für die Personen, die dieses Playbook anzeigen, ändern und ausführen dürfen.", - "rbrahO": "Schließen", - "rDvvQs": "{completed, number} / {total, number} erledigt", - "qyJtWy": "Weniger anzeigen", - "qp3Fk4": "Ein Playbook ist ein Arbeitsablauf, dem Ihre Teams und Tools folgen sollten, einschließlich Checklisten, Aktionen, Vorlagen und Retrospektiven.", - "q6f8x9": "Änderung seit der letzten Aktualisierung", - "prYDT6": "Ankündigungskanal", - "pjt3qA": "Neue Checkliste", - "oVHn4s": "Letztes Aktualisierung", - "nqVby7": "{numTasksChecked, number} von {numTasks, number} {numTasks, plural, =1 {Aufgabe} other {Aufgaben}} geprüft", - "nmpevl": "Verwerfen", - "nkCCM2": "Sie werden nicht mehr daran erinnert.", - "lrbrjv": "Ja, starte Retrospektive", - "lJyq2a": "Durchlauf nicht gefunden", - "kvgvNW": "Wissen, was passiert ist", - "kXFojL": "Sie können ein Playbook auch vorab erstellen, damit es zur Verfügung steht, wenn Sie es brauchen.", - "kGI46P": "Aufgabenbeschreibung", - "k9q07e": "Aktualisierung an andere Kanäle senden", - "jwimQJ": "OK", - "jIgqRa": "Besitzer / Teilnehmer", - "j7jdWG": "Zu einer kommerziellen Edition wechseln.", - "ijAUQf": "Informieren Sie Ihren Systemadministrator über das Upgrade.", - "iNU1lj": "Der Durchlauf, den Sie anfragen ist privat oder existiert nicht.", - "hfrrC7": "Team Initialen", - "hVFgh4": "Abgeschlossene einbeziehen", - "guunZt": "Zuweisen", - "gt6BhE": "Durchlaufdetails", - "g4IF1x": "Es gibt keine Durchläufe für dieses Playbook.", - "fpuWL1": "Playbook löschen", - "fmylXu": "Aufforderung zur Ausführung des Playbooks, wenn ein Benutzer eine Nachricht schreibt", - "fdQDz+": "Das Playbook {title} wurde erfolgreich gelöscht.", - "fV6578": "Besitzerrolle zuweisen", - "edxtzC": "Playbook erstellen", - "eKv7yX": "Beitrag", - "e/AZL5": "Ihr 30-Tage Test hat begonnen", - "dsTLW1": "Aufgabe bearbeiten", - "dIwav9": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Sie wird aus diesem Durchlauf entfernt, hat aber keine Auswirkungen auf das Playbook.", - "d8KvXJ": "Ihre Testlizenz läuft am {expiryDate} ab. Sie können jederzeit eine Lizenz durch das Kundenportal erwerben um Unterbrechungen zu vermeiden.", - "c6LNcW": "Aufgabe löschen", - "bE1Cro": "Nur meine Durchläufe", - "b3TdyZ": "Mit Klicken von Starte Test, stimme ich dem Mattermost Software Evaluation Agreement und der Datenschutzrichtlinie zu, und erhalte Produktemails.", - "b/QBNs": "Aktualisierung fällig", - "aYIUar": "Danke!", - "aWpBzj": "Zeige mehr", - "ZdWYcm": "Nein, Retrospektive überspringen", - "ZAJviT": "Wir waren nicht in der Lage, den Systemadministrator zu benachrichtigen.", - "Z7vWDQ": "Es gab einen Fehler", - "YORRGQ": "Aktualisierung senden", - "YKn+7s": "Dieser Kanal hat keine Playbooks.", - "Y+U8La": "Sind Sie sicher, dass Sie das Playbook {title} löschen wollen?", - "X/koAN": "Ungültiger Eintrag: Die maximal zulässige Anzahl von Webhooks beträgt 64", - "WTQpnI": "Starten Sie jetzt mit Playbooks", - "WIxhrv": "Durchlaufnamen müssen mindestens zwei Zeichen haben", - "WAHCT2": "Systemadministrator benachrichtigen", - "W1Qs5O": "Durchläufe", - "W/V6+Y": "Zuklappen", - "VmnoW8": "Bitte prüfen Sie die Systemprotokolle für weitere Informationen.", - "VOzlSL": "Die Ausführung eines Playbooks orchestriert die Arbeitsabläufe für Ihr Team und Ihre Tools.", - "V5TY0z": "Teilnehmer hinzufügen?", - "TdTXXf": "Erfahre mehr", - "TDaF6J": "Ablehnen", - "TBez4r": "Es sind keine Playbooks vorhanden. Sie haben keine Berechtigung zum Erstellen von Playbooks in diesem Arbeitsbereich.", - "SmAUf9": "Eine Erinnerung wird um {timestamp} gesendet", - "S0kWcH": "Aktualisierung überfällig", - "Rgo4VW": "Jeder in diesem Arbeitsbereich kann Playbooks erstellen. Systemadministratoren können diese Einstellung ändern.", - "R4vA+C": "Nur die unten aufgeführten Benutzer können Playbooks erstellen. Diese Benutzer sowie die Systemadministratoren können diese Einstellung ändern.", - "Qrl6bQ": "Optimieren Sie Ihre Prozesse mit Playbooks", - "QaZNp9": "Durchlauf beenden", - "QVQrgH": "Nachdem Sie Ihren eigenen Zugang zu diesem Playbook entfernt haben, können Sie sich selbst nicht wieder hinzufügen. Sind Sie sicher, dass Sie diese Aktion durchführen möchten?", - "QUwMsX": "Erinnerung an das Ausfüllen der Retrospektive", - "Q7hMnp": "Playbook starten", - "Q67RuY": "Alle Durchläufe anzeigen", - "OK8u0r": "Erstellen Sie ein Playbook, um den Arbeitsablauf festzulegen, den Ihre Teams und Tools befolgen sollten, einschließlich aller Checklisten, Aktionen, Vorlagen und Retrospektiven.", - "OHfpS1": "Enthält eines der folgenden Schlüsselwörter", - "Nh91Us": "{from, number}–{to, number} von {total, number}", - "N2IrpM": "Bestätigen", - "Mm1Gse": "Suche nach Teilnehmer", - "MhKICa": "Ihr Abo erlaubt ein Playbook pro Team. Aktualisieren Sie Ihr Abo und erstellen Sie mehrere Playbooks mit spezifischen Abläufen für jedes Team.", - "MDP9TS": "Vom Playbook löschen", - "M/2yY/": "Bisher keiner.", - "Lg3I1b": "@{targetUsername}, bitte geben Sie einen aktuellen Status an.", - "Leh2tk": "Hier klicken um alle Durchläufe des Teams anzuzeigen.", - "LVYPbG": "Besitzer zuweisen", - "L6k6aT": "---oder starte mit einer Vorlage", - "KJu1sq": "Checkliste löschen", - "K4O03z": "Neue Aufgabe", - "K3r6DQ": "Löschen", - "JXdbo8": "Erledigt", - "JJNc3c": "Vorherige", - "JJMNME": "{withRunName, select, true {@{authorUsername} hat ein Update für [{runName}]({overviewURL}) geposted} other {@{authorUsername} hat ein Update gepostet}}", - "J1G4S4": "Es sind bisher keine Playbooks definiert.", - "IwY/wg": "Ein Playbook für jeden Prozess", - "Ietscn": "Aufgaben beendet", - "I90sbW": "gerade jetzt", - "HSi3uv": "Kein Bearbeiter", - "HAlOn1": "Name", - "GxJAK1": "Das von Ihnen angeforderte Playbook ist privat oder existiert nicht.", - "GwtR3W": "Verschieben Sie eine vorhandene Aufgabe per Drag & Drop oder klicken Sie um eine neue Aufgabe zu erstellen.", - "GRTyvN": "Playbookliste umschalten", - "G/yZLu": "Entfernen", - "DuRxjT": "Playbook erstellen", - "DtCplA": "{numParticipants, plural, =1 {# Teilnehmer} other {# Teilnehmer}}", - "DSVJjB": "Aktuell läuft das {playbookTitle} Playbook", - "D55vrs": "Ihre Lizenz konnte nicht generiert werden", - "D2CE02": "Webhook eingeben", - "CyGaem": "Durchlaufname", - "Cy1AK/": "Details zum Durchlauf anzeigen", - "CkYhdY": "Fügen Sie den Kanal zu einer Seitenleistenkategorie hinzu", - "CSts8B": "Team Symbol", - "CBM4vh": "Timer für das nächste Update", - "C9NScU": "Übertragen Sie Ihrem Team die Kontrolle", - "C1khRR": "Zurück zu Playbooks", - "BQtd5I": "Willkommen bei Playbooks!", - "BNB75h": "Ein Playbook definiert Checklisten, Automatisierungen und Vorlagen für wiederholbare Abläufe. {br} Es hilft Teams, Fehler zu vermeiden, Vertrauen bei den Beteiligten zu gewinnen und mit jedem Durchlauf effektiver zu werden.", - "B487HA": "In Bearbeitung", - "Auj1ap": "Test starten oder Abonnement aktualisieren.", - "ApULhK": "Teilnehmer einladen", - "A8dbCS": "Playbook nicht gefunden", - "A21Mgv": "Lauf beendet", - "9tBhzB": "Jetzt aktualisieren", - "9qc7BX": "Schlummern", - "9kCT7Q": "Erleichtern Sie Retrospektiven mit einer Zeitleiste, die automatisch die wichtigsten Ereignisse und Botschaften festhält, so dass Teams sie sofort zur Hand haben.", - "5j6GD/": "{numParticipants, plural, =0 {Kein Teilnehmer} =1 {# Teilnehmer} other {# Teilnehmer}}", - "wX3k9U": "Unbenanntes Playbook", - "l7zMH6": "Wählen Sie eine Option oder definieren Sie eine eigene Laufzeit", - "l0hFoB": "Playbook Beschreibung hinzufügen...", - "eLeFE2": "Name und Beschreibung bearbeiten", - "dvhvum": "(Optional) Beschreiben Sie, wie dieses Playbook genutzt werden soll", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {Durchlauf} other {Durchläufe}} in Arbeit", - "IfxUgC": "Durchlauf-Zusammenfassung hinzufügen…", - "IOnm/Z": "Es ist keine Durchlauf-Zusammenfassung vorhanden.", - "YMrTRm": "Durchlauf-Zusammenfassung", - "Oo5sdB": "Playbook Name", - "E0LnBo": "Sie können eine Option auswählen oder eine individuelle Dauer (\"2 Wochen\", \"3 Tage 12 Stunden\", \"45 Minuten\",...) angeben", - "ZWtlyd": "Durchlauf wiederhergestellt durch {name}", - "zz6ObK": "Wiederherstellen", - "z3B83t": "Suche nach einem Playbook", - "ypIsVG": "Aufgabe wiederherstellen", - "wO6NOM": "Sind Sie sicher, dass Sie diese Aufgabe wiederherstellen möchten? Diese Aufgabe wird zu diesem Lauf hinzugefügt", - "vjzpnC": "Es gibt keine Playbooks, die diesen Filtern entsprechen.", - "hrgo+E": "Archivieren", - "dSC1YD": "Aufgabe überspringen", - "XXbWAU": "Wählen Sie diese Option aus, um automatisch Aktualisierungen zu erhalten, wenn dieses Playbook ausgeführt wird.", - "Vhnd2J": "Beschreibung umschalten", - "EQpfkS": "Beendet", - "7VTSeD": "Sind Sie sicher, dass Sie diese Aufgabe auslassen wollen? Dies wird aus diesem Lauf gestrichen, hat aber keine Auswirkungen auf das Playbook.", - "36GNZj": "Das Playbook {title} wurde erfolgreich archiviert.", - "0HT+Ib": "Archiviert", - "/4tOwT": "Überspringen", - "+Tmpup": "Sie erhalten automatisch Updates, wenn dieses Playbook ausgeführt wird.", - "h+e7G+": "Aufforderung zur Ausführung dieses Playbooks, wenn eine Nachricht {numKeywords, select, 1 {das Schlüsselwort} andere {eines oder mehrere davon}} enthält", - "fuDLDJ": "Kanal erstellen", - "cp7KUI": "Playbook", - "cPIKU2": "Gefolgt", - "UMoxP9": "Vorlage für den Kanalnamen (optional)", - "RO+BaS": "Link zum Lauf kopieren", - "NA7Cw1": "Link zum Playbook kopieren", - "C6Oghd": "Zusammenfassung bearbeiten", - "3MSGcL": "Der Kanalname ist nicht gültig.", - "0oL1zz": "Kopiert!", - "d4g2r8": "Gelöscht: {timestamp}", - "4vuNrq": "{duration} nach Start des Laufs", - "/gbqA6": "{duration} vor Beginn des Laufs", - "Mu2aDs": "Alle Mitglieder des Teams ({team}) haben Zugang.", - "O8o2lE": "Kanal zur Kategorie hinzufügen", - "q0cpUe": "Checkliste hinzufügen", - "nSFBC2": "+ Aufgabe hinzufügen", - "m/Q4ye": "Checkliste umbenennen", - "k1djnL": "Checkliste löschen", - "iXNbPf": "Umbenennen", - "X2K92H": "Name der Checkliste", - "WbsomC": "Retrospektive veröffentlichen", - "TxCTXQ": "Sind Sie sicher, dass Sie den Lauf beenden wollen?", - "QywYDe": "Markieren Sie den Lauf auch als beendet", - "MrJPOh": "Statusaktualisierungen aktivieren", - "D9IV7i": "Retrospektiven wurden für diesen Playbook-Lauf deaktiviert.", - "Ja1sVR": "Statusaktualisierungen wurden für diesen Playbook-Lauf deaktiviert.", - "I5NMJ8": "Mehr", - "D/wCS9": "Sind Sie sicher, dass Sie die Retrospektive veröffentlichen wollen?", - "5Ofkag": "Retrospektive ermöglichen", - "2563nT": "Bestätigen Sie das Beenden des Laufs", - "2/2yg+": "Hinzufügen", - "/ZsEUy": "Sind Sie sicher, dass Sie diese Checkliste löschen wollen? Sie wird aus diesem Lauf entfernt, hat aber keine Auswirkungen auf das Playbook.", - "kDcpd/": "{numKeywords, plural, other {# Schlüsselwörter}}", - "vaYTD+": "Es gibt {outstanding, plural, =1 {# ausstehende Aufgabe} other {# ausstehende Aufgaben}}. Sind Sie sicher, dass Sie den Lauf beenden wollen?", - "pK6+CW": "@{displayName} ist kein Mitglied des Kanals [{runName}]({overviewUrl}). Möchten Sie sie zu diesem Kanal hinzufügen? Er hat dann Zugriff auf den gesamten Nachrichtenverlauf.", - "iDMOiz": "KANALMITGLIEDER", - "JqKASQ": "@{displayName} zum Kanal hinzufügen", - "5ciuDD": "NICHT IM KANAL", - "Lo10yH": "Unbekannter Kanal", - "3Ls2m+": "Playbook-Mitglied", - "0tznw6": "In privates Playbook umwandeln", - "wylJpv": "Jeder in {team} kann dieses Playbook einsehen.", - "tVPYMu": "Playbook Administrator", - "sDKojV": "Playbook archivieren", - "ruJGqS": "Zugang zum Playbook", - "osuP6z": "Ziehen, um die Checkliste neu zu ordnen", - "o+ZEL3": "Veröffentlicht {timestamp}", - "lQT7iD": "Playbook erstellen", - "gGcNUr": "Sie haben keine Berechtigung", - "g0mp+I": "Bei der Umwandlung in ein privates Playbook bleiben die Mitgliedschaft und der Verlauf der Ausführung erhalten. Diese Änderung ist dauerhaft und kann nicht rückgängig gemacht werden. Sind Sie sicher, dass Sie {playbookTitle} in einen privaten Playbook konvertieren möchten?", - "SXJ98n": "Sie können die Retrospektive nach der Veröffentlichung nicht mehr bearbeiten. Möchten Sie die Retrospektive veröffentlichen?", - "R/2lqw": "Vorlage auswählen", - "MJ89uW": "In privates Playbook umwandeln", - "HLn43R": "Zugang verwalten", - "EvBQLq": "Playbook Admin definieren", - "EWz2w5": "Playbook ausführen", - "8oCVbz": "Sind Sie sicher, dass Sie veröffentlichen wollen", - "5BUxvl": "Jeder in diesem Team kann dieses Playbook einsehen.", - "QpUBDr": "{members, plural, =0 {Keiner kann} =1 {Eine Person kann} other {# Personen können}} auf dieses Playbook zugreifen.", - "0Vvpht": "Zum Playbook Teilnehmer machen", - "qsr3Zk": "Aktualisiere die Durchlauf-Zusammenfassung", - "0q+hj2": "Definiere eine Vorlage für eine übersichtliche Beschreibung, die jeden Lauf den Stakeholdern erklärt.", - "FXCLuZ": "Summe {total, number}", - "3PoGhY": "Sind Sie sicher, dass Sie veröffentlichen wollen?", - "4fHiNl": "Duplizieren", - "4alprY": "Playbook Vorlagen", - "/urtZ8": "Ihre Playbooks", - "9XUYQt": "Importieren", - "SVwJTM": "Exportieren", - "xvBDOH": "Sind Sie sicher, dass Sie das Playbook {title} archivieren möchten?", - "lBqu4h": "Playbook wiederherstellen", - "bTgMQ2": "Dieses Playbook ist archiviert.", - "MTzF3S": "Sind Sie sicher, dass Sie das Playbook {title} wiederherstellen möchten?", - "4cwL43": "Mit archivierten", - "4aupaG": "Das Playbook {title} wurde erfolgreich wiederhergestellt.", - "6D6ffM": "Bitte geben Sie eine Dauer im Format: dd:hh:mm (z.B. 25:19:30 25 Tage 19 Stunden 30 Minuten), oder lassen Sie das Feld leer.", - "y7o4Rn": "Sind Sie sicher, dass Sie löschen wollen?", - "uT4ebt": "z.B. Ressourcenanzahl, betroffene Kunden", - "tbjmvS": "Eine Metrik mit diesem Name existiert bereits. Bitte verwenden Sie einen eindeutigen Namen für jede Metrik.", - "rzbYbE": "Ziel", - "rMhrJH": "Bitte geben Sie einen Namen für Ihre Metrik an.", - "q/Qo8l": "Private Playbooks sind nur in Mattermost Enterprise verfügbar", - "mbo96h": "Konfigurieren Sie eigene Metriken, die mit der Retrospektive ausgefüllt werden", - "mVpO8u": "Schon mal gesehen?", - "gsMPAS": "Dollar", - "f+bqgK": "Name der Metrik", - "a0hBZ0": "Metrik löschen", - "XpDetT": "Diese Tipps nicht mehr anzeigen.", - "VZRWFk": "z.B. Kosten, Einkäufe", - "TxmjKI": "Beschreibe was diese Metrik aussagt", - "Sx3lHL": "Ganzzahl", - "OyZnsJ": "pro Durchlauf", - "NYTGIb": "Verstanden", - "NJ9uPu": "Schlüsselmetriken", - "LI7YlB": "Fügen Sie Details über was diese Metrik aussagt und wie sie gefüllt werden soll, hinzu. Diese Beschreibung wird auf der Retrospektive-Seite für jeden Durchlauf vorhanden sein, wo die Werte für diese Metrik eingegeben werden.", - "LDYFkN": "Dauer (in dd:hh:mm)", - "JrZ2th": "Metrik hinzufügen", - "FGzxgY": "z.B. Bestätigungszeit, Lösungszeit", - "F4pfM/": "Bitte geben Sie eine Zahl ein oder lassen Sie das Feld leer.", - "9SIW2x": "Zielwert für jeden Durchlauf", - "4BN53Q": "Wir werden zeigen, wie nah oder weit entfernt vom Ziel der Wert jedes Durchlaufs ist und dies auch in einer Grafik darstellen.", - "1ikfp3": "Wenn Sie diese Metrik löschen, werden für diese keine Werte bei kommenden Durchläufen gesammelt.", - "0Xt1ea": "Sie werden weiterhin auf Verlaufsdaten für diese Metrik zugreifen können.", - "wbdGb5": "Weisen Sie Aufgaben zu, haken Sie sie ab oder überspringen Sie sie, um sicherzustellen, dass das Team sich darüber im Klaren ist, wie es gemeinsam auf die Ziellinie zusteuert.", - "wPVxBN": "Klicken Sie auf \"Bearbeiten\", um die Vorlage an Ihre eigenen Modelle und Prozesse anzupassen. Sie können die Vorlage auf dieser Seite im Detail erkunden.", - "vQqT/8": "Wählen Sie Bearbeiten, um mit der Anpassung an Ihre eigenen Modelle und Prozesse zu beginnen. Sie können die Vorlage auf dieser Seite im Detail erkunden.", - "vL4++D": "Verfolgung von Fortschritt und Verantwortung", - "vJ2SaW": "Automatisieren Sie Aspekte Ihres Playbooks, wie das Senden einer Willkommensnachricht, das Einladen wichtiger Mitglieder und das Erstellen eines Aktualisierungskanals.", - "udrLSP": "Nutzen Sie Metriken, um Muster und Fortschritte in verschiedenen Läufen zu verstehen und die Leistung zu verfolgen.", - "q/VD+s": "Legen Sie Zeitpläne fest und erstellen Sie eine Vorlage für Statusaktualisierungen, damit die Beteiligten immer auf dem neuesten Stand der Entwicklungen sind.", - "lgZf0l": "Mit Playbooks starten", - "lUfDe1": "Exportieren Sie den Playbook-Durchlaufkanal und speichern Sie ihn zur späteren Analyse.", - "hw83pa": "Verfolgen Sie wichtige Kennzahlen und messen Sie Werte", - "fhMaTZ": "Nehmen Sie eine kurze Einführungstour", - "dxyZg3": "Lassen Sie mich selbst erkunden", - "dZmYk6": "Playbook erfolgreich dupliziert", - "cEWBE3": "Bewerten Sie Ihre Prozesse mithilfe einer Retrospektive, um sie bei jedem Durchlauf zu verfeinern und zu verbessern.", - "ZkhArX": "Auf geht's!", - "Tt04f1": "Sehen Sie, wer beteiligt ist und was zu tun ist, ohne die Unterhaltung zu verlassen.", - "RzEVnf": "Playbooks machen wichtige Abläufe wiederholbar und nachvollziehbar. Ein Playbook kann mehrfach ausgeführt werden, und jeder Durchlauf hat seine eigene Aufzeichnung und Rückschau.", - "R5Zh+l": "Auf diese Weise können Sie zunächst ein Beispiel-Playbook ausprobieren, bevor Sie Zeit in die Erstellung Ihres eigenen Playbooks investieren.", - "QbGfqo": "Mit nur einer Nachricht können Sie sich an mehrere Beteiligte wenden und für die Rückschau ein Protokoll führen.", - "Q5hysF": "Machen Sie mehr mit Playbooks", - "Q3R9Uj": "Dokumentieren Sie die Schritte für den gesamten Prozess hier. Weisen Sie jede Aufgabe Verantwortlichen zu und fügen Sie optional Zeitpläne oder verknüpfte Aktionen hinzu.", - "Pue+oV": "Starte das Playbook um es in Aktion zu sehen", - "I5DYM+": "Lerne UND Reflektiere", - "HXvk56": "Sende Status Aktualisierungen", - "HGdWwZ": "Erstellen oder weise Aufgaben zu", - "GjCS6U": "Wählen Sie eine Vorlage", - "GG1yhI": "Es gibt Vorlagen für eine Vielzahl von Anwendungsfällen und Ereignissen. Sie können ein Playbook so verwenden wie es ist, oder auf Ihre Bedürfnisse anpassen, und es dann mit Ihrem Team teilen.", - "GAuN6w": "Annahmen festlegen", - "9m0I/B": "Halten Sie Interessenvertreter auf dem Laufenden", - "8n24G2": "Betrachten Sie Durchlaufdetails in einer Seitenbereich", - "6GTzTR": "Sehen Sie zu jeder Zeit was in diesem Playbook ist", - "1isgPF": "Wir haben den ersten Durchlauf automatisch erstellt", - "1QosTr": "Benutzt von", - "0EEIkR": "Glückwunsch! Sie haben das erste Playbook aus einer Vorlage erstellt!", - "/fU9y/": "Sie können unterschiedliche Abschnitte dieses Playbooks in den Details dieser Seite prüfen.", - "xVyHgP": "Starte einen Testlauf", - "ru+JCk": "Durchschnitt", - "mvZUm3": "Hier kannst Du die Playbook-Komponenten im Detail erforschen. Wähle Bearbeiten um Dein Playbook an Deine Prozesse und Modelle anzupassen.", - "lbs7UO": "pro Durchlauf über die letzten 10 Durchläufe", - "l5/RKZ": "Es gibt keine abgeschlossenen Durchläufe für dieses Playbook.", - "fmbSyg": "Wert hinzufügen (dd:hh:mm)", - "efeNi1": "Durchschnittswert 10 Durchläufe", - "awG90C": "Ziele pro Durchlauf", - "ZNNjWw": "Bitte gib eine Zahl ein.", - "Vf/QlZ": "Wertebereich", - "NiAH1z": "Zielwert", - "NMxVd+": "Bitte gib einen metrischen Wert ein.", - "NLeFGn": "bis", - "M4gAc9": "Wert hinzufügen", - "KXVV4+": "Willkommen zur Playbook Vorschauseite!", - "9a9+ww": "Titel", - "69nlA3": "Bitte gib eine Zeitdauer im Format: dd:hh:mm (12:00:00) ein.", - "u7qh13": "Bist Du bereit, dein Playbook zu starten?", - "p1I/Fx": "Wir haben deinen Lauf automatisch erstellt", - "c23IHq": "Kanalaktionen ermöglichen Dir die Automatisierung von Aktivitäten für diesen Kanal", - "ao44YC": "Metriken konfigurieren", - "Y4MU/9": "Wähle Starte einen Testlauf, um das Playbook in Aktion zu sehen.", - "RUlvbf": "Probiere dein neues Playbook aus!", - "MHzP9I": "Definiere eine Nachricht zur Begrüßung von Benutzern, die dem Kanal beitreten.", - "MBNMo9": "Kanal-Aktionen", - "DPj6DM": "Wählen Sie Lauf, um es in Aktion zu sehen.", - "B3Q5mz": "Auslöser", - "5AJmOz": "Wenn ein Benutzer dem Kanal beitritt", - "0RlzlZ": "Senden einer temporären Willkommensnachricht an den Benutzer", - "hCMWC+": "Starte Folgen für {folgende, plural, =0 {keinen Benutzer} =1 {einen Benutzer} other {# Benutzer}}", - "u4L4yd": "Du hast ungesicherte Änderungen", - "e3z3P8": "Änderungen verwerfen", - "Ob5cSv": "Änderungen, die du vorgenommen hast, werden nicht gespeichert, wenn du diese Seite verläss. Bist du sicher, dass du die Änderungen verwerfen willst?", - "Ek1Fx2": "Wenn eine Nachricht mit diesen Schlüsselwörtern gesendet wird", - "2Q5PhZ": "Ausführen eines Playbooks bestätigen", - "+/x2FM": "Wähle ein Playbook aus", - "dCtjdj": "Bereit dein Playbook zu starten?", - "Z3ybv/": "Kanal zu einer Seitenleisten-Kategorie des Benutzers hinzufügen", - "9j5KzL": "Kategorienamen eingeben", - "+PMJAg": "Starte folgendes für {followers, plural, =1 {einen Benutzer} other {# Benutzer}}", - "aEhjYg": "Übersicht", - "Ppx673": "Berichte", - "zWgbGg": "Heute", - "mLrh+0": "Kein Fälligkeitsdatum", - "iMjjOH": "Nächste Woche", - "MtrTNy": "Morgen", - "I7+d55": "Definiere Datum/Zeit (\"in 4 Stunden\",\"1. Mai\"...)", - "AF7+5o": "Füge Fälligkeitsdatum hinzu", - "MbapTE": "{num} {num, plural, =1 {Aufgabe} other {Aufgaben}} überfällig", - "UlJJ1i": "Slash-Kommando hinzufügen", - "mw9jVA": "Titel hinzufügen", - "lyXljU": "Aufgabe kopieren", - "lglICE": "Beschreibung hinzufügen (optional)", - "W0aij2": "Zuweisen an...", - "oBeKB4": "Fällig am {date}", - "lkv547": "Fälligkeitsdatum (Verfügbar im Professional Plan)", - "g9pEhE": "Fällig", - "TTIQ6E": "Gibt Aufgaben ein Fälligkeitsdatum, damit Bearbeiter priorisieren und Dinge erledigen können.", - "NFyWnZ": "Arbeite effektiver", - "371AC3": "Aktualisiere die Durchlauf-Zusammenfassung", - "Xgxruo": "Checkliste überspringen", - "7P5T3W": "Checkliste wieder herstellen", - "oAJsne": "Öffentliches Playbook", - "mm5vL8": "Neu eingeladene Mitglieder", - "lJ48wN": "Privates Playbook", - "RQl8IW": "Schlummern für…", - "9trZXa": "Jeder im Team kann lesen", - "OqCzNb": "Aufgabe hinzufügen", - "JcefuP": "Beschreibung hinzufügen (optional)", - "v5/Cox": "Checkliste kopieren", - "mCrdeS": "Summe Playbook Durchläufe", - "IxtSML": "Checkliste hinzufügen", - "CwwzAU": "Checklistennamen hinzufügen", - "4GjZsL": "Summe Playbooks", - "cyR7Kh": "Zurück", - "XF8rrh": "Link kopieren zu ''{name}''", - "MyIJbr": "Inhalt", - "5ZIN3u": "Status-Updates", - "k12r+v": "Vorlage für Laufzusammenfassung hinzufügen...", - "RrCui3": "Zusammenfassung", - "x1phlu": "Kein Zeitrahmen", - "kYCbJE": "Zeitrahmen hinzufügen", - "HvAcYh": "{text}{rest, plural, =0 {} one { und andere} other { und {rest} andere}}", - "xHNF7i": "Starte Aktionen", - "uhDKO8": "Verwende Markdown um eine Vorlage zu erstellen", - "sX5Mn5": "Bitte einen Webhook pro Zeile eingeben", - "mkLeuq": "Sende Aktualisierungen an ausgewählte Kanäle", - "kkw4kS": "Diese Aktualisierung wird an {hasChannels, select, true {{broadcastChannelCount, plural, =1 {einen Kanal} other {{broadcastChannelCount, number} Kanäle}}} other {}}{hasFollowersAndChannels, select, true { und } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {eine Direktnachricht} other {{followersChannelCount, number} Direktnachrichten}}} other {}}gesendet.", - "kV5GkX": "Wenn eine Statusaktualisierung geposted wird", - "j940pJ": "Diese Aktualisierung wird in der Übersicht gespeichert.", - "giM/X9": "Eine Statusaktualisierung wird alle erwartet. Neue Aktualisierungen werden in {channelCount, plural, =0 {keinen Kanal} one {# Kanal} other {# Kanäle}} und {webhookCount, plural, =0 {keinen ausgehenden Webhook} one {# ausgehenden Webhook} other {# ausgehende Webhooks}} gepostet.", - "aM44Z/": "Wähle oder setzte eine eigene Dauer…", - "YQOmSf": "Gib einen Webhook pro Zeile ein", - "XRyRzf": "Statusaktualisierungen werden nicht erwartet.", - "F9LrJA": "Einträge filtern", - "DaHpK1": "Suche nach einem Kanal", - "28FTjr": "Starte Aktionen, die Aktivitäten in diesem Kanal automatisieren", - "/RnCQb": "Sende ausgehenden Webhook", - "OuZhcQ": "Dauer angeben (\"8 Stunden\", \"3 Tage\"...)", - "zl6378": "Konfiguriere Metriken in der Retrospektive", - "aZGAOI": "Füge eine Vorlage für Status-Aktualisierungen hinzu…", - "OKhRC6": "Teilen", - "LcC/pi": "Sende eine Willkommen-Nachricht…", - "Brya9X": "Füge eine Vorlage für eine Durchlauf-Zusammenfassung hinzu…", - "9kQNdp": "Dieses Playbook ist privat.", - "3hBelc": "Eine Retrospektive wird nicht erwartet.", - "yllba1": "Dieses archivierte Playbook kann nicht umbenannt werden.", - "TD8WrM": "Duplizieren ist für dieses Team deaktiviert.", - "OQplDX": "Eine Status-Aktualisierung wird alle erwartet. Neue Updates werden in {channelCount, plural, =0 {keinen Kanal} one {einen Kanal} other {# Kanäle}} und {webhookCount, plural, =0 {keinen ausgehenden Webhook} one {einen ausgehenden Webhook} other {# ausgehende Webhooks}} gesendet.", - "xEQYo5": "Konfiguriere eigene Metriken um die Retrospektive zu füllen.", - "oL7YsP": "Zuletzt bearbeitet {timestamp}", - "Z2Hfu4": "Erstelle eine Zusammenfassung des Durchlaufs", - "vSMfYU": "Durchlauf Info", - "iigkp8": "Zeit zum Aufräumen?", - "hjteuA": "Alle Playbooks, auf die du Zugriff hast, werden hier angezeigt", - "ZJS10z": "Keine Aktualisierungen wurden bisher gesendet", - "Q15rLN": "Aktualisierung anfordern...", - "GDCpPr": "Letzte Statusaktualisierung", - "+qDKgW": "Zeige alle Aktualisierungen", - "opn6uf": "Zeige Zeitleiste", - "o6N9pU": "Starte Aktionen", - "lbr3Lq": "Kopiere Link", - "bf5rs0": "Zeige Info", - "kEMvwX": "Es gibt keine Durchläufe auf welche die Filter zutreffen.", - "GXjP8g": "Alle Durchläufe auf die du Zugriff hast, werden hier angezeigt", - "ocYb9S": "Schlüssel-Metriken", - "nc8QpJ": "Letzte Aktivitäten", - "m/KtHt": "Du hast keine Berechtigung den Besitzer zu ändern", - "RnOiCg": "Es war nicht möglich dem Durchlauf {isFollowing, select, true {nicht mehr zu folgen} other {zu folgen}}", - "4mCpAv": "Der Besitzer konnte nicht geändert werden", - "lr1CUA": "Durchsuche Playbooks", - "Ul0aFX": "Importiere Playbook", - "LfhTNW": "Durchsuche oder erstelle Playbooks und Durchläufe", - "GVpA4Q": "Erstelle neues Playbook", - "CFysvS": "Erstelle Playbook Dropdown", - "/qDObA": "Durchläufe durchsuchen", - "/+8SGX": "Zeige {filteredNum} von {totalNum} Ereignissen", - "Xx0WZV": "Sende Nachricht", - "UePrSL": "{num} {num, plural, one {Teilnehmer} other {Teilnehmer}}", - "UMFnWV": "Retrospektive anzeigen", - "9xs0pp": "Wert hinzufügen...", - "jboo9u": "Aktualisierung anfordern", - "P9PKvb": "Ein Nachricht wurde an den Durchlauf-Kanal gesendet.", - "NGqzDU": "Aktualisierungsanfrage bestätigen", - "JvEwg/": "Es war nicht möglich eine Aktualisierung anzufordern", - "Jli9m7": "Eine Nachricht wird an den Durchlauf-Kanal gesendet mit der Aufforderung eine Aktualisierung zu senden.", - "VpQKQE": "{displayName} sind keine Teilnehmer dieses Durchlaufs. Möchtest du sie zu Teilnehmern machen? Sei werden Zugriff auf den kompletten Nachrichtenverlauf im Durchlauf-Kanal haben.", - "RCT0Px": "Füge {displayName} zum Kanal hinzu", - "KeO51o": "Kanal", - "lKeJ+i": "Es gibt keine Zusammenfassung", - "pFK6bJ": "Zeige alle", - "hIWK05": "Eine Nachricht wird an den Durchlaufkanal gesendet, um dich als Beteiligten hinzuzufügen.", - "U8u4uF": "Beteiligt werden", - "J2NmIY": "Bestätige um beteiligt zu werden", - "MD6oav": "Es war nicht möglich, eine Anfrage auf Beteiligung zu stellen", - "3O8M5M": "Anfrage wurde an den Durchlaufkanal gesendet.", - "zW/5AB": "Professional Funktion Dies ist eine kostenpflichtige Funktion, die mit einem kostenlosen 30-Tage Test verfügbar ist", - "vDvWJ6": "Teste Aktualisierung anfordern mit einem kostenlosen Test", - "ch4Vs1": "Fordere Aktualisierungen für Durchläufe mit einem einzelnen Klick an und werde direkt benachrichtigt, wenn eine Aktualisierung gesendet wird. Starte einen kostenlosen 30-Tage Test um es auszuprobieren.", - "PdRg+3": "Zeige alle...", - "P6NEL/": "Befehl...", - "1fXVVz": "Fälligkeitsdatum...", - "1GOpgL": "Bearbeiter...", - "u6Fyic": "Deine Anfrage wurde an den Durchlaufkanal gesendet.", - "pzTOmv": "Verfolger", - "pXWclp": "Deine Teilnahme-Anfrage wird an den Durchlaufkanal gesendet.", - "Nf9oAA": "Du bist dabei an diesem Durchlauf teilzunehmen.", - "5PpBsd": "Deine Anfrage war nicht erfolgreich.", - "4Iqlfe": "Du nimmst an diesem Durchlauf teil.", - "wGp7l3": "{icon} Dollar", - "s+rSpl": "{icon} Ganzzahl", - "qp5G0Z": "Upgrade benötigt für Zugriff auf die Retrospektive-Funktionen.", - "ojQue/": "{icon} Dauer (in dd:hh:mm)", - "mNgqXf": "Zum Freischalten dieser Funktion:", - "j2VYGA": "Zeige alle Playbooks", - "SMrXWc": "Favoriten", - "PWmZrW": "Zeige alle Durchläufe", - "PW+sL4": "N.V.", - "KzHQCQ": "Es gibt keine beendeten Durchläufe, die auf diese Filter passen.", - "CUhlqp": "Tutorial Tour Tipp Produktbild", - "5HXkY/": "Typ: {typeTitle}", - "3zF589": "Zurücksetzen auf alle {filterName}", - "xfnuXm": "Teilnehmen", - "wRM2AO": "Die Aktualisierungsanfrage war nicht erfolgreich.", - "ePhhuK": "Deine Anfrage wurde an den Durchlaufkanal gesendet.", - "b+DwLA": "Anfrage zur Teilnahme an diesem Durchlauf.", - "PoX2HN": "Sende Anfrage", - "OfN7IN": "Eine Statusaktualisierungsanfrage wird in den Durchlaufkanal gesendet.", - "Gwmqz5": "Frage eine Aktualisierung an", - "CV1ddt": "Nimm am Durchlauf teil", - "B9z0uZ": "Deine Anfrage zur Teilnahme am Durchlauf war nicht erfolgreich.", - "AH+V3r": "Werde ein Teilnehmer des Durchlaufs.", - "+6DCr9": "Als Teilnehmer kannst du Statusaktualisierungen senden, Aufgaben zuweisen und abschliessen und Retrospektiven durchführen.", - "wBZz47": "Du hast den Durchlauf verlassen.", - "gfUBRi": "Weise einen neuen Besitzer zu bevor du den Durchlauf verläßt.", - "fnihsY": "Verlassen", - "a1vQ5Q": "Verlassen bestätigen", - "SK5APX": "Es war nicht möglich den Durchlauf zu verlassen.", - "N9CTUJ": "Durchlauf verlassen", - "F/HKIy": "Bist du sicher, dass die den Durchlauf verlassen möchtest?", - "Mjq//Y": "Nicht favorisieren", - "5Hzwqs": "Favorisieren", - "XS4umx": "{name} hat ein Statusupdate verschlafen", - "mttASm": "Verlassen und Durchlauf nicht mehr folgen", - "lpWBJE": "Bestätige verlassen und nicht mehr folgen", - "hnYSP3": "Wenn du einen Durchlauf verläßt und nicht mehr folgst, wird er aus der linken Randleiste entfernt. Du kannst ihn über die Anzeige aller Durchläufe wiederfinden.", - "AhY0vJ": "Verlassen und nicht mehr folgen", - "egUE/K": "Senden an ausgewählte Kanäle", - "Xm0L7N": "Wenn eine Statusaktualisierung gesendet oder eine Retrospektive veröffentlicht wird", - "iEtImk": "Wenn du einen Durchlauf verlässt{isFollowing, select, true { und nicht mehr folgst} other { }}, wird er aus der linken Seitenleiste entfernt. Du findest ihn weiterhin, wenn du alle Durchläufe ansiehst.", - "cnfVhV": "Verlassen {isFollowing, select, true { und nicht folgen } other {}}run", - "Q4sutg": "Bestätige verlassen{isFollowing, select, true { und nicht mehr folgen} other {}}", - "Suyx6A": "Der Playbook Import ist fehlgeschlagen. Bitte prüfe, dass das JSON valide ist und nochmal versuchen.", - "QegBKq": "Playbook beitreten", - "P6PLpi": "Teilnehmen", - "FgydNe": "Ansehen", - "qGlwfc": "Starte Durchlauf", - "j2FnDV": "Es wird ein Kanal mit diesem Namen erstellt", - "iQhFxR": "Zuletzt verwendet", - "03oqA2": "Aktive Durchläufe", - "KjNfA8": "Ungültiger Zeitraum", - "k5EChD": "Bist du sicher, dass du den Durchlauf neu starten möchtest?", - "vqmRBs": "Bestätige Neustart Durchlauf", - "Zg0obP": "Durchlauf erneut starten", - "XnICdK": "Es war nicht möglich am Durchlauf teilzunehmen", - "unwVil": "Die Anfrage um dem Kanal beizutreten war nicht erfolgreich.", - "ZRv7Dm": "Antrag auf Beitritt", - "M9tXoZ": "Es wird eine Beitrittsanfrage an den Durchlaufkanal gesendet.", - "0QD99o": "Beitritt zum Kanal anfragen", - "q48ca7": "Gib Feedback zu Playbooks.", - "bCmvTY": "Feedback geben", - "fVMECF": "Teilnehmer", - "FLG4Iu": "Zum Besitzer des Durchlaufs hochstufen", - "6rygzu": "Aus dem Durchlauf entfernen", - "0Azlrb": "Verwalten", - "/GCoTA": "Leeren", - "w4Nhhb": "Teilnehmer hinzufügen", - "cUCiWw": "Teilnehmer werden", - "1OVPiC": "Werde Teilnehmer des Laufs. Als Teilnehmer kannst du Statusaktualisierungen senden, Aufgaben zuweisen und abschliessen und Retrospektiven durchführen.", - "jrOlPO": "Erhalte Statusbenachrichtigungen des Durchlaufs", - "utHl3F": "Personen hinzufügen zu {runName}", - "l/W5n7": "Die Teilnehmer werden auch zu dem mit diesem Durchlauf verbundenen Kanal hinzugefügt", - "WC+NOj": "Füge auch Personen zu dem mit diesem Durchlauf verknüpften Kanal hinzu", - "1prgB2": "Mitglieder suchen", - "wCDmf3": "Updates aktivieren", - "nsd54s": "Bestätige die Deaktivierung von Statusaktualisierungen", - "cpGAhx": "Bist du sicher, dass du die Statusaktualisierung für diesen Durchlauf deaktivieren möchtest?", - "WFA0Cg": "Bist du sicher, dass du die Statusaktualisierung für diesen Durchlauf aktivieren möchtest?", - "H7IzRB": "Statusaktualisierungen deaktivieren", - "1OluNs": "Bestätige die Aktivierung von Statusaktualisierungen", - "//o1Nu": "Updates deaktivieren", - "jAo8dd": "Statusaktualisierungen ausführen, die durch {name} deaktiviert wurden", - "b8Gps8": "Statusaktualisierungen ausführen, die durch {name} aktiviert wurden", - "9qqGGd": "Teilnehmer einladen", - "qDxsQH": "Werde ein Teilnehmer, um an diesem Durchlauf teilzunehmen", - "lqzBNa": "Entferne sie aus dem Kanal für den Durchlauf", - "ieL3dC": "Kanalaktionen einrichten", - "ha1TB3": "Wenn ein Teilnehmer an dem Durchlauf teilnimmt", - "Z18I+c": "Kanalaktionen ermöglichen dir die Automatisierung von Aktivitäten für den Kanal", - "Y1EoT/": "Wenn ein Teilnehmer den Durchlauf verlässt", - "5b1zuB": "Füge sie dem Kanal für den Durchlauf hinzu", - "u/yGzS": "{name} hat @{user} zum Durchlauf hinzugefügt", - "t6lwwM": "{requester} entfernt {users} aus dem Durchlauf", - "jfpnye": "@{user} hat den Durchlauf verlassen", - "feNxoJ": "{requester} hat {users} zum Durchlauf hinzugefügt", - "ecS/qx": "{name} hat {num} Teilnehmer dem Durchlauf hinzugefügt", - "VM75su": "{name} hat {num} Teilnehmer aus dem Durchlauf entfernt", - "SwlL5j": "@{user} nimmt am Durchlauf teil", - "RXjd3Q": "{name} entfernte @{user} aus dem Durchlauf", - "zSOvI0": "Filter", - "qxYWTy": "Alle Aufgaben aus meinen eigenen Durchläufen anzeigen", - "grv9Fm": "Wählen, um eine Aufgabenliste umzuschalten.", - "YBvwXR": "Keine zugewiesenen Aufgaben", - "WFd88+": "Geprüfte Aufgaben anzeigen", - "TnUG7m": "Du hast keine ausstehende Aufgabe zugewiesen bekommen.", - "SRqpbI": "{assignedNum, plural, =0 {Keine zugewiesenen Aufgaben} other {# zugewiesen}}", - "I0NIMp": "Deine Aufgaben", - "DUU48k": "Es gibt keine Aufgabe, die dir explizit zugewiesen ist. Du kannst deine Suche mit Hilfe der Filter erweitern.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# überfällig}}", - "meD+1Q": "DURCHLAUF TEILNEHMER", - "L6vn9U": "Teilnehmer am Durchlauf", - "Gg/nch": "NICHT TEILNEHMEND", - "36NwLv": "Verwalte Teilnehmerliste des Durchlaufs", - "iH5e4J": "Du wirst auch zu dem mit diesem Durchlauf verbundenen Kanal hinzugefügt.", - "UAS7Bn": "Zugriff auf den mit diesem Durchlauf verbundenen Kanal anfordern", - "NGKqOC": "Füge mich auch zu dem mit diesem Durchlauf verknüpften Kanal hinzu", - "BJNrYQ": "Als Teilnehmer kannst du die Zusammenfassung des Laufs aktualisieren, Aufgaben abhaken, Status-Updates posten und die Retrospektive bearbeiten.", - "fBG/Ge": "Kosten", - "VjJYEV": "z.B. Verkaufsauswirkungen, Einkäufe", - "9X3jwi": "{icon} Kosten", - "dK2JKl": "Mit einem bestehenden Kanal verknüpfen", - "IdTL+v": "Einen Durchlauf-Kanal erstellen", - "2BCWLD": "Kanal konfigurieren", - "lqceIp": "oder Playbook importieren", - "ORJ0Hb": "Es gibt {outstanding, plural, =1 {# ausstehende Aufgabe} other {# ausstehende Aufgaben}}. Bist du sicher, dass du den Lauf beenden willst?", - "0boT49": "Bist du sicher, dass du den Durchlauf für alle Teilnehmer beenden möchtest?", - "AG7PKJ": "Durchlauf umbenennen", - "a2r7Vb": "Privater Kanal", - "VA1Q/S": "Öffentlicher Kanal", - "zxj2Gh": "Zuletzt aktualisiert {time}", - "yP3Ud4": "Es sind keine aktiven Durchläufe mit diesem Kanal verknüpft", - "tqAmbk": "Aktive Durchläufe", - "Z1sgPO": "Beendete Durchläufe ansehen", - "RgQwWr": "Durchläufe sortieren nach", - "RC6rA2": "Kürzlich erstellt", - "Q/t0//": "Beendete Durchläufe", - "NNksk4": "Alphabetisch", - "AoNLta": "Es sind keine fertigen Durchläufe mit diesem Kanal verknüpft", - "2NDgJq": "Letzte Statusaktualisierung", - "prs4kX": "Wenn eine Nachricht mit bestimmten Schlüsselwörtern gesendet wird", - "m8hzTK": "Zuletzt verwendet {time}", - "kQAf2d": "Auswählen", - "gS1i4/": "Markiere die Aufgabe als erledigt", - "gGtlrk": "Deine Playbooks", - "fvNMLo": "Aufgaben-Aktionen", - "cGCoJe": "Gesendet von", - "Wy3sw+": "{count, plural, =1{1 Durchlauf aktiv} =0 {kein Durchlauf aktiv} other {# Aktive Durchläufe}}", - "W1EKh5": "Neues Playbook erstellen", - "SRbTcY": "Andere Playbooks", - "L1tFef": "Prüfe die Schreibweise oder probieren eine andere Suche", - "KQunC7": "In diesem Kanal verwendet", - "HfjhwE": "Playbooks suchen", - "GZoWl1": "Aktivitäten für diese Aufgabe automatisieren", - "EVSn9A": "Starte Durchlauf", - "9AQ5FE": "Durchlauf-Zusammenfassung", - "95v+5O": "{actions, plural, =0 {Aufgaben-Aktion} one {# Aktion} other {# Aktionen}}", - "7KMbBa": "Nie benutzt", - "3sXVwy": "Aufgaben-Aktionen...", - "3Yvt4d": "Playbooks sind konfigurierbare Checklisten, die einen wiederholbaren Prozess für Teams definieren, um bestimmte und vorhersehbare Ergebnisse zu erzielen", - "0CeyUV": "Keine Ergebnisse für \"{searchTerm}\"", - "zscc/+": "Es gibt {outstanding, plural, =1 {# ausstehende Aufgabe} other {# ausstehende Aufgaben}}. Bist du sicher, dass du den Durchlauf {runName} beenden willst?", - "LKu0ex": "Bist du sicher, dass du den Durchlauf {runName} für alle Teilnehmer beenden möchtest?", - "bEoDyV": "@{authorUsername} hat ein Update für [{runName}]({overviewURL}) gepostet", - "ZSa3cf": "@{targetUsername}, bitte aktualisiere den Status von [{runName}]({playbookURL}).", - "Bgt0C8": "Diese Aktualisierung für den Durchlauf {runName} wird an {hasChannels, select, true {{broadcastChannelCount, plural, =1 {einen Kanal} other {{broadcastChannelCount, number} Kanäle}}} other {}}{hasFollowersAndChannels, select, true { und } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {eine Direktnachricht} other {{followersChannelCount, number} Direktnachrichten}}} other {}} gesendet.", - "uCS6py": "Du hast keine Erlaubnis, dieses Playbook zu sehen", - "l3QwVw": "Kanal auswählen", - "ksG35Q": "Du hast keine Berechtigung zum Erstellen von Playbooks in diesem Arbeitsbereich.", - "YKLHXL": "Aktive Durchläufe anzeigen", - "QvEO6m": "Du hast keine Berechtigung, diesen Durchlauf zu bearbeiten", - "QJTSaI": "Durchlauf mit einem anderen Kanal verknüpfen", - "BiQjuS": "Durchlauf verschoben nach {channel}", - "k7Nzfi": "Einladung deaktivieren", - "fwW0T1": "Bestätige das Entfernen eines vordefinierten Mitglieds", - "TP/O/b": "Benutzer entfernen", - "IE2BzH": "Es gibt Benutzer, die bereits vorab einer oder mehreren Aufgaben zugewiesen sind. Wenn du die Einladungen deaktivierst, werden alle Vorabzuweisungen gelöscht.{br}{br}Bist du sicher, dass du Einladungen deaktivieren möchtest?", - "DQn9Uj": "Der Benutzer {name} ist bereits vorab einer oder mehreren Aufgaben zugewiesen. Wenn du diesen Benutzer nicht automatisch einlädst, werden seine Vorab-Zuweisungen gelöscht.{br}{br}Bist du sicher, dass du diesen Benutzer nicht mehr als Mitglied des Durchlaufs einladen möchtest?", - "9w0mDI": "Bestätige das Entfernen eines vordefinierten Mitglieds", - "mILd++": "Der Durchlaufname darf nicht mehr als {maxLength} Zeichen enthalten", - "uYrkxy": "Die Datei muss ein gültiges JSON-Playbook Template sein.", - "m4vqJl": "Dateien", - "Zbk+OU": "Die Dateigröße überschreitet die Grenze von 5 MB.", - "MieztS": "Lege eine Playbook-Exportdatei ab um sie zu importieren.", - "HGSVzc": "Es können nicht mehrere Dateien auf einmal importiert werden.", - "LaseGE": "Du hast keine Berechtigung, diese Checkliste zu bearbeiten", - "Edy3wX": "Checkliste verschoben nach {channel}", - "8//+Yb": "Checkliste mit einem anderen Kanal verknüpfen", - "706Soh": "erledigte Aufgaben", - "XHJUSG": "Durchläufen automatisch folgen", - "DqTQOp": "Einmalig", - "vjb+hS": "{user} hat den Checklistenpunkt \"{name}\" wiederhergestellt", - "OqWwvQ": "{user} hat den Checklistenpunkt \"{name}\" zurückgesetzt", - "DKiv0o": "{user} hat den Checklistenpunkt \"{name}\" übersprungen", - "8FzC0B": "{user} hat den Checklistenpunkt \"{name}\" abgehakt", - "3qPQMX": "{name} hat ein Status-Update angefordert", - "9M92On": "Kanäle auswählen", - "N7Ln74": "Wiederholung", - "DoskyC": "Alle Teams", - "9S+ZiL": "Kein Team ist ausgewählt" -} diff --git a/webapp/playbooks/i18n/en.json b/webapp/playbooks/i18n/en.json deleted file mode 100644 index d2680f5b158..00000000000 --- a/webapp/playbooks/i18n/en.json +++ /dev/null @@ -1,608 +0,0 @@ -{ - "+/x2FM": "Select a playbook", - "+8G9qr": "Default text for the retrospective.", - "+Tmpup": "You automatically receive updates when this playbook is run.", - "+hddg7": "Add to run timeline", - "+qDKgW": "View all updates", - "/+8SGX": "Showing {filteredNum} of {totalNum} events", - "//o1Nu": "Disable updates", - "/1FEJW": "ACTIVE PARTICIPANTS per day over the last 14 days", - "/GCoTA": "Clear", - "/HtNUp": "Select or specify a {mode, select, DurationValue {time span (\"4 hours\", \"7 days\"...)} DateTimeValue {time (\"in 4 hours\", \"May 1\", \"Tomorrow at 1 PM\"...)} other {time or time span}}", - "/MaJux": "Start retrospective", - "/RnCQb": "Send outgoing webhook", - "/YZ/sw": "Start trial", - "/gbqA6": "{duration} before run started", - "/jUtaM": "ACTIVE RUNS per day over the last 14 days", - "/qDObA": "Browse Runs", - "03oqA2": "Active Runs", - "0Azlrb": "Manage", - "0CeyUV": "No results for \"{searchTerm}\"", - "0HT+Ib": "Archived", - "0QD99o": "Request to join channel", - "0RlzlZ": "Send a temporary welcome message to the user", - "0Vvpht": "Make Playbook Member", - "0Xt1ea": "You will still be able to access historical data for this metric.", - "0oL1zz": "Copied!", - "0oLj/t": "Expand", - "0tznw6": "Convert to private playbook", - "15jbT0": "Add more to your timeline", - "1GOpgL": "Assignee...", - "1I48bs": "Retrospective template", - "1MQ3XZ": "{numActiveRuns, plural, =0 {no active runs} =1 {# active run} other {# active runs}}", - "1OluNs": "Confirm enable status updates", - "1QosTr": "Used by", - "1fXVVz": "Due date...", - "1ikfp3": "If you delete this metric, the values for it will not be collected for any future runs.", - "1prgB2": "Search for people", - "2/2yg+": "Add", - "2563nT": "Confirm finish run", - "28FTjr": "Run actions allow you to automate activities for this channel", - "2BCWLD": "Configure channel", - "2NDgJq": "Last status update", - "2Q5PhZ": "Prompt to run a playbook", - "2QkJ4s": "Save important messages for a complete picture that streamlines retrospectives.", - "2VrVHu": "Search by run name", - "3/wF0G": "Slash commands", - "36GNZj": "The playbook {title} was successfully archived.", - "36NwLv": "Manage run participants list", - "3Ls2m+": "Playbook Member", - "3MSGcL": "Channel name is not valid.", - "3PoGhY": "Are you sure you want to publish?", - "3Yvt4d": "Playbooks are configurable checklists that define a repeatable process for teams to achieve specific and predictable outcomes", - "3hBelc": "A retrospective is not expected.", - "3qPQMX": "{name} requested a status update", - "3rCdDw": "Status updates", - "3sXVwy": "Task Actions...", - "3zF589": "Reset to all {filterName}", - "42qmJ5": "You do not have permission to post an update.", - "47FYwb": "Cancel", - "4BN53Q": "We’ll show you how close or far from the target each run’s value is and also plot it on a chart.", - "4GjZsL": "Total Playbooks", - "4Hrh5B": "{name} changed status from {summary}", - "4Iqlfe": "You've joined this run.", - "4alprY": "Playbook Templates", - "4aupaG": "The playbook {title} was successfully restored.", - "4cwL43": "With archived", - "4fHiNl": "Duplicate", - "4ltHYh": "Go to playbook", - "4mCpAv": "It was not possible to change the owner", - "4vuNrq": "{duration} after run started", - "5AJmOz": "When a user joins the channel", - "5BUxvl": "Everyone in this team can view this playbook.", - "5CI3KH": "Contact support", - "5HXkY/": "Type: {typeTitle}", - "5Hzwqs": "Favorite", - "5ZIN3u": "Status Updates", - "5b1zuB": "Add them to the run channel", - "5j6GD/": "{numParticipants, plural, =0 {no participants} =1 {# participant} other {# participants}}", - "5qBEKB": "What are playbook runs?", - "69nlA3": "Please enter a duration in the format: dd:hh:mm (e.g., 12:00:00).", - "6CGo3o": "Status / Last update", - "6D6ffM": "Please enter a duration in the format: dd:hh:mm (e.g., 12:00:00), or leave the target blank.", - "6rygzu": "Remove from run", - "6uhSSw": "Select a channel", - "706Soh": "tasks done", - "7KMbBa": "Never used", - "7P5T3W": "Restore checklist", - "8//+Yb": "Link checklist to a different channel", - "8FzC0B": "{user} checked off checklist item \"{name}\"", - "8n24G2": "View run details in a side panel", - "9+Ddtu": "Next", - "91Hr5f": "Drag me to reorder", - "95v+5O": "{actions, plural, =0 {Task Actions} one {# action} other {# actions}}", - "9AQ5FE": "Run summary", - "9M92On": "Select channels", - "9Obw6C": "Filter", - "9PXW6Q": "Duration / Started on", - "9SIW2x": "Target value for each run", - "9TTfXU": "Your System Admin has been notified.", - "9X3jwi": "{icon} Cost", - "9XUYQt": "Import", - "9a9+ww": "Title", - "9j5KzL": "Enter category name", - "9kQNdp": "This playbook is private.", - "9qqGGd": "Invite participants", - "9tBhzB": "Upgrade now", - "9trZXa": "Anyone on the team can view", - "9uOFF3": "Overview", - "9w0mDI": "Confirm remove pre-assigned member", - "9xs0pp": "Add value...", - "A21Mgv": "Run finished", - "A8dbCS": "Playbook Not Found", - "AF7+5o": "Add due date", - "AG7PKJ": "Rename run", - "AML4RW": "Task assignments", - "AhY0vJ": "Leave and unfollow", - "AoNLta": "There are no finished runs linked to this channel", - "Auj1ap": "Start a trial or upgrade your subscription.", - "B3Q5mz": "Trigger", - "B487HA": "In Progress", - "BJNrYQ": "As a participant, you’ll be able to update the run summary, check off tasks, post status updates and edit the retrospective.", - "BNB75h": "A playbook prescribes the checklists, automations, and templates for any repeatable procedures. {br} It helps teams reduce errors, earn trust with stakeholders, and become more effective with every iteration.", - "BQtd5I": "Welcome to Playbooks!", - "Bgt0C8": "This update for the run {runName} will be broadcasted to {hasChannels, select, true {{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}} other {}}.", - "BiQjuS": "Run moved to {channel}", - "Brya9X": "Add a run summary template…", - "C1khRR": "Back to playbooks", - "C6Oghd": "Edit run summary", - "C9NScU": "Put your team in control", - "CBM4vh": "Timer for next update", - "CFysvS": "Create Playbook Dropdown", - "CUhlqp": "tutorial tour tip product image", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# overdue}}", - "CwwzAU": "Add checklist name", - "CyGaem": "Run name", - "D2CE02": "Enter webhook", - "D55vrs": "Your license could not be generated", - "DCl7Vv": "inline code", - "DKiv0o": "{user} skipped checklist item \"{name}\"", - "DQn9Uj": "The user {name} is pre-assigned to one or more tasks. Not automatically inviting this user will clear their pre-assignments.{br}{br}Are you sure you want to stop inviting this user as a member of the run?", - "DUU48k": "There is no task explicitly assigned to you. You can expand your search using the filters.", - "DXACD6": "Publish retrospective report and access the timeline", - "DaHpK1": "Search for a channel", - "DnBhRg": "Add People", - "DqTQOp": "Once", - "DtCplA": "{numParticipants, plural, =1 {# participant} other {# participants}}", - "EQpfkS": "Finished", - "EVSn9A": "Start a run", - "EWz2w5": "Run Playbook", - "Edy3wX": "Checklist moved to {channel}", - "Ek1Fx2": "When a message with these keywords is posted", - "EvBQLq": "Make Playbook Admin", - "F4pfM/": "Please enter a number, or leave the target blank.", - "F9LrJA": "Filter items", - "FEGywG": "Please specify a future date/time for the update reminder.", - "FGzxgY": "e.g., Time to acknowledge, Time to resolve", - "FLG4Iu": "Make run owner", - "FXCLuZ": "{total, number} total", - "FgydNe": "View", - "G/yZLu": "Remove", - "GDCpPr": "Recent status update", - "GG1yhI": "There are templates for a range of use cases and events. You can use a playbook as-is or customize it—then share it with your team.", - "GVpA4Q": "Create New Playbook", - "GXjP8g": "All the runs that you can access will show here", - "GZoWl1": "Automate activities for this task", - "Gg/nch": "NOT PARTICIPATING", - "GjCS6U": "Choose a template", - "Gwmqz5": "Request an update", - "GxJAK1": "The playbook you're requesting is private or does not exist.", - "H7IzRB": "Disable status updates", - "HAlOn1": "Name", - "HGSVzc": "Can not import multiple files at once.", - "HLn43R": "Manage access", - "HSi3uv": "No Assignee", - "HXvk56": "Post status updates", - "HfjhwE": "Search playbooks", - "HhLp57": "quote", - "HvAcYh": "{text}{rest, plural, =0 {} one { and other} other { and {rest} others}}", - "I0NIMp": "Your tasks", - "I2zEie": "Celebrate success and learn from mistakes with retrospective reports. Filter timeline events for process review, stakeholder engagement, and auditing purposes.", - "I5NMJ8": "More", - "I7+d55": "Specify date/time (“in 4 hours”, “May 1”...)", - "I90sbW": "just now", - "IE2BzH": "There are users that are pre-assigned to one or more tasks. Disabling invitations will clear all pre-assignments.{br}{br}Are you sure you want to disable invitations?", - "IdTL+v": "Create a run channel", - "IfxUgC": "Add a run summary…", - "IxtSML": "Add a checklist", - "JJNc3c": "Previous", - "JXdbo8": "Done", - "JcefuP": "Add a description (optional)", - "JeqL8w": "Retrospective canceled by {name}", - "JrZ2th": "Add Metric", - "KQunC7": "Used in this channel", - "KeO51o": "Channel", - "KiXNvz": "Run", - "KjNfA8": "Invalid time duration", - "KzHQCQ": "There are no finished runs matching those filters.", - "L1tFef": "Please check spelling or try another search", - "L6k6aT": "…or start with a template", - "L6vn9U": "Run participants", - "LDYFkN": "Duration (in dd:hh:mm)", - "LI7YlB": "Add details on what this metric is about and how it should be filled in. This description will be available on the retrospective page for each run where values for these metrics will be input.", - "LKu0ex": "Are you sure you want to finish the run {runName} for all participants?", - "LaseGE": "You do not have permission to edit this checklist", - "LfhTNW": "Browse or create Playbooks and Runs", - "LmhSmU": "Confirm Entry Delete", - "Lo10yH": "Unknown Channel", - "M/2yY/": "Nobody yet.", - "M4gAc9": "Add value", - "M9tXoZ": "A join request will be sent to the run channel.", - "MBNMo9": "Channel Actions", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {task} other {tasks}}", - "MHzP9I": "Define a message to welcome users joining the channel.", - "MJ89uW": "Convert to Private playbook", - "MTzF3S": "Are you sure you want to restore the playbook {title}?", - "MbapTE": "{num} {num, plural, =1 {task} other {tasks}} overdue", - "MieztS": "Drop a playbook export file to import it.", - "Mjq//Y": "Unfavorite", - "MrJPOh": "Enable status updates", - "MtrTNy": "Tomorrow", - "MvEydR": "{name} posted a status update", - "MyIJbr": "Contents", - "N1U/QR": "Task state changes", - "N2IrpM": "Confirm", - "N7Ln74": "Rerun", - "NFyWnZ": "Work more effectively", - "NGKqOC": "Also add me to the channel linked to this run", - "NJ9uPu": "Key metrics", - "NLeFGn": "to", - "NMxVd+": "Please fill in the metric value.", - "NNksk4": "Alphabetically", - "NYTGIb": "Got it", - "Nh91Us": "{from, number}–{to, number} of {total, number} total", - "NiAH1z": "Target value", - "OQplDX": "A status update is expected every . New updates will be posted to {channelCount, plural, =0 {no channels} one {# channel} other {# channels}} and {webhookCount, plural, =0 {no outgoing webhooks} one {# outgoing webhook} other {# outgoing webhooks}}.", - "Ob5cSv": "Changes that you made will not be saved if you leave this page. Are you sure you want to discard changes and leave?", - "ObmjTB": "Slash Command", - "OcpRSQ": "Delete Entry", - "OfN7IN": "A status update request will be sent to the run channel.", - "Oo5sdB": "Playbook name", - "OqCzNb": "Add a task", - "OqWwvQ": "{user} unchecked checklist item \"{name}\"", - "OsDomv": "All events", - "OuZhcQ": "Specify duration (\"8 hours\", \"3 days\"...)", - "OyZnsJ": "per run", - "P6NEL/": "Command...", - "P6PLpi": "Join", - "PW+sL4": "N/A", - "PWmZrW": "View all runs", - "PdRg+3": "View all...", - "PoX2HN": "Send request", - "Ppx673": "Reports", - "Q/t0//": "Finished runs", - "Q15rLN": "Request update...", - "Q4sutg": "Confirm leave{isFollowing, select, true { and unfollow} other {}}", - "Q5hysF": "Do more with Playbooks", - "Q7aZO4": "{numParticipants, plural, =0 {no active participants} =1 {# active participant} other {# active participants}}", - "Q7hMnp": "Run playbook", - "Q8Qw5B": "Description", - "QJTSaI": "Link run to a different channel", - "QUwMsX": "Reminder to fill out the retrospective", - "QaZNp9": "Finish run", - "QbGfqo": "Broadcast to stakeholders in multiple places and keep a paper trail for retrospective with just one post.", - "QegBKq": "Join playbook", - "QiKcO7": "Enter retrospective template", - "QpUBDr": "{members, plural, =0 {No one} =1 {One person} other {# people}} can access this playbook.", - "QvEO6m": "You do not have permission to edit this run", - "QywYDe": "Also mark the run as finished", - "R/2lqw": "Select a template", - "R5Zh+l": "This lets you experience a sample playbook first before investing time to create your own.", - "RC6rA2": "Recently created", - "RO+BaS": "Copy link to run", - "RQl8IW": "Snooze for…", - "RXjd3Q": "{name} removed @{user} from the run", - "RgQwWr": "Sort runs by", - "RnOiCg": "It was not possible to {isFollowing, select, true {unfollow} other {follow}} the run", - "RoGxij": "Runs active on {date}", - "RrCui3": "Summary", - "RthEJt": "Retrospective", - "RzEVnf": "Playbooks make important procedures more repeatable and accountable. A playbook can be run multiple times, and each run has its own record and retrospective.", - "S0kWcH": "Update overdue", - "SDSqfA": "When a run starts", - "SK5APX": "It wasn't possible to leave the run.", - "SMrXWc": "Favorites", - "SRbTcY": "Other playbooks", - "SRqpbI": "{assignedNum, plural, =0 {No assigned tasks} other {# assigned}}", - "SVwJTM": "Export", - "SXJ98n": "You will not be able to edit the retrospective report after publishing it. Do you want to publish the retrospective report?", - "SmAUf9": "A reminder will be sent {timestamp}", - "Suyx6A": "The playbook import has failed. Please check that JSON is valid and try again.", - "SwlL5j": "@{user} joined the run", - "Sx3lHL": "Integer", - "TBez4r": "There are no playbooks to view. You don't have permission to create playbooks in this workspace.", - "TD8WrM": "Duplicate is disabled for this team.", - "TJo5E6": "Preview", - "TP/O/b": "Remove user", - "TSSNg/": "TOTAL RUNS started per week over the last 12 weeks", - "TTIQ6E": "Assign due dates to tasks so assignees can prioritize and get things done.", - "TZYiF/": "strike", - "TdTXXf": "Learn more", - "TnUG7m": "You don't have any pending task assigned.", - "Tt04f1": "See who is involved and what needs to be done without leaving the conversation.", - "TxmjKI": "Describe what this metric is about", - "UAS7Bn": "Request access to the channel linked to this run", - "UMFnWV": "View Retrospective", - "UMoxP9": "Channel name template (optional)", - "UbTsGY": "Runs started between {start} and {end}", - "UePrSL": "{num} {num, plural, one {Participant} other {Participants}}", - "Ul0aFX": "Import Playbook", - "VA1Q/S": "Public channel", - "VM75su": "{name} removed {num} participants from the run", - "VOzlSL": "Running a playbook orchestrates workflows for your team and tools.", - "Vf/QlZ": "Value range", - "Vhnd2J": "Toggle description", - "VjJYEV": "e.g., Sales impact, Purchases", - "VmnoW8": "Please check the system logs for more information.", - "W/V6+Y": "Collapse", - "W1EKh5": "Create new playbook", - "W1Qs5O": "Runs", - "WAHCT2": "Notify System Admin", - "WC+NOj": "Also add people to the channel linked to this run", - "WFA0Cg": "Are you sure you want to enable status updates for this run?", - "WFd88+": "Show checked tasks", - "WIxhrv": "Run name must have at least two characters", - "Wy3sw+": "{count, plural, =1{1 run in progress} =0 {No runs in progress} other {# runs in progress}}", - "X/koAN": "Invalid entry: the maximum number of webhooks allowed is 64", - "X2K92H": "Checklist name", - "XF8rrh": "Copy link to ''{name}''", - "XHJUSG": "Auto-follow runs", - "XRyRzf": "Status updates are not expected.", - "XS4umx": "{name} snoozed a status update", - "XXbWAU": "Select this to automatically receive updates when this playbook is run.", - "Xgxruo": "Skip checklist", - "XmUdvV": "All the statistics you need", - "XnICdK": "It wasn't possible to join the run", - "XpDetT": "Opt out of these tips.", - "Xx0WZV": "Send message", - "Y1EoT/": "When a participant leaves the run", - "YBvwXR": "No assigned tasks", - "YKLHXL": "View in progress runs", - "YORRGQ": "Post update", - "YQOmSf": "Enter one webhook per line", - "Z18I+c": "Channel actions allow you to automate activities for the channel", - "Z1sgPO": "View finished runs", - "Z2Hfu4": "Add a run summary", - "Z3ybv/": "Add the channel to a sidebar category for the user", - "Z7vWDQ": "There was an error", - "ZAJviT": "We weren't able to notify the System Admin.", - "ZJS10z": "No updates have been posted yet", - "ZNNjWw": "Please enter a number.", - "ZRv7Dm": "Request to Join", - "ZSa3cf": "@{targetUsername}, please provide a status update for [{runName}]({playbookURL}).", - "ZWtlyd": "Run restored by {name}", - "Zbk+OU": "The file size exceeds the limit of 5MB.", - "ZdWYcm": "No, skip retrospective", - "Zg0obP": "Restart run", - "ZkhArX": "Let's go!", - "a0hBZ0": "Delete metric", - "a2r7Vb": "Private channel", - "aACJNp": "Run started by {name}", - "aEhjYg": "Outline", - "aWpBzj": "Show more", - "aYIUar": "Thank you!", - "aZGAOI": "Add a status update template…", - "avPeEI": "Upgrade to view trends for total runs, active runs and participants involved in runs of this playbook.", - "awG90C": "Target per run", - "b/QBNs": "Update due", - "b3TdyZ": "By clicking Start trial, I agree to the Mattermost Software Evaluation Agreement, Privacy Policy, and receiving product emails.", - "b8Gps8": "Run status updates enabled by {name}", - "bCmvTY": "Give feedback", - "bE1Cro": "My runs only", - "bEoDyV": "@{authorUsername} posted an update for [{runName}]({overviewURL})", - "bLK+Kr": "Reminds the channel at a specified interval to fill out the retrospective.", - "bPLen5": "Runs finished in the last 30 days", - "bTgMQ2": "This playbook is archived.", - "bf5rs0": "View Info", - "c23IHq": "Channel actions allow you to automate activities for this channel", - "c6LNcW": "Delete task", - "c8hxKk": "Week of {date}", - "cGCoJe": "Posted by", - "cPIKU2": "Following", - "cUCiWw": "Become a participant", - "ch4Vs1": "Request updates for playbook runs in a single click and get notified directly when an update is posted. Start a free, 30-day trial to try it out.", - "cnfVhV": "Leave {isFollowing, select, true { and unfollow } other {}}run", - "cp7KUI": "Playbook", - "cpGAhx": "Are you sure you want to disable status updates for this run?", - "cyR7Kh": "Back", - "d4g2r8": "Deleted: {timestamp}", - "d8KvXJ": "Your trial license expires on {expiryDate}. You can purchase a license at any time through the Customer Portal to avoid any disruption.", - "d9epHh": "Export channel log", - "dK2JKl": "Link to an existing channel", - "dSC1YD": "Skip task", - "dZmYk6": "Successfully duplicated playbook", - "dvhvum": "(Optional) Describe how this playbook should be used", - "dxyZg3": "Let me explore for myself", - "e/AZL5": "Your 30-day trial has started", - "e3z3P8": "Discard & leave", - "eHAvFf": "bold", - "ePhhuK": "Your request was sent to the run channel.", - "ecS/qx": "{name} added {num} participants to the run", - "edxtzC": "Create playbook", - "efeNi1": "10-run average value", - "egvJrY": "Assignee Changed", - "eiPBw7": "Retrospective reminder interval", - "f+bqgK": "Name of the metric", - "fBG/Ge": "Cost", - "fV6578": "Assign the owner role", - "fVMECF": "Participant", - "fXGjhC": "Owner changed from {summary}", - "feNxoJ": "{requester} added {users} to the run", - "fhMaTZ": "Take a quick tour", - "fmbSyg": "Add value (in dd:hh:mm)", - "fnihsY": "Leave", - "fvNMLo": "Task actions", - "fwW0T1": "Confirm remove pre-assigned members", - "g0mp+I": "When you convert to a private playbook, membership and run history is preserved. This change is permanent and cannot be undone. Are you sure you want to convert {playbookTitle} to a private playbook?", - "g4IF1x": "There are no runs for this playbook.", - "g9pEhE": "Due", - "gGcNUr": "You do not have permissions", - "gGtlrk": "Your playbooks", - "gS1i4/": "Mark the task as done", - "gfUBRi": "Assign a new owner before you leave the run.", - "grv9Fm": "Select to toggle a list of tasks.", - "gt6BhE": "Run details", - "guunZt": "Assign", - "hVFgh4": "Include finished", - "hXIYHG": "Install and enable the Channel Export plugin to support exporting the channel", - "ha1TB3": "When a participant joins the run", - "hjteuA": "All the playbooks that you can access will show here", - "hrgo+E": "Archive", - "hw83pa": "Track key metrics and measure value", - "iEtImk": "When you leave{isFollowing, select, true { and unfollow a run} other { a run}}, it's removed from the left-hand sidebar. You can find it again by viewing all runs.", - "iH5e4J": "You’ll also be added to the channel linked to this run.", - "iMjjOH": "Next week", - "iNU1lj": "The run you're requesting is private or does not exist.", - "iQhFxR": "Last used", - "iXNbPf": "Rename", - "ieGrWo": "Follow", - "iigkp8": "Time to wrap up?", - "ijAUQf": "Notify your System Admin to upgrade.", - "izWS4J": "Unfollow", - "j2VYGA": "View all playbooks", - "j7jdWG": "Convert to a commercial edition.", - "j940pJ": "This update will be saved to overview page.", - "jAo8dd": "Run status updates disabled by {name}", - "jIIWN+": "preformatted", - "jIgqRa": "Owner / Participants", - "jfpnye": "@{user} left the run", - "jrOlPO": "Get run status update notifications", - "jvo0vs": "Save", - "jwimQJ": "Ok", - "k1djnL": "Delete checklist", - "k5EChD": "Are you sure you want to restart the run?", - "k7Nzfi": "Disable invitation", - "kEMvwX": "There are no runs matching those filters.", - "kQAf2d": "Select", - "kV5GkX": "When a status update is posted", - "kYCbJE": "Add time frame", - "ksG35Q": "You don't have permission to create playbooks in this workspace.", - "l/W5n7": "Participants will also be added to the channel linked to this run", - "l3QwVw": "Select channel", - "l5/RKZ": "There are no finished runs for this playbook.", - "lBqu4h": "Restore playbook", - "lJ48wN": "Private playbook", - "lJyq2a": "Run not found", - "lKeJ+i": "There's no summary", - "lQT7iD": "Create Playbook", - "lUfDe1": "Export the playbook run channel and save it for later analysis.", - "lZwZi+": "Day: {date}", - "lbhO3D": "italic", - "lbr3Lq": "Copy link", - "lbs7UO": "per run over the last 10 runs", - "lgZf0l": "Get started with Playbooks", - "lkv547": "Due date (Available in the Professional plan)", - "lqceIp": "or Import a playbook", - "lqzBNa": "Remove them from the run channel", - "lr1CUA": "Browse Playbooks", - "lrbrjv": "Yes, start retrospective", - "lyXljU": "Duplicate task", - "m/KtHt": "You have no permissions to change the owner", - "m/Q4ye": "Rename checklist", - "m4vqJl": "Files", - "m8hzTK": "Last used {time}", - "mCrdeS": "Total Playbook Runs", - "mILd++": "The run name should not exceed {maxLength} characters", - "mLrh+0": "No due date", - "mNgqXf": "To unlock this feature:", - "mVpO8u": "Seen this before?", - "meD+1Q": "RUN PARTICIPANTS", - "mkLeuq": "Broadcast update to selected channels", - "mm5vL8": "Only invited members", - "mw9jVA": "Add a title", - "nc8QpJ": "Recent Activity", - "nkCCM2": "You will not be reminded again.", - "nqVby7": "{numTasksChecked, number} of {numTasks, number} {numTasks, plural, =1 {task} other {tasks}} checked", - "nsd54s": "Confirm disable status updates", - "o+ZEL3": "Published {timestamp}", - "o2eHmz": "Run finished by {name}", - "o6N9pU": "Run actions", - "oAJsne": "Public playbook", - "oBeKB4": "Due on {date}", - "oL7YsP": "Last edited {timestamp}", - "oVHn4s": "Last update", - "ocYb9S": "Key Metrics", - "ojQue/": "{icon} Duration (in dd:hh:mm)", - "opn6uf": "View Timeline", - "osuP6z": "Drag to reorder checklist", - "p1I/Fx": "We’ve auto-created your run", - "pFK6bJ": "View all", - "pKLw8O": "Are you sure you want to delete this event? Deleted events will be permanently removed from the timeline.", - "prs4kX": "When a message with specific keywords is posted", - "pzTOmv": "Followers", - "q/Qo8l": "Private playbooks are only available in Mattermost Enterprise", - "q48ca7": "Give feedback about Playbooks.", - "q6f8x9": "Change since last update", - "qDxsQH": "Become a participant to interact with this run", - "qGlwfc": "Start run", - "qxYWTy": "Show all tasks from runs I own", - "qyJtWy": "Show less", - "rDvvQs": "{completed, number} / {total, number} done", - "rMhrJH": "Please add a title for your metric.", - "rX08cW": "Date must be in the future.", - "rbrahO": "Close", - "ru+JCk": "Average value", - "ruJGqS": "Playbook Access", - "ryrP8K": "Manage permission for who can view, modify, and run this playbook.", - "rzbYbE": "Target", - "s+rSpl": "{icon} Integer", - "s3jjqi": "{num_actions, plural, =0 {no actions} one {# action} other {# actions}}", - "sDKojV": "Archive playbook", - "sGJpuF": "Add a description…", - "sIX63S": "Your System Admin has been notified", - "sQu1rA": "{numTotalRuns, plural, =0 {no runs started} =1 {# run started} other {# runs started}}", - "sVlNlY": "Every team's structure is different. You can manage which users in the team can create playbooks.", - "sX5Mn5": "Please enter one webhook per line", - "scYyVv": "Would you like to fill out the retrospective report?", - "soePYH": "{num_checklists, plural, =0 {no checklists} one {# checklist} other {# checklists}}", - "sqNmlF": "Skip retrospective", - "syEQFE": "Publish", - "t6SiGO": "Runs currently in progress", - "t6lwwM": "{requester} removed {users} from the run", - "tVPYMu": "Playbook Admin", - "tbjmvS": "A metric with the same name already exists. Please add a unique name for each metric.", - "tqAmbk": "Runs in progress", - "twieZh": "Go to run overview", - "u/yGzS": "{name} added @{user} to the run", - "u4L4yd": "You have unsaved changes", - "u4MwUB": "Save your playbook run history", - "uCS6py": "You do not have permission to see this playbook", - "uT4ebt": "e.g., Resource count, Customers affected", - "uYrkxy": "The file must be a valid JSON playbook template.", - "udrLSP": "Use metrics to understand patterns and progress across runs, and track performance.", - "uhu5aG": "Public", - "unwVil": "The join channel request was unsuccessful.", - "uny3Zy": "Playbooks", - "utHl3F": "Add people to {runName}", - "v1DNMW": "Retrospective published by {name}", - "v1SpKO": "Role changes", - "v5/Cox": "Duplicate checklist", - "vDvWJ6": "Try request update with a free trial", - "vL4++D": "Track progress and ownership", - "vSMfYU": "Run info", - "viXE32": "Private", - "vjb+hS": "{user} restored checklist item \"{name}\"", - "vjzpnC": "There are no playbooks matching those filters.", - "vndQuC": "Slash Command Executed", - "vqmRBs": "Confirm restart run", - "w0muFd": "Send outgoing webhook (One per line)", - "w4Nhhb": "Add participant", - "wBZz47": "You've left the run.", - "wCDmf3": "Enable updates", - "wEQDC6": "Edit", - "wL7VAE": "Actions", - "wRM2AO": "The update request was unsuccessful.", - "wZ83YL": "Not right now", - "waVyVY": "Participants currently active", - "wbdGb5": "Assign, check off, or skip tasks to ensure the team is clear on how to move toward the finish line together.", - "wbsq7O": "Usage", - "wcWpGs": "Invalid webhook URLs", - "wylJpv": "Everyone in {team} can view this playbook.", - "x1phlu": "No time frame", - "x5Tz6M": "Report", - "x8cvBr": "View run overview", - "xEQYo5": "Configure custom metrics to fill out with the retrospective report.", - "xHNF7i": "Run Actions", - "xVyHgP": "Start a test run", - "xfnuXm": "Participate", - "xmcVZ0": "Search", - "xvBDOH": "Are you sure you want to archive the playbook {title}?", - "y7o4Rn": "Are you sure you want to delete?", - "yP3Ud4": "There are no runs in progress linked to this channel", - "yhU1et": "Tasks", - "yllba1": "This archived playbook cannot be renamed.", - "ypIsVG": "Restore task", - "yqpcOa": "Use", - "z3B83t": "Search for a playbook", - "zELxbG": "Saved messages", - "zINlao": "Owner", - "zSOvI0": "Filters", - "zW/5AB": "Professional feature This is a paid feature, available with a free 30-day trial", - "zWgbGg": "Today", - "zWkvNO": "Timeline", - "zl6378": "Configure metrics in Retrospective", - "zscc/+": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run {runName} for all participants?", - "zx0myy": "Participants", - "zxj2Gh": "Last updated {time}", - "zz6ObK": "Restore" -} diff --git a/webapp/playbooks/i18n/en_AU.json b/webapp/playbooks/i18n/en_AU.json deleted file mode 100644 index e0d8327290e..00000000000 --- a/webapp/playbooks/i18n/en_AU.json +++ /dev/null @@ -1,838 +0,0 @@ -{ - "zy3cJT": "Prompt to run this playbook when a user posts a message containing the keywords", - "zx0myy": "Participants", - "zWkvNO": "Timeline", - "zINlao": "Owner", - "zELxbG": "Saved messages", - "z5RMPO": "Only you can access this playbook", - "z3A0LP": "Last run was {relativeTime}", - "yxguVq": "Discard changes", - "yqpcOa": "Use", - "yhzuSC": "Time: {time}", - "yhU1et": "Tasks", - "xmcVZ0": "Search", - "x8cvBr": "View run overview", - "x5Tz6M": "Report", - "wsUmh9": "Team", - "wcWpGs": "Invalid webhook URLs", - "wbwhbH": "Task name", - "wbsq7O": "Usage", - "waVyVY": "Participants currently active", - "wZ83YL": "Not right now", - "wX3k9U": "Untitled playbook", - "wL7VAE": "Actions", - "wEQDC6": "Edit", - "w7tf2z": "Published", - "w0muFd": "Send outgoing webhook (one per line)", - "vndQuC": "Slash Command Executed", - "vir0m9": "Invalid category name.", - "viXE32": "Private", - "vOFN0m": "Status post deleted:", - "vNiZXF": "There are no runs in progress at the moment. Run a playbook to begin orchestrating workflows for your team and tools.", - "v8ZnNc": "Select a team", - "v3+TmO": "{members, plural, =0 {No one} =1 {One person} other {# people}} can access this playbook", - "v1SpKO": "Role changes", - "v1DNMW": "Retrospective published by {name}", - "usa8vQ": "Send a welcome message", - "uny3Zy": "Playbooks", - "uhu5aG": "Public", - "uJ3bRR": "This template helps to standardise the format for a concise description that explains each run to its stakeholders.", - "uBLF+D": "What is a playbook?", - "u4MwUB": "Save your playbook run history", - "tzMNF3": "Status", - "twieZh": "Go to run overview", - "t6SiGO": "Runs currently in progress", - "syEQFE": "Publish", - "sqNmlF": "Skip retrospective", - "soePYH": "{num_checklists, plural, =0 {no checklists} one {# checklist} other {# checklists}}", - "scYyVv": "Would you like to fill out the retrospective report?", - "sVlNlY": "Every team's structure is different. You can manage which users in the team can create playbooks.", - "sQu1rA": "{numTotalRuns, plural, =0 {no runs started} =1 {# run started} other {# runs started}}", - "sIX63S": "Your System Admin has been notified.", - "s3jjqi": "{num_actions, plural, =0 {no actions} one {# action} other {# actions}}", - "ryrP8K": "Manage permission for who can view, modify, and run this playbook.", - "recCg9": "Updates", - "rbrahO": "Close", - "rX08cW": "Date must be in the future.", - "rDvvQs": "{completed, number} / {total, number} done", - "qyJtWy": "Show less", - "qp3Fk4": "A playbook is a workflow that your teams and tools should follow, including everything from checklists, actions, templates, and retrospectives.", - "q6f8x9": "Change since last update", - "prYDT6": "Announcement Channel", - "pjt3qA": "New checklist", - "pKLw8O": "Are you sure you want to delete this event? Deleted events are permanently removed from the timeline.", - "oVHn4s": "Last update", - "oS0w4E": "Default update timer", - "o2eHmz": "Run finished by {name}", - "nqVby7": "{numTasksChecked, number} of {numTasks, number} {numTasks, plural, =1 {task} other {tasks}} checked", - "nmpevl": "Discard", - "nkCCM2": "You will not be reminded again.", - "lxfpbh": "The owner will {reminderEnabled, select, true {be prompted to provide a status update every} other {not be prompted to provide a status update}}", - "lrbrjv": "Yes, start retrospective", - "lbhO3D": "italic", - "lZwZi+": "Day: {date}", - "lJyq2a": "Run not found", - "l7zMH6": "Select an option or specify a custom duration", - "l0hFoB": "Add playbook description", - "kvgvNW": "Know what happened", - "kXFojL": "You can also create a playbook ahead of time so it’s available when you need it.", - "kGI46P": "Task description", - "k9q07e": "Broadcast update to other channels", - "jwimQJ": "OK", - "jvo0vs": "Save", - "jq4eWU": "Playbook access", - "jnmORb": "In this playbook", - "jXT2++": "Go to channel", - "jS/UOn": "Update template", - "jIgqRa": "Owner / Participants", - "jIIWN+": "preformatted", - "j7jdWG": "Convert to a commercial edition.", - "izWS4J": "Unfollow", - "ijAUQf": "Notify your System Admin to upgrade.", - "ieGrWo": "Follow", - "iNU1lj": "The run you're requesting is private or does not exist.", - "hzt6l8": "Use Markdown to create a template.", - "hfrrC7": "Team Initials", - "hXIYHG": "Install and enable the Channel Export plugin to support exporting the channel", - "hVFgh4": "Include finished", - "hO9EdA": "Invite {numInvitedUsers, plural, =0 {no members} =1 {one member} other {# members}} to the channel", - "gy/Kkr": "(edited)", - "guunZt": "Assign", - "gt6BhE": "Run details", - "g5pX+a": "About", - "g4IF1x": "There are no runs for this playbook.", - "fpuWL1": "Delete playbook", - "fmylXu": "Prompt to run the playbook when a user posts a message", - "fdQDz+": "The playbook {title} was successfully deleted.", - "fXGjhC": "Owner changed from {summary}", - "fV6578": "Assign the owner role", - "fUEpLA": "There are no Timeline events matching those filters.", - "eiPBw7": "Retrospective reminder interval", - "egvJrY": "Assignee Changed", - "edxtzC": "Create playbook", - "ebkl6I": "Everyone in this team can access this playbook", - "eLeFE2": "Edit name and description", - "eKv7yX": "Post", - "eHAvFf": "bold", - "e/AZL5": "Your 30-day trial has started", - "dvhvum": "(Optional) Describe how this playbook should be used", - "dsTLW1": "Edit task", - "djXM+y": "Only selected users can access.", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {run} other {runs}} in progress", - "dIwav9": "Are you sure you want to delete this task? This will be removed from this run but will not affect the playbook.", - "d9epHh": "Export channel log", - "d8KvXJ": "Your trial licence expires on {expiryDate}. You can purchase a licence at any time through the Customer Portal to avoid any disruption.", - "c8hxKk": "Week of {date}", - "c6LNcW": "Delete task", - "bPLen5": "Runs finished in the last 30 days", - "bLK+Kr": "Reminds the channel at a specified interval to fill out the retrospective.", - "bGhCLX": "When an update is posted", - "bE1Cro": "My runs only", - "b5FaCc": "Add the channel to the sidebar category", - "b40Pr7": "Reporter", - "b3TdyZ": "By clicking Start trial, I agree to the Mattermost Software Evaluation Agreement, Privacy Policy, and receiving product emails.", - "b/QBNs": "Update due", - "avPeEI": "Upgrade to view trends for total runs, active runs and participants involved in runs of this playbook.", - "aYIUar": "Thank you!", - "aWpBzj": "Show more", - "aACJNp": "Run started by {name}", - "ZdWYcm": "No, skip retrospective", - "ZWtlyd": "Run restored by {name}", - "ZAJviT": "Unable to notify the System Admin.", - "Z7vWDQ": "An error occurred", - "Z/hwEf": "The channel will be reminded to perform the retrospective {reminderEnabled, select, true {every} other {}}", - "YORRGQ": "Post update", - "YMrTRm": "Run Summary", - "YKn+7s": "This channel is not running any playbook.", - "YDuW/T": "{num_runs, plural, =0 {Not run yet} one {# run} other {# total runs}}", - "Y+U8La": "Are you sure you want to delete the playbook {title}?", - "XmUdvV": "All the statistics you need", - "X3DLGJ": "Everyone in this workspace can create playbooks.", - "X/koAN": "Invalid entry: the maximum number of webhooks allowed is 64", - "WTQpnI": "Take action now using playbooks", - "WIxhrv": "Run name must have at least two characters", - "WAHCT2": "Notify System Admin", - "W1Qs5O": "Runs", - "W/V6+Y": "Collapse", - "VmnoW8": "Please check the system logs for more information.", - "VOzlSL": "Running a playbook orchestrates workflows for your team and tools.", - "V5TY0z": "Add participants?", - "Ui6GK/": "When a new member joins the channel", - "UbTsGY": "Runs started between {start} and {end}", - "TyrY2b": "Playbook creation", - "TvihSy": "Republish", - "TdTXXf": "Learn more", - "TZYiF/": "strike", - "TSSNg/": "TOTAL RUNS started per week over the last 12 weeks", - "TJo5E6": "Preview", - "TDaF6J": "Dismiss", - "TBez4r": "There are no playbooks to view. You don't have permission to create playbooks in this workspace.", - "T7Ry38": "Message", - "T5rX+W": "How often should an update be posted?", - "SmAUf9": "A reminder will be sent {timestamp}", - "SFuk1v": "Permissions", - "SENRqu": "Help", - "SDSqfA": "When a run starts", - "S0kWcH": "Update overdue", - "RthEJt": "Retrospective", - "RoGxij": "Runs active on {date}", - "Rgo4VW": "Everyone in this workspace can create playbooks. System Administrators may change this setting.", - "R4vA+C": "Only the users below can create playbooks. These users, as well as System Administrators, may change this setting.", - "R+JQaJ": "Channel members", - "Qrl6bQ": "Streamline your processes with playbooks", - "QnZAit": "Add optional description", - "QiKcO7": "Enter retrospective template", - "QaZNp9": "Finish run", - "QVQrgH": "If you remove your own access to this playbook, you won't be able to add yourself back. Are you sure you'd like to perform this action?", - "QUwMsX": "Reminder to fill out the retrospective", - "Q8Qw5B": "Description", - "Q7hMnp": "Run playbook", - "Q7aZO4": "{numParticipants, plural, =0 {no active participants} =1 {# active participant} other {# active participants}}", - "Q67RuY": "See all runs", - "OsDomv": "All events", - "Oo5sdB": "Playbook name", - "OcpRSQ": "Delete Entry", - "ObmjTB": "Slash Command", - "OK8u0r": "Create a playbook to prescribe the workflow that your teams and tools should follow, including everything from checklists, actions, templates, and retrospectives.", - "OINwWS": "Create a {isPublic, select, true {public} other {private}} channel", - "OHfpS1": "Containing any of these keywords", - "Nh91Us": "{from, number}–{to, number} of {total, number} total", - "NE1OeI": "Everyone on team({team}) can access.", - "N2IrpM": "Confirm", - "N1U/QR": "Task state changes", - "MvEydR": "{name} posted a status update", - "Mm1Gse": "Search for member", - "MhKICa": "Your subscription allows one playbook per team. Upgrade your subscription to create multiple playbooks with unique workflows for each team.", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {task} other {tasks}}", - "MDP9TS": "Remove from playbook", - "M/2yY/": "Nobody yet.", - "LmhSmU": "Confirm Entry Delete", - "Lg3I1b": "@{targetUsername}, please provide a status update.", - "Leh2tk": "Click here to see all runs in the team.", - "LVYPbG": "Assign Owner", - "LRFvqz": "Announce in the {oneChannel, plural, one {channel} other {channels}}", - "L6k6aT": "…or start with a template", - "KiXNvz": "Run", - "KUr+sG": "Update run summary", - "KJu1sq": "Remove checklist", - "K4O03z": "New task", - "K3r6DQ": "Delete", - "JeqL8w": "Retrospective cancelled by {name}", - "JXdbo8": "Done", - "JJNc3c": "Previous", - "JJMNME": "{withRunName, select, true {@{authorUsername} posted an update for [{runName}]({overviewURL})} other {@{authorUsername} posted an update}}", - "JCGvY/": "This template helps to standardise the format for recurring updates that take place throughout each run to keep.", - "J1G4S4": "There are no playbooks defined yet.", - "IwY/wg": "A playbook for every process", - "IuFETn": "Duration", - "IfxUgC": "Add a run summary", - "Ietscn": "Tasks finished", - "IOnm/Z": "There is no run summary available.", - "ICqy9/": "Checklists", - "I90sbW": "just now", - "I2zEie": "Celebrate success and learn from mistakes with retrospective reports. Filter timeline events for process review, stakeholder engagement, and auditing purposes.", - "Hzwzgs": "Broadcast updates in the {oneChannel, plural, one {channel} other {channels}}", - "HhLp57": "quote", - "HSi3uv": "No Assignee", - "HAlOn1": "Name", - "GxJAK1": "The playbook you're requesting is private or does not exist.", - "GwtR3W": "Drag and drop an existing task or click to create a new task.", - "GRTyvN": "Toggle Playbook List", - "G/yZLu": "Remove", - "FEGywG": "Please specify a future date/time for the update reminder.", - "EC5MJD": "There are no updates available.", - "E0LnBo": "You can select an option or specify a custom duration ('2 weeks', '3 days and 12 hours', '45 minutes', ...)", - "DuRxjT": "Create a playbook", - "DtCplA": "{numParticipants, plural, =1 {# participant} other {# participants}}", - "DnBhRg": "Add People", - "DXACD6": "Publish retrospective report and access the timeline", - "DSVJjB": "Currently running the {playbookTitle} playbook", - "DCl7Vv": "inline code", - "D55vrs": "Your licence could not be generated", - "D3idYv": "Settings", - "D2CE02": "Enter webhook", - "CyGaem": "Run name", - "Cy1AK/": "View run details", - "CkYhdY": "Add the channel to a sidebar category", - "CjNrqO": "Retrospective report template", - "CSts8B": "Team Icon", - "CL5OZP": "Only users who you select will be able to edit or run this playbook.", - "CBM4vh": "Timer for next update", - "C9NScU": "Put your team in control", - "C1khRR": "Back to playbooks", - "BQtd5I": "Welcome to Playbooks!", - "BNB75h": "A playbook prescribes the checklists, automations, and templates for any repeatable procedures. {br} It helps teams reduce errors, earn trust with stakeholders, and become more effective with every iteration.", - "BD66u6": "Download a CSV containing all messages from the channel", - "B487HA": "In Progress", - "Auj1ap": "Start a trial or upgrade your subscription.", - "ArpdYl": "Timeline events are displayed here as they occur. Hover over an event to remove it.", - "ApULhK": "Invite members", - "AT2QBo": "Only selected users can create playbooks.", - "AS5kar": "Participants ({participants})", - "AML4RW": "Task assignments", - "AF9wda": "This update will be saved to the overview page{hasBroadcast, select, true { and broadcast to {broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}.", - "A8dbCS": "Playbook Not Found", - "A3ptul": "Templates", - "A21Mgv": "Run finished", - "9uOFF3": "Overview", - "9tBhzB": "Upgrade now", - "9qc7BX": "Snooze", - "9kCT7Q": "Make retrospectives easy with a timeline that automatically keeps track of the key events and messages so that teams have it at their fingertips.", - "9TTfXU": "Your System Admin has been notified.", - "9PXW6Q": "Duration / Started on", - "91Hr5f": "Click and drag to reorder", - "9Obw6C": "Filter", - "9+Ddtu": "Next", - "8hDbW6": "Send an outgoing webhook", - "6uhSSw": "Select a channel", - "6n0XDG": "Are you sure you want to remove the checklist? All tasks will be removed.", - "6jDabx": "Give Feedback", - "6Lwe7T": "Everyone in {team} can access this playbook", - "6CGo3o": "Status / Last update", - "5wqhGy": "Toggle Run Details", - "5qBEKB": "What are playbook runs?", - "5j6GD/": "{numParticipants, plural, =0 {no participants} =1 {# participant} other {# participants}}", - "4ltHYh": "Go to playbook", - "5Ot7cd": "Determine the type of channel this playbook creates.", - "5FRgqE": "Downloading channel log", - "5CI3KH": "Contact support", - "5A46pW": "Add a slash command", - "4Hrh5B": "{name} changed status from {summary}", - "47FYwb": "Cancel", - "42qmJ5": "You do not have permission to post an update.", - "3rCdDw": "Status updates", - "3Psa+5": "Add keywords", - "3/wF0G": "Slash commands", - "2VrVHu": "Search by run name", - "2Qq4YX": "Are you sure you want to discard your changes?", - "2QkJ4s": "Save important messages for a complete picture that streamlines retrospectives.", - "2PNrBQ": "Export the channel of your Playbook run and save it for later analysis.", - "1MQ3XZ": "{numActiveRuns, plural, =0 {no active runs} =1 {# active run} other {# active runs}}", - "1I48bs": "Retrospective template", - "15jbT0": "Add more to your timeline", - "0wJ7N+": "Task", - "0oLj/t": "Expand", - "/jUtaM": "ACTIVE RUNS per day over the last 14 days", - "/YZ/sw": "Start trial", - "/MaJux": "Start retrospective", - "/HtNUp": "Select or specify a {mode, select, DurationValue {time span (\"4 hours\", \"7 days\"...)} DateTimeValue {time (\"in 4 hours\", \"1st of May\", \"Tomorrow at 1300\"...)} other {time or time span}}", - "/1FEJW": "ACTIVE PARTICIPANTS per day over the last 14 days", - "+hddg7": "Add to run timeline", - "+ZIXOR": "Channel access", - "+QgvjN": "Assign the owner role to", - "+8G9qr": "Default text for the retrospective.", - "xvBDOH": "Are you sure you want to archive the playbook {title}?", - "sDKojV": "Archive playbook", - "hrgo+E": "Archive", - "EQpfkS": "Finished", - "36GNZj": "Successfully archived playbook {title}.", - "0HT+Ib": "Archived", - "zz6ObK": "Restore", - "ypIsVG": "Restore task", - "wO6NOM": "Are you sure you want to Restore this task? This Task will be added to this run.", - "kDcpd/": "{numKeywords, plural, other {# keywords}}", - "h+e7G+": "Prompt to run this playbook when a message contains {numKeywords, select, 1 {the keyword} other {one or more of these}}", - "dSC1YD": "Skip task", - "XXbWAU": "Select this to automatically receive updates when this playbook is run.", - "Vhnd2J": "Toggle description", - "7VTSeD": "Are you sure you want to skip this task? This will be crossed from this run but will not affect the playbook.", - "/4tOwT": "Skip", - "+Tmpup": "You'll automatically receive updates when this playbook is run.", - "z3B83t": "Search for a playbook", - "vjzpnC": "There are no playbooks matching those filters.", - "fuDLDJ": "Create a channel", - "cp7KUI": "Playbook", - "UMoxP9": "Channel name template (optional)", - "RO+BaS": "Copy link to run", - "NA7Cw1": "Copy link to playbook", - "C6Oghd": "Edit run summary", - "3MSGcL": "Invalid Channel name.", - "0oL1zz": "Copied!", - "cPIKU2": "Following", - "d4g2r8": "Deleted on {timestamp}", - "O8o2lE": "Add channel to category", - "Mu2aDs": "Everyone on team ({team}) has permission to access.", - "4vuNrq": "{duration} after run started", - "/gbqA6": "{duration} before run started", - "vaYTD+": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run?", - "q0cpUe": "Add checklist", - "nSFBC2": "+ Add task", - "m/Q4ye": "Rename checklist", - "k1djnL": "Delete checklist", - "iXNbPf": "Rename", - "X2K92H": "Checklist name", - "WbsomC": "Publish retrospective", - "TxCTXQ": "Are you sure you want to finish the run?", - "QywYDe": "Also mark the run as finished", - "MrJPOh": "Enable status updates", - "Ja1sVR": "Status updates were disabled for this playbook run.", - "I5NMJ8": "More", - "D9IV7i": "Retrospectives were disabled for this playbook run.", - "D/wCS9": "Are you sure you want to publish the retrospective?", - "5Ofkag": "Enable retrospective", - "2563nT": "Confirm finish run", - "2/2yg+": "Add", - "/ZsEUy": "Are you sure you want to delete this checklist? It will be removed from this run but will not affect the playbook.", - "pK6+CW": "@{displayName} is not a member of the [{runName}]({overviewUrl}) channel. Would you like to add them to this channel? They will have access to all of the message history.", - "iDMOiz": "CHANNEL MEMBERS", - "JqKASQ": "Add @{displayName} to Channel", - "5ciuDD": "NOT IN CHANNEL", - "Lo10yH": "Unknown Channel", - "wylJpv": "Everyone in {team} can view this playbook.", - "tVPYMu": "Playbook Admin", - "ruJGqS": "Playbook Access", - "osuP6z": "Drag to reorder checklist", - "o+ZEL3": "Published {timestamp}", - "lQT7iD": "Create Playbook", - "gGcNUr": "You do not have permissions", - "g0mp+I": "When you convert to a private playbook, membership and run history is preserved. This change is permanent and cannot be undone. Are you sure you want to convert {playbookTitle} to a private playbook?", - "SXJ98n": "You will not be able to edit the retrospective report after publishing it. Do you want to publish the retrospective report?", - "R/2lqw": "Select a template", - "QpUBDr": "{members, plural, =0 {No one} =1 {One person} other {# people}} can access this playbook.", - "MJ89uW": "Convert to Private playbook", - "HLn43R": "Manage access", - "EvBQLq": "Make Playbook Admin", - "EWz2w5": "Run Playbook", - "8oCVbz": "Are you sure you want to publish?", - "5BUxvl": "Everyone in this team can view this playbook.", - "3Ls2m+": "Playbook Member", - "0tznw6": "Convert to private playbook", - "0Vvpht": "Make Playbook Member", - "qsr3Zk": "Update the Run Summary", - "0q+hj2": "Define a template for a concise description that explains each run to its stakeholders.", - "FXCLuZ": "{total, number} total", - "3PoGhY": "Are you sure you want to publish?", - "SVwJTM": "Export", - "9XUYQt": "Import", - "4fHiNl": "Duplicate", - "4alprY": "Playbook Templates", - "/urtZ8": "Your Playbooks", - "lBqu4h": "Restore playbook", - "bTgMQ2": "This playbook is archived.", - "MTzF3S": "Are you sure you want to restore the playbook {title}?", - "4cwL43": "With archived", - "4aupaG": "The playbook {title} was successfully restored.", - "y7o4Rn": "Are you sure you want to delete?", - "uT4ebt": "e.g. Resource Count, Customers Affected", - "tbjmvS": "A metric with the same name already exists. Please add a unique name for each metric.", - "rzbYbE": "Target", - "rMhrJH": "Please add a title for your metric.", - "q/Qo8l": "Private playbooks are only available in Mattermost Enterprise", - "mbo96h": "Configure custom metrics to fill out with the retrospective report", - "mVpO8u": "Seen this before?", - "gsMPAS": "Dollars", - "f+bqgK": "Metric Name", - "a0hBZ0": "Delete metric", - "XpDetT": "Opt out of these tips.", - "VZRWFk": "e.g. Cost, Purchases", - "TxmjKI": "Describe what this metric is about", - "Sx3lHL": "Integer", - "OyZnsJ": "per run", - "NYTGIb": "Got it", - "NJ9uPu": "Key metrics", - "LI7YlB": "Add details on what this metric is about and how it should be filled in. This description will be available on the retrospective page for each run where values for these metrics will be input.", - "LDYFkN": "Duration (in dd:hh:mm)", - "FGzxgY": "e.g. Time to acknowledge, Time to resolve", - "6D6ffM": "Please enter a duration in the format: dd:hh:mm (e.g. for 12 days, 0 hours and 0 minutes: 12:00:00), or leave the target blank.", - "JrZ2th": "Add Metric", - "F4pfM/": "Please enter a number, or leave the target blank.", - "9SIW2x": "Target value for each run", - "4BN53Q": "You'll see a plot of how close or far from the target each run’s value is.", - "1ikfp3": "If you delete this metric, the values for it will not be collected for any future runs.", - "0Xt1ea": "You will still be able to access historical data for this metric.", - "wbdGb5": "Assign, check off, or skip tasks to ensure the team is clear on how to move toward the finish line together.", - "wPVxBN": "Click on edit to start customising it and tailor it to your own models and processes. You can explore the template in detail on this page.", - "vQqT/8": "Select edit to start customising it and tailor it to your own models and processes. You can explore the template in detail on this page.", - "vL4++D": "Track progress and ownership", - "vJ2SaW": "Automate aspects of your playbook, such as sending a welcome message, inviting key members, and creating an update channel.", - "q/VD+s": "Set timers and put together a template for status updates so stakeholders are always up to date with developments.", - "lgZf0l": "Get started with Playbooks", - "fhMaTZ": "Take a quick tour", - "dxyZg3": "Let me explore for myself", - "dZmYk6": "Playbook duplicated successfully", - "cEWBE3": "Evaluate your processes using a retrospective to refine and improve with each run.", - "ZkhArX": "Let's go!", - "Tt04f1": "See who is involved and what needs to be done without leaving the conversation.", - "RzEVnf": "Playbooks make important procedures more repeatable and accountable. A playbook can be run multiple times, and each run has its own record and retrospective.", - "R5Zh+l": "This lets you experience a sample playbook first before investing time to create your own.", - "QbGfqo": "Broadcast to stakeholders in multiple places and keep a paper trail for retrospective with just one post.", - "Q5hysF": "Do more with Playbooks", - "Q3R9Uj": "Document steps for the entire process here. Assign each task to responsible individuals and optionally add timelines or linked actions.", - "Pue+oV": "Run the playbook to see it in action", - "I5DYM+": "Learn AND reflect", - "HXvk56": "Post status updates", - "HGdWwZ": "Create and assign tasks", - "GjCS6U": "Choose a template", - "GG1yhI": "There are templates for a range of use cases and events. You can use a playbook as-is or customise it - then share it with your team.", - "GAuN6w": "Set up assumptions", - "9m0I/B": "Keep stakeholders updated", - "8n24G2": "View run details in a side panel", - "6GTzTR": "See what’s in this playbook at any time", - "1isgPF": "Your first run has been auto-created", - "1QosTr": "Used by", - "0EEIkR": "Congratulations! You’ve created your first playbook using a template!", - "/fU9y/": "You can check out different sections of the playbook in detail on this page.", - "udrLSP": "Use metrics to understand patterns and progress across runs, and track performance.", - "lUfDe1": "Export the playbook run channel and save it for later analysis.", - "hw83pa": "Track key metrics and measure value", - "xVyHgP": "Start a test run", - "ru+JCk": "Average value", - "mvZUm3": "This is where you can explore your playbook components in detail. Select 'Edit' to customise your playbook to fit your processes and models.", - "lbs7UO": "per run over the last 10 runs", - "l5/RKZ": "There are no finished runs for this playbook.", - "fmbSyg": "Add value (dd:hh:mm)", - "efeNi1": "10-run average value", - "awG90C": "Target per run", - "ZNNjWw": "Please enter a number.", - "Vf/QlZ": "Value range", - "NiAH1z": "Target value", - "NMxVd+": "Please fill in the metric value.", - "NLeFGn": "to", - "M4gAc9": "Add value", - "KXVV4+": "Welcome to the playbook preview page!", - "9a9+ww": "Title", - "69nlA3": "Please enter a duration in the format: dd:hh:mm (e.g., 14:00:00).", - "5AJmOz": "When a user joins the channel", - "0RlzlZ": "Send a temporary welcome message to the user", - "u7qh13": "Ready run to your playbook?", - "p1I/Fx": "Your run has been auto-created", - "c23IHq": "Channel actions allow you to automate activities for this channel", - "ao44YC": "Configure metrics", - "Y4MU/9": "Select Start a test run to see it in action.", - "RUlvbf": "Test your new playbook!", - "MHzP9I": "Define a message to welcome users joining the channel.", - "MBNMo9": "Channel Actions", - "DPj6DM": "Select Run to see it in action.", - "B3Q5mz": "Trigger", - "hCMWC+": "Begin following for {followers, plural, =0 {no user} =1 {one user} other {# users}}", - "u4L4yd": "You have unsaved changes", - "e3z3P8": "Discard changes and leave", - "Ob5cSv": "Changes that you made will not be saved if you leave this page. Are you sure you want to discard changes and leave?", - "dCtjdj": "Ready to run your playbook?", - "Z3ybv/": "Add the channel to a sidebar category for the user", - "Ek1Fx2": "When a message with these keywords is posted", - "9j5KzL": "Enter category name", - "2Q5PhZ": "Prompt to run a playbook", - "+/x2FM": "Select a playbook", - "+PMJAg": "Begin following for {followers, plural, =1 {one user} other {# users}}", - "zWgbGg": "Today", - "mLrh+0": "No due date", - "iMjjOH": "Next week", - "aEhjYg": "Outline", - "Ppx673": "Reports", - "MtrTNy": "Tomorrow", - "MbapTE": "{num} {num, plural, =1 {task} other {tasks}} overdue", - "I7+d55": "Specify date/time ('in 4 hours', '1st of May'...)", - "AF7+5o": "Add a due date", - "mw9jVA": "Add a title", - "lyXljU": "Duplicate task", - "lglICE": "Add a description (optional)", - "W0aij2": "Assign to...", - "UlJJ1i": "Add slash command", - "oBeKB4": "Due on {date}", - "lkv547": "Due date (Available in the Professional plan)", - "g9pEhE": "Due", - "TTIQ6E": "Assign due dates to tasks so assignees can prioritise and get things done.", - "NFyWnZ": "Work more effectively", - "oAJsne": "Public playbook", - "mm5vL8": "Only invited members", - "lJ48wN": "Private playbook", - "Xgxruo": "Skip checklist", - "RQl8IW": "Snooze for…", - "OqCzNb": "Add a task", - "JcefuP": "Add a description (optional)", - "9trZXa": "Anyone on the team can view", - "7P5T3W": "Restore checklist", - "371AC3": "Update the run summary", - "v5/Cox": "Duplicate checklist", - "mCrdeS": "Total Playbook Runs", - "cyR7Kh": "Back", - "XF8rrh": "Copy link to ''{name}''", - "MyIJbr": "Contents", - "IxtSML": "Add a checklist", - "CwwzAU": "Add checklist name", - "5ZIN3u": "Status Updates", - "4GjZsL": "Total Playbooks", - "k12r+v": "Add run summary template", - "RrCui3": "Summary", - "xHNF7i": "Run Actions", - "x1phlu": "No time frame", - "sX5Mn5": "Please enter one webhook per line", - "mkLeuq": "Broadcast update to selected channels", - "kkw4kS": "This update will be broadcast to {hasChannels, select, true {{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}} other {}}.", - "kYCbJE": "Add time frame", - "kV5GkX": "When a status update is posted", - "j940pJ": "This update will be saved to overview page.", - "HvAcYh": "{text}{rest, plural, =0 {} one { and other} other { and {rest} others}}", - "28FTjr": "Run actions allow you to automate activities for this channel", - "/RnCQb": "Send outgoing webhook", - "uhDKO8": "Use markdown to create a template", - "giM/X9": "A status update is expected every . New updates will be posted to {channelCount, plural, =0 {no channels} one {# channel} other {# channels}} and {webhookCount, plural, =0 {no outgoing webhooks} one {# outgoing webhook} other {# outgoing webhooks}} .", - "aM44Z/": "Select or specify a custom duration", - "YQOmSf": "Enter one webhook per line", - "XRyRzf": "Status updates are not expected.", - "F9LrJA": "Filter items", - "DaHpK1": "Search for a channel", - "OuZhcQ": "Specify duration ('8 hours', '3 days', etc.)", - "zl6378": "Configure metrics in Retrospective", - "sGJpuF": "Add a description", - "aZGAOI": "Add a status update template", - "OKhRC6": "Share", - "LcC/pi": "Send a welcome message", - "Brya9X": "Add a run summary template", - "9kQNdp": "This playbook is private.", - "3hBelc": "A retrospective is not expected.", - "yllba1": "This archived playbook cannot be renamed.", - "xEQYo5": "Configure custom metrics to fill out with the retrospective report.", - "TD8WrM": "Duplicate is disabled for this team.", - "OQplDX": "A status update is expected every . New updates will be posted to {channelCount, plural, =0 {no channels} one {# channel} other {# channels}} and {webhookCount, plural, =0 {no outgoing webhooks} one {# outgoing webhook} other {# outgoing webhooks}}.", - "vSMfYU": "Run info", - "oL7YsP": "Last edited {timestamp}", - "Z2Hfu4": "Add a run summary", - "opn6uf": "View Timeline", - "o6N9pU": "Run actions", - "lbr3Lq": "Copy link", - "iigkp8": "Time to wrap up?", - "hjteuA": "All the playbooks that you can access will show here", - "bf5rs0": "View Info", - "ZJS10z": "No updates have been posted yet", - "Q15rLN": "Request update", - "GDCpPr": "Recent status update", - "+qDKgW": "View all updates", - "kEMvwX": "There are no runs matching those filters.", - "GXjP8g": "All the runs that you can access will show here", - "ocYb9S": "Key Metrics", - "nc8QpJ": "Recent Activity", - "m/KtHt": "You have insufficient permissions to change the owner", - "lr1CUA": "Browse Playbooks", - "Ul0aFX": "Import Playbook", - "RnOiCg": "It was not possible to {isFollowing, select, true {unfollow} other {follow}} the run", - "LfhTNW": "Browse or create Playbooks and Runs", - "GVpA4Q": "Create New Playbook", - "CFysvS": "Create Playbook Dropdown", - "4mCpAv": "It was not possible to change the owner", - "/qDObA": "Browse Runs", - "UMFnWV": "View Retrospective", - "9xs0pp": "Add value", - "/+8SGX": "Showing {filteredNum} of {totalNum} events", - "jboo9u": "Request update", - "Xx0WZV": "Send message", - "VpQKQE": "{displayName} is not a participant of the run. Would you like to make them a participant? They will have access to all of the message history in the run channel.", - "UePrSL": "{num} {num, plural, one {Participant} other {Participants}}", - "RCT0Px": "Add {displayName} to Channel", - "P9PKvb": "A message was sent to the run channel.", - "NGqzDU": "Confirm request update", - "JvEwg/": "It was not possible to request an update", - "Jli9m7": "A message will be sent to the run channel, requesting an update.", - "KeO51o": "Channel", - "zW/5AB": "Professional feature This is a paid feature, available with a free 30-day trial", - "vDvWJ6": "Try request update with a free trial", - "u6Fyic": "Your request has been sent to the run channel.", - "pzTOmv": "Followers", - "pXWclp": "Your participation request will be sent to the run channel.", - "pFK6bJ": "View all", - "lKeJ+i": "There's no summary", - "ch4Vs1": "Request updates for playbook runs in a single click and get notified directly when an update is posted. Start a free 30-day trial to try it out.", - "U8u4uF": "Get involved", - "P6NEL/": "Command", - "PdRg+3": "View all", - "Nf9oAA": "You're about to join this run.", - "J2NmIY": "Confirm get involved", - "5PpBsd": "Your request wasn't successful.", - "4Iqlfe": "You've joined this run.", - "1fXVVz": "Due date:", - "1GOpgL": "Assignee:", - "wGp7l3": "{icon} Dollars", - "s+rSpl": "{icon} Integer", - "qp5G0Z": "Upgrade required for access to retrospective features.", - "ojQue/": "{icon} Duration (in dd:hh:mm)", - "mNgqXf": "To unlock this feature:", - "j2VYGA": "View all playbooks", - "SMrXWc": "Favourites", - "PWmZrW": "View all runs", - "PW+sL4": "N/A", - "KzHQCQ": "There are no finished runs matching those filters.", - "5HXkY/": "Type: {typeTitle}", - "3zF589": "Reset to all {filterName}", - "xfnuXm": "Participate", - "wRM2AO": "The update request was unsuccessful.", - "wBZz47": "You've left the run.", - "mttASm": "Leave and unfollow run", - "lpWBJE": "Confirm leave and unfollow", - "hnYSP3": "When you leave and unfollow a run, it's removed from the left-hand sidebar. You can find it again by viewing all runs.", - "gfUBRi": "Assign a new owner before you leave the run.", - "ePhhuK": "Your request was sent to the run channel.", - "b+DwLA": "Request to participate in this run.", - "XS4umx": "{name} snoozed a status update", - "SK5APX": "It wasn't possible to leave the run.", - "PoX2HN": "Send request", - "OfN7IN": "An request will be sent to the run channel for a status update.", - "Mjq//Y": "Unfavourite", - "Gwmqz5": "Request an update", - "CV1ddt": "Participate in the run", - "B9z0uZ": "Your request to join the run was unsuccessful.", - "AhY0vJ": "Leave and unfollow", - "AH+V3r": "Become a participant of the run.", - "5Hzwqs": "Favourite", - "+6DCr9": "As a participant, you can post status updates, assign and complete tasks as well as perform retrospectives.", - "iEtImk": "When you leave{isFollowing, select, true { and unfollow a run} other { a run}}, it's removed from the left-hand sidebar. You can find it again by viewing all runs.", - "fnihsY": "Leave", - "egUE/K": "Broadcast to selected channels", - "cnfVhV": "Leave {isFollowing, select, true { and unfollow } other {}}run", - "Xm0L7N": "When a status update is posted, or a retrospective is published", - "Suyx6A": "The playbook import has failed. Please check that JSON is valid and try again.", - "QegBKq": "Join playbook", - "Q4sutg": "Confirm leave{isFollowing, select, true { and unfollow} other {}}", - "P6PLpi": "Join", - "FgydNe": "View", - "qGlwfc": "Start run", - "vqmRBs": "Confirm restart run", - "k5EChD": "Are you sure you want to restart the run?", - "j2FnDV": "A channel will be created with this name", - "iQhFxR": "Last used", - "Zg0obP": "Restart run", - "KjNfA8": "Invalid time duration", - "03oqA2": "Active Runs", - "unwVil": "The join channel request was unsuccessful.", - "ZRv7Dm": "Request to Join", - "XnICdK": "It wasn't possible to join the run", - "M9tXoZ": "A join request will be sent to the run channel.", - "0QD99o": "Request to join channel", - "w4Nhhb": "Add participant", - "q48ca7": "Give feedback about Playbooks.", - "jrOlPO": "Get run status update notifications", - "fVMECF": "Participant", - "cUCiWw": "Become a participant", - "bCmvTY": "Give feedback", - "FLG4Iu": "Make run owner", - "6rygzu": "Remove from run", - "1OVPiC": "Become a participant of the run. As a participant, you can post status updates, assign and complete tasks, and perform retrospectives.", - "0Azlrb": "Manage", - "/GCoTA": "Clear", - "wCDmf3": "Enable updates", - "utHl3F": "Add people to {runName}", - "qDxsQH": "Become a participant to interact with this run", - "nsd54s": "Confirm disable status updates", - "lqzBNa": "Remove them from the run channel", - "l/W5n7": "Participants will also be added to the channel linked to this run", - "jAo8dd": "Run status updates disabled by {name}", - "ieL3dC": "Setup channel actions", - "ha1TB3": "When a participant joins the run", - "cpGAhx": "Are you sure you want to disable status updates for this run?", - "b8Gps8": "Run status updates enabled by {name}", - "Z18I+c": "Channel actions allow you to automate activities for the channel", - "Y1EoT/": "When a participant leaves the run", - "WFA0Cg": "Are you sure you want to enable status updates for this run?", - "WC+NOj": "Also add people to the channel linked to this run", - "H7IzRB": "Disable status updates", - "9qqGGd": "Invite participants", - "5b1zuB": "Add them to the run channel", - "1prgB2": "Search for people", - "1OluNs": "Confirm enable status updates", - "//o1Nu": "Disable updates", - "u/yGzS": "{name} added @{user} to the run", - "t6lwwM": "{requester} removed {users} from the run", - "jfpnye": "@{user} left the run", - "feNxoJ": "{requester} added {users} to the run", - "ecS/qx": "{name} added {num} participants to the run", - "VM75su": "{name} removed {num} participants from the run", - "SwlL5j": "@{user} joined the run", - "RXjd3Q": "{name} removed @{user} from the run", - "zSOvI0": "Filters", - "qxYWTy": "Show all tasks from runs I own", - "grv9Fm": "Select to toggle a list of tasks.", - "YBvwXR": "No assigned tasks", - "TnUG7m": "You don't have any pending task assigned.", - "SRqpbI": "{assignedNum, plural, =0 {No assigned tasks} other {# assigned}}", - "I0NIMp": "Your tasks", - "DUU48k": "There are no tasks explicitly assigned to you. You can expand your search using the filters.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# overdue}}", - "meD+1Q": "RUN PARTICIPANTS", - "iH5e4J": "You’ll also be added to the channel linked to this run.", - "fBG/Ge": "Cost", - "dK2JKl": "Link to an existing channel", - "WFd88+": "Show completed tasks", - "VjJYEV": "e.g. Sales impact, Purchases", - "UAS7Bn": "Request access to the channel linked to this run", - "NGKqOC": "Also add me to the channel linked to this run", - "L6vn9U": "Run participants", - "IdTL+v": "Create a run channel", - "Gg/nch": "NOT PARTICIPATING", - "BJNrYQ": "As a participant, you’ll be able to update the run summary, tick off tasks, post status updates and edit the retrospective.", - "9X3jwi": "{icon} Cost", - "36NwLv": "Manage run participants list", - "2BCWLD": "Configure channel", - "lqceIp": "or Import a playbook", - "a2r7Vb": "Private channel", - "VA1Q/S": "Public channel", - "ORJ0Hb": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run for all participants?", - "AG7PKJ": "Rename run", - "0boT49": "Are you sure you want to finish the run for all participants?", - "zxj2Gh": "Last updated {time}", - "yP3Ud4": "There are no runs in progress linked to this channel", - "tqAmbk": "Runs in progress", - "Z1sgPO": "View finished runs", - "RgQwWr": "Sort runs by", - "NNksk4": "Alphabetically", - "RC6rA2": "Recently created", - "Q/t0//": "Finished runs", - "AoNLta": "There are no finished runs linked to this channel", - "2NDgJq": "Last status update", - "gS1i4/": "Mark the task as done", - "cGCoJe": "Posted by", - "bEoDyV": "@{authorUsername} posted an update for [{runName}]({overviewURL})", - "SRbTcY": "Other playbooks", - "L1tFef": "Please check the spelling or try another search", - "KQunC7": "Used in this channel", - "HfjhwE": "Search playbooks", - "GZoWl1": "Automate activities for this task", - "EVSn9A": "Start a run", - "Bgt0C8": "This update for the run {runName} will be broadcast to {hasChannels, select, true {{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}} other {}}.", - "9AQ5FE": "Run summary", - "95v+5O": "{actions, plural, =0 {Task Actions} one {# action} other {# actions}}", - "7KMbBa": "Never used", - "3sXVwy": "Task Actions...", - "3Yvt4d": "Playbooks are configurable checklists that define a repeatable process for teams to achieve specific and predictable outcomes.", - "0CeyUV": "No results for '\\{searchTerm}'\\", - "zscc/+": "There {outstanding, plural, =1 {is # outstanding task} other {are # outstanding tasks}}. Are you sure you want to finish the run {runName} for all participants?", - "prs4kX": "When a message with specific keywords is posted", - "m8hzTK": "Last used {time}", - "kQAf2d": "Select", - "gGtlrk": "Your playbooks", - "fvNMLo": "Task actions", - "ZSa3cf": "@{targetUsername}, please provide a status update for [{runName}]({playbookURL}).", - "Wy3sw+": "{count, plural, =1{1 run in progress} =0 {No runs in progress} other {# runs in progress}}", - "W1EKh5": "Create new playbook", - "LKu0ex": "Are you sure you want to finish the run {runName} for all participants?", - "QvEO6m": "You do not have permission to edit this run", - "DQn9Uj": "The user {name} is pre-assigned to one or more tasks. Not automatically inviting this user will clear their pre-assignments.{br}{br}Are you sure you want to stop inviting this user as a member of the run?", - "uCS6py": "You do not have permission to see this playbook", - "l3QwVw": "Select channel", - "ksG35Q": "You don't have permission to create playbooks in this workspace.", - "k7Nzfi": "Disable invitation", - "fwW0T1": "Confirm removal of pre-assigned members", - "YKLHXL": "View in progress runs", - "TP/O/b": "Remove user", - "QJTSaI": "Link run to a different channel", - "IE2BzH": "There are users that are pre-assigned to one or more tasks. Disabling invitations will clear all pre-assignments.{br}{br}Are you sure you want to disable invitations?", - "BiQjuS": "Run moved to {channel}", - "9w0mDI": "Confirm removal of pre-assigned member", - "mILd++": "The run name should not exceed {maxLength} characters", - "uYrkxy": "The file must be a valid JSON playbook template.", - "m4vqJl": "Files", - "Zbk+OU": "The file size exceeds the limit of 5MB.", - "MieztS": "Drop a playbook export file to import it.", - "HGSVzc": "Unable to import multiple files at once.", - "XHJUSG": "Auto-follow runs", - "LaseGE": "You do not have permission to edit this checklist", - "Edy3wX": "Checklist moved to {channel}", - "DqTQOp": "Once", - "8//+Yb": "Link checklist to a different channel", - "706Soh": "tasks done", - "vjb+hS": "{user} restored checklist item '\\{name}'\\", - "OqWwvQ": "{user} unticked checklist item '\\{name}'\\", - "8FzC0B": "{user} ticked off checklist item '\\{name}'\\", - "DKiv0o": "{user} skipped checklist item '\\{name}'\\", - "3qPQMX": "{name} requested a status update", - "9M92On": "Select channels" -} diff --git a/webapp/playbooks/i18n/es.json b/webapp/playbooks/i18n/es.json deleted file mode 100644 index 1e78d7f9356..00000000000 --- a/webapp/playbooks/i18n/es.json +++ /dev/null @@ -1,453 +0,0 @@ -{ - "soePYH": "{num_checklists, plural, =0 {no hay checklists} one {# checklist} other {# checklists}}", - "s3jjqi": "{num_actions, plural, =0 {no hay acciones} one {# acción} other {# acciones}}", - "YDuW/T": "{num_runs, plural, =0 {No hay ejecuciones aún} one {# ejecución} other {# ejecuciones en total}}", - "zz6ObK": "", - "Ja1sVR": "", - "wbwhbH": "", - "5wqhGy": "", - "wO6NOM": "", - "4vuNrq": "", - "MFpAtm": "", - "kDcpd/": "", - "HLn43R": "", - "2VrVHu": "", - "C6Oghd": "", - "x5Tz6M": "", - "15jbT0": "", - "OsDomv": "", - "2563nT": "", - "d4g2r8": "", - "9PXW6Q": "", - "9uOFF3": "", - "C1khRR": "", - "iXNbPf": "", - "4ltHYh": "", - "DSVJjB": "", - "6n0XDG": "", - "9kCT7Q": "", - "q0cpUe": "", - "B487HA": "", - "9TTfXU": "", - "A8dbCS": "", - "eiPBw7": "", - "+hddg7": "", - "X2K92H": "", - "AML4RW": "", - "5A46pW": "", - "lxfpbh": "", - "SDSqfA": "", - "QUwMsX": "", - "Lg3I1b": "", - "zWkvNO": "", - "0oLj/t": "", - "ypIsVG": "", - "h+e7G+": "", - "47FYwb": "", - "ApULhK": "", - "Q7hMnp": "", - "Q7aZO4": "", - "BD66u6": "", - "0Vvpht": "", - "9qc7BX": "", - "XmUdvV": "", - "5CI3KH": "", - "5Ofkag": "", - "IuFETn": "", - "C9NScU": "", - "/MaJux": "", - "/ZsEUy": "", - "FXCLuZ": "", - "/YZ/sw": "", - "jnmORb": "", - "/gbqA6": "", - "lbhO3D": "", - "0q+hj2": "", - "OcpRSQ": "", - "o+ZEL3": "", - "1MQ3XZ": "", - "z3A0LP": "", - "BNB75h": "", - "BQtd5I": "", - "Leh2tk": "", - "hrgo+E": "", - "5ciuDD": "", - "GRTyvN": "", - "yxguVq": "", - "yqpcOa": "", - "vaYTD+": "", - "pK6+CW": "", - "m/Q4ye": "", - "lrbrjv": "", - "lJyq2a": "", - "jvo0vs": "", - "jIgqRa": "", - "j7jdWG": "", - "hVFgh4": "", - "g5pX+a": "", - "g0mp+I": "", - "eHAvFf": "", - "d8KvXJ": "", - "bLK+Kr": "", - "avPeEI": "", - "Z/hwEf": "", - "YORRGQ": "", - "W/V6+Y": "", - "VmnoW8": "", - "VOzlSL": "", - "V5TY0z": "", - "Ui6GK/": "", - "TxCTXQ": "", - "TJo5E6": "", - "T7Ry38": "", - "SmAUf9": "", - "RthEJt": "", - "RoGxij": "", - "RO+BaS": "", - "QywYDe": "", - "Q8Qw5B": "", - "Q67RuY": "", - "OK8u0r": "", - "GwtR3W": "", - "yhU1et": "", - "vndQuC": "", - "syEQFE": "", - "sIX63S": "", - "sDKojV": "", - "ryrP8K": "", - "ruJGqS": "", - "qsr3Zk": "", - "q6f8x9": "", - "o2eHmz": "", - "nqVby7": "", - "nSFBC2": "", - "jwimQJ": "", - "iNU1lj": "", - "hzt6l8": "", - "hfrrC7": "", - "b40Pr7": "", - "aYIUar": "", - "YKn+7s": "", - "W1Qs5O": "", - "OHfpS1": "", - "Nh91Us": "", - "NA7Cw1": "", - "L6k6aT": "", - "KJu1sq": "", - "K4O03z": "", - "JXdbo8": "", - "JJNc3c": "", - "k1djnL": "", - "tVPYMu": "", - "lQT7iD": "", - "gGcNUr": "", - "SXJ98n": "", - "8oCVbz": "", - "wylJpv": "", - "t6SiGO": "", - "R/2lqw": "", - "QpUBDr": "", - "MJ89uW": "", - "EvBQLq": "", - "EWz2w5": "", - "5BUxvl": "", - "3Ls2m+": "", - "0tznw6": "", - "wL7VAE": "", - "viXE32": "", - "osuP6z": "", - "jS/UOn": "", - "SENRqu": "", - "S0kWcH": "", - "Lo10yH": "", - "jIIWN+": "", - "iDMOiz": "", - "JqKASQ": "", - "uhu5aG": "", - "D9IV7i": "", - "MrJPOh": "", - "zx0myy": "", - "N1U/QR": "", - "LmhSmU": "", - "I5NMJ8": "", - "G/yZLu": "", - "FEGywG": "", - "D2CE02": "", - "CyGaem": "", - "Cy1AK/": "", - "AF9wda": "", - "5qBEKB": "", - "2/2yg+": "", - "O8o2lE": "", - "cPIKU2": "", - "UMoxP9": "", - "T5rX+W": "", - "twieZh": "", - "cp7KUI": "", - "pjt3qA": "", - "fuDLDJ": "", - "EQpfkS": "", - "3MSGcL": "", - "l7zMH6": "", - "l0hFoB": "", - "kGI46P": "", - "DXACD6": "", - "5j6GD/": "", - "0oL1zz": "", - "z3B83t": "", - "w0muFd": "", - "vjzpnC": "", - "fpuWL1": "", - "Y+U8La": "", - "K3r6DQ": "", - "Vhnd2J": "", - "dSC1YD": "", - "b3TdyZ": "", - "7VTSeD": "", - "/4tOwT": "", - "k9q07e": "", - "XXbWAU": "", - "+Tmpup": "", - "bGhCLX": "", - "b5FaCc": "", - "36GNZj": "", - "0HT+Ib": "", - "ZWtlyd": "", - "xmcVZ0": "", - "x8cvBr": "", - "wZ83YL": "", - "wX3k9U": "", - "uBLF+D": "", - "u4MwUB": "", - "tzMNF3": "", - "jXT2++": "", - "gt6BhE": "", - "g4IF1x": "", - "egvJrY": "", - "edxtzC": "", - "eLeFE2": "", - "dvhvum": "", - "djALPR": "", - "YMrTRm": "", - "Oo5sdB": "", - "IfxUgC": "", - "IOnm/Z": "", - "fUEpLA": "", - "dsTLW1": "", - "b/QBNs": "", - "aACJNp": "", - "ZdWYcm": "", - "ZAJviT": "", - "Z7vWDQ": "", - "X/koAN": "", - "JJMNME": "", - "E0LnBo": "", - "fmylXu": "", - "WTQpnI": "", - "WIxhrv": "", - "WAHCT2": "", - "R+JQaJ": "", - "Qrl6bQ": "", - "N2IrpM": "", - "MvEydR": "", - "GxJAK1": "", - "EC5MJD": "", - "D55vrs": "", - "vNiZXF": "", - "uny3Zy": "", - "nmpevl": "", - "nkCCM2": "", - "ijAUQf": "", - "guunZt": "", - "fV6578": "", - "e/AZL5": "", - "bE1Cro": "", - "aWpBzj": "", - "TdTXXf": "", - "TBez4r": "", - "QiKcO7": "", - "QaZNp9": "", - "Mm1Gse": "", - "M/2yY/": "", - "Ietscn": "", - "I90sbW": "", - "HSi3uv": "", - "HAlOn1": "", - "DuRxjT": "", - "DtCplA": "", - "CkYhdY": "", - "CSts8B": "", - "CBM4vh": "", - "Auj1ap": "", - "ArpdYl": "", - "A21Mgv": "", - "9tBhzB": "", - "9Obw6C": "", - "91Hr5f": "", - "9+Ddtu": "", - "6uhSSw": "", - "6jDabx": "", - "6CGo3o": "", - "42qmJ5": "", - "3Psa+5": "", - "2Qq4YX": "", - "2QkJ4s": "", - "2PNrBQ": "", - "0wJ7N+": "", - "zELxbG": "", - "wbsq7O": "", - "v1SpKO": "", - "pKLw8O": "", - "ieGrWo": "", - "fXGjhC": "", - "JeqL8w": "", - "I2zEie": "", - "v1DNMW": "", - "4Hrh5B": "", - "3/wF0G": "", - "hXIYHG": "", - "bPLen5": "", - "wsUmh9": "", - "wcWpGs": "", - "sqNmlF": "", - "scYyVv": "", - "sVlNlY": "", - "recCg9": "", - "rbrahO": "", - "rDvvQs": "", - "qyJtWy": "", - "qp3Fk4": "", - "oVHn4s": "", - "kvgvNW": "", - "kXFojL": "", - "usa8vQ": "", - "hO9EdA": "", - "d9epHh": "", - "c8hxKk": "", - "OINwWS": "", - "LRFvqz": "", - "KUr+sG": "", - "Hzwzgs": "", - "CjNrqO": "", - "8hDbW6": "", - "+QgvjN": "", - "sQu1rA": "", - "waVyVY": "", - "wEQDC6": "", - "oS0w4E": "", - "lZwZi+": "", - "DnBhRg": "", - "zINlao": "", - "QnZAit": "", - "ObmjTB": "", - "ICqy9/": "", - "rX08cW": "", - "gy/Kkr": "", - "UbTsGY": "", - "TZYiF/": "", - "TSSNg/": "", - "KiXNvz": "", - "JCGvY/": "", - "HhLp57": "", - "DCl7Vv": "", - "3rCdDw": "", - "1I48bs": "", - "+8G9qr": "", - "AS5kar": "", - "5FRgqE": "", - "/jUtaM": "", - "/1FEJW": "", - "/HtNUp": "", - "8n24G2": "", - "LI7YlB": "", - "dZmYk6": "", - "wPVxBN": "", - "6GTzTR": "", - "F4pfM/": "", - "mbo96h": "", - "XpDetT": "", - "rMhrJH": "", - "mVpO8u": "", - "QbGfqo": "", - "vQqT/8": "", - "f+bqgK": "", - "Q5hysF": "", - "0Xt1ea": "", - "GG1yhI": "", - "HXvk56": "", - "hw83pa": "", - "cEWBE3": "", - "tbjmvS": "", - "fhMaTZ": "", - "rzbYbE": "", - "GjCS6U": "", - "1ikfp3": "", - "udrLSP": "", - "1QosTr": "", - "q/Qo8l": "", - "y7o4Rn": "", - "wbdGb5": "", - "vL4++D": "", - "lgZf0l": "", - "R5Zh+l": "", - "Q3R9Uj": "", - "Pue+oV": "", - "9m0I/B": "", - "a0hBZ0": "", - "ZkhArX": "", - "Tt04f1": "", - "RzEVnf": "", - "JrZ2th": "", - "1isgPF": "", - "FGzxgY": "", - "0EEIkR": "", - "gsMPAS": "", - "NYTGIb": "", - "uT4ebt": "", - "TxmjKI": "", - "Sx3lHL": "", - "VZRWFk": "", - "OyZnsJ": "", - "6D6ffM": "", - "4BN53Q": "", - "xvBDOH": "", - "lBqu4h": "", - "bTgMQ2": "", - "MTzF3S": "", - "4cwL43": "", - "4aupaG": "", - "SVwJTM": "", - "9XUYQt": "", - "4alprY": "", - "/urtZ8": "", - "4fHiNl": "", - "3PoGhY": "", - "9SIW2x": "", - "lUfDe1": "", - "/fU9y/": "", - "dxyZg3": "", - "vJ2SaW": "", - "HGdWwZ": "", - "q/VD+s": "", - "I5DYM+": "", - "LDYFkN": "", - "NJ9uPu": "", - "GAuN6w": "", - "Vf/QlZ": "", - "ru+JCk": "", - "mvZUm3": "", - "efeNi1": "", - "KXVV4+": "", - "NMxVd+": "", - "xVyHgP": "", - "lbs7UO": "", - "NiAH1z": "", - "awG90C": "", - "fmbSyg": "", - "l5/RKZ": "", - "M4gAc9": "", - "NLeFGn": "", - "ZNNjWw": "", - "9a9+ww": "", - "69nlA3": "" -} diff --git a/webapp/playbooks/i18n/fa.json b/webapp/playbooks/i18n/fa.json deleted file mode 100644 index 41f8a4ebeb2..00000000000 --- a/webapp/playbooks/i18n/fa.json +++ /dev/null @@ -1,459 +0,0 @@ -{ - "hXIYHG": "", - "6n0XDG": "", - "tVPYMu": "", - "dvhvum": "", - "EC5MJD": "", - "ObmjTB": "", - "OINwWS": "", - "lZwZi+": "", - "k9q07e": "", - "3Ls2m+": "", - "d8KvXJ": "", - "3/wF0G": "", - "uBLF+D": "", - "Ja1sVR": "", - "FEGywG": "", - "15jbT0": "افزودن موارد بیشتر به تایملاین", - "KJu1sq": "", - "3MSGcL": "", - "q0cpUe": "", - "ApULhK": "", - "k1djnL": "", - "hfrrC7": "", - "/YZ/sw": "", - "UMoxP9": "", - "B487HA": "", - "wcWpGs": "", - "d9epHh": "", - "0tznw6": "", - "e/AZL5": "", - "DCl7Vv": "", - "vaYTD+": "", - "5CI3KH": "", - "eHAvFf": "", - "8hDbW6": "", - "bLK+Kr": "", - "l7zMH6": "", - "guunZt": "", - "u4MwUB": "", - "zx0myy": "", - "OsDomv": "", - "/MaJux": "", - "9qc7BX": "", - "hVFgh4": "", - "6jDabx": "", - "+hddg7": "", - "5Ofkag": "", - "w0muFd": "", - "IfxUgC": "", - "9+Ddtu": "", - "wylJpv": "", - "AF9wda": "", - "X/koAN": "", - "5A46pW": "", - "9tBhzB": "", - "9Obw6C": "", - "lrbrjv": "", - "0oL1zz": "کپی شد!", - "5ciuDD": "", - "osuP6z": "", - "91Hr5f": "", - "wbsq7O": "", - "JqKASQ": "", - "2QkJ4s": "", - "fuDLDJ": "", - "g5pX+a": "", - "kvgvNW": "", - "KUr+sG": "", - "zELxbG": "", - "D9IV7i": "", - "UbTsGY": "", - "lQT7iD": "", - "g4IF1x": "", - "Auj1ap": "", - "C6Oghd": "", - "7VTSeD": "", - "SmAUf9": "", - "l0hFoB": "", - "Z/hwEf": "", - "fpuWL1": "", - "gt6BhE": "", - "wEQDC6": "", - "g0mp+I": "", - "Q67RuY": "", - "gGcNUr": "", - "HhLp57": "", - "+QgvjN": "", - "6CGo3o": "", - "0q+hj2": "", - "iDMOiz": "", - "jnmORb": "", - "x5Tz6M": "", - "yhU1et": "", - "wO6NOM": "", - "lJyq2a": "", - "/1FEJW": "", - "SXJ98n": "", - "+Tmpup": "", - "zz6ObK": "", - "2PNrBQ": "", - "vndQuC": "", - "edxtzC": "", - "z3B83t": "", - "pjt3qA": "", - "d4g2r8": "", - "9uOFF3": "", - "pKLw8O": "", - "viXE32": "", - "lbhO3D": "", - "bE1Cro": "", - "L6k6aT": "", - "JXdbo8": "", - "JJMNME": "", - "GxJAK1": "", - "GwtR3W": "", - "FXCLuZ": "", - "wL7VAE": "", - "syEQFE": "", - "scYyVv": "", - "ryrP8K": "", - "rbrahO": "", - "qyJtWy": "", - "oVHn4s": "", - "o2eHmz": "", - "nkCCM2": "", - "lxfpbh": "", - "kXFojL": "", - "jwimQJ": "", - "jvo0vs": "", - "jXT2++": "", - "ieGrWo": "", - "iNU1lj": "", - "hzt6l8": "", - "hrgo+E": "", - "fUEpLA": "", - "eiPBw7": "", - "egvJrY": "", - "bGhCLX": "", - "aYIUar": "", - "aACJNp": "", - "ZdWYcm": "", - "ZWtlyd": "", - "ZAJviT": "", - "YORRGQ": "", - "YDuW/T": "", - "Y+U8La": "", - "WIxhrv": "", - "W/V6+Y": "", - "VmnoW8": "", - "TxCTXQ": "", - "TdTXXf": "", - "TBez4r": "", - "SENRqu": "", - "SDSqfA": "", - "QywYDe": "", - "QiKcO7": "", - "QaZNp9": "", - "Q7hMnp": "", - "Q7aZO4": "", - "M/2yY/": "", - "LmhSmU": "", - "Lg3I1b": "", - "LRFvqz": "", - "JJNc3c": "", - "sDKojV": "", - "qsr3Zk": "", - "HSi3uv": "", - "DtCplA": "", - "C9NScU": "", - "vNiZXF": "", - "o+ZEL3": "", - "8oCVbz": "", - "R/2lqw": "", - "QpUBDr": "", - "MJ89uW": "", - "HLn43R": "", - "EvBQLq": "", - "EWz2w5": "", - "5BUxvl": "", - "0Vvpht": "", - "wsUmh9": "", - "Lo10yH": "", - "pK6+CW": "", - "XmUdvV": "", - "CkYhdY": "", - "eLeFE2": "", - "Z7vWDQ": "", - "2563nT": "", - "djALPR": "", - "dSC1YD": "", - "cp7KUI": "", - "X2K92H": "", - "WTQpnI": "", - "V5TY0z": "", - "Ui6GK/": "", - "T5rX+W": "", - "I5NMJ8": "", - "soePYH": "", - "nqVby7": "", - "nSFBC2": "", - "m/Q4ye": "", - "jIgqRa": "", - "ijAUQf": "", - "iXNbPf": "", - "fV6578": "", - "bPLen5": "", - "b/QBNs": "", - "avPeEI": "", - "RO+BaS": "", - "MrJPOh": "", - "hO9EdA": "", - "OHfpS1": "", - "KiXNvz": "", - "K4O03z": "", - "IuFETn": "", - "Ietscn": "", - "9kCT7Q": "", - "5FRgqE": "", - "2/2yg+": "", - "/ZsEUy": "", - "z3A0LP": "", - "yxguVq": "", - "yqpcOa": "", - "s3jjqi": "", - "O8o2lE": "", - "NA7Cw1": "", - "N2IrpM": "", - "WAHCT2": "", - "W1Qs5O": "", - "T7Ry38": "", - "Oo5sdB": "", - "OcpRSQ": "", - "Nh91Us": "", - "N1U/QR": "", - "MFpAtm": "", - "IOnm/Z": "", - "Hzwzgs": "", - "HAlOn1": "", - "EQpfkS": "", - "DuRxjT": "", - "DnBhRg": "", - "DXACD6": "", - "DSVJjB": "", - "D55vrs": "", - "D2CE02": "", - "CyGaem": "", - "C1khRR": "", - "AS5kar": "", - "AML4RW": "", - "4vuNrq": "", - "4ltHYh": "", - "4Hrh5B": "", - "47FYwb": "", - "42qmJ5": "", - "/gbqA6": "", - "zWkvNO": "", - "zINlao": "", - "ypIsVG": "", - "waVyVY": "", - "wZ83YL": "", - "vjzpnC": "", - "v1DNMW": "", - "usa8vQ": "", - "uny3Zy": "", - "uhu5aG": "", - "tzMNF3": "", - "twieZh": "", - "t6SiGO": "", - "sQu1rA": "", - "sIX63S": "", - "qp3Fk4": "", - "kGI46P": "", - "kDcpd/": "", - "jIIWN+": "", - "j7jdWG": "", - "h+e7G+": "", - "gy/Kkr": "", - "YMrTRm": "", - "YKn+7s": "", - "XXbWAU": "", - "Vhnd2J": "", - "VOzlSL": "", - "TZYiF/": "", - "TSSNg/": "", - "TJo5E6": "", - "S0kWcH": "", - "RthEJt": "", - "RoGxij": "", - "R+JQaJ": "", - "Qrl6bQ": "", - "QUwMsX": "", - "Q8Qw5B": "", - "OK8u0r": "", - "MvEydR": "", - "Mm1Gse": "", - "Leh2tk": "", - "K3r6DQ": "", - "JeqL8w": "", - "JCGvY/": "", - "ICqy9/": "", - "I90sbW": "", - "I2zEie": "", - "GRTyvN": "", - "G/yZLu": "", - "E0LnBo": "", - "CjNrqO": "", - "CSts8B": "", - "CBM4vh": "", - "BQtd5I": "", - "BNB75h": "", - "BD66u6": "", - "ArpdYl": "", - "A8dbCS": "", - "A21Mgv": "", - "9TTfXU": "", - "9PXW6Q": "", - "6uhSSw": "", - "Cy1AK/": "", - "QnZAit": "", - "ruJGqS": "", - "b3TdyZ": "", - "dsTLW1": "", - "xmcVZ0": "", - "x8cvBr": "", - "wbwhbH": "", - "wX3k9U": "", - "v1SpKO": "", - "sqNmlF": "", - "sVlNlY": "", - "recCg9": "", - "rX08cW": "", - "rDvvQs": "", - "q6f8x9": "", - "oS0w4E": "", - "nmpevl": "", - "jS/UOn": "", - "fmylXu": "", - "fXGjhC": "", - "cPIKU2": "", - "c8hxKk": "", - "b5FaCc": "", - "b40Pr7": "", - "aWpBzj": "", - "5wqhGy": "", - "5qBEKB": "", - "5j6GD/": "", - "3rCdDw": "", - "3Psa+5": "", - "36GNZj": "", - "2VrVHu": "", - "2Qq4YX": "", - "1MQ3XZ": "", - "1I48bs": "", - "0wJ7N+": "", - "0oLj/t": "بازکردن", - "0HT+Ib": "آرشیو شده", - "/jUtaM": "", - "/HtNUp": "", - "/4tOwT": "", - "+8G9qr": "", - "rzbYbE": "", - "tbjmvS": "", - "I5DYM+": "", - "6D6ffM": "", - "q/VD+s": "", - "Q3R9Uj": "", - "/fU9y/": "", - "VZRWFk": "", - "hw83pa": "", - "udrLSP": "", - "cEWBE3": "", - "OyZnsJ": "", - "F4pfM/": "", - "ZkhArX": "", - "1QosTr": "", - "GAuN6w": "", - "Pue+oV": "", - "NJ9uPu": "", - "4BN53Q": "", - "wPVxBN": "", - "0Xt1ea": "", - "Sx3lHL": "", - "NYTGIb": "", - "q/Qo8l": "", - "8n24G2": "", - "XpDetT": "", - "LI7YlB": "", - "9SIW2x": "", - "uT4ebt": "", - "mbo96h": "", - "FGzxgY": "", - "JrZ2th": "", - "1ikfp3": "", - "f+bqgK": "", - "gsMPAS": "", - "wbdGb5": "", - "vL4++D": "", - "fhMaTZ": "", - "Tt04f1": "", - "HGdWwZ": "", - "QbGfqo": "", - "HXvk56": "", - "1isgPF": "", - "lgZf0l": "", - "RzEVnf": "", - "GG1yhI": "", - "y7o4Rn": "", - "dZmYk6": "", - "vQqT/8": "", - "rMhrJH": "", - "a0hBZ0": "", - "6GTzTR": "", - "0EEIkR": "", - "mVpO8u": "", - "xvBDOH": "", - "lBqu4h": "", - "bTgMQ2": "", - "MTzF3S": "", - "4cwL43": "", - "4aupaG": "", - "SVwJTM": "", - "9XUYQt": "", - "4alprY": "", - "/urtZ8": "", - "3PoGhY": "", - "4fHiNl": "", - "R5Zh+l": "", - "LDYFkN": "", - "dxyZg3": "", - "lUfDe1": "", - "9m0I/B": "", - "GjCS6U": "", - "TxmjKI": "", - "Q5hysF": "", - "vJ2SaW": "", - "awG90C": "", - "69nlA3": "", - "Vf/QlZ": "", - "M4gAc9": "", - "NiAH1z": "", - "fmbSyg": "", - "xVyHgP": "", - "efeNi1": "", - "ZNNjWw": "", - "NLeFGn": "", - "NMxVd+": "", - "KXVV4+": "", - "9a9+ww": "", - "lbs7UO": "", - "l5/RKZ": "", - "mvZUm3": "", - "ru+JCk": "", - "0RlzlZ": "ارسال پیام خوشآمدگویی موقت به کاربر", - "0QD99o": "درخواست عضویت در کانال", - "0Azlrb": "مدیریت", - "//o1Nu": "غیرفعال کردن بروزرسانی", - "/+8SGX": "نمایش {filteredNum} از {totalNum} رخداد", - "+qDKgW": "مشاهده همه بروزرسانی ها" -} diff --git a/webapp/playbooks/i18n/fr.json b/webapp/playbooks/i18n/fr.json deleted file mode 100644 index 2d2769938e2..00000000000 --- a/webapp/playbooks/i18n/fr.json +++ /dev/null @@ -1,513 +0,0 @@ -{ - "QnZAit": "Ajouter une description facultative", - "QiKcO7": "Entrer le modèle de rétrospective", - "Q8Qw5B": "Description", - "ObmjTB": "Commande slash", - "NE1OeI": "Tout le monde de l'équipe ({team}) peut accéder.", - "KiXNvz": "Exécuter", - "IuFETn": "Durée", - "HhLp57": "citation", - "EC5MJD": "Pas de mise à jour disponible.", - "DnBhRg": "Ajouter des personnes", - "D3idYv": "Paramètres", - "CL5OZP": "Seulement les utilisateurs que vous sélectionnez pourront modifier ou exécuter ce playbook.", - "BD66u6": "Télécharger a CSV contenant tous les messages de ce canal", - "AT2QBo": "Seulement les utilisateurs sélectionnés peuvent créer des playbooks.", - "AS5kar": "Participants ({participants})", - "A3ptul": "Modèles", - "9uOFF3": "Aperçu", - "6Lwe7T": "Tout le monde dans {team} peu accéder à ce playbook", - "5Ot7cd": "Déterminer le type de canal créé par ce playbook.", - "5FRgqE": "Télécharger les logs du canal", - "47FYwb": "Annuler", - "3rCdDw": "Mises à jour des statuts", - "1I48bs": "Modèle de rétrospective", - "/1FEJW": "PARTICIPANTS ACTIFS par jour sur les 14 derniers jours", - "+ZIXOR": "Accès au canal", - "+8G9qr": "Texte par défaut pour la rétrospective.", - "/4tOwT": "Passer", - "+Tmpup": "Vous recevrez automatiquement des mises à jour quand ce playbook sera lancé.", - "zINlao": "Propriétaire", - "lxfpbh": "Le propriétaire {reminderEnabled, select, true {recevra un rappel pour envoyer une mise à jour de statut tous les} other {ne recevra pas de rappel pour envoyer une mise à jour de statut}}", - "jIgqRa": "Propriétaire / Participants", - "fXGjhC": "Propriétaire changé depuis {summary}", - "fV6578": "Assigner le rôle de propriétaire", - "+QgvjN": "Assigner le rôle de propriétaire à", - "C9NScU": "Donnez le contrôle à votre équipe", - "FXCLuZ": "{total, number} total", - "9kCT7Q": "Facilitez les rétrospectives grâce à une chronologie qui garde automatiquement la trace des événements et des messages clés afin que les équipes les aient à portée de main.", - "MrJPOh": "Activez les mises à jour de statut", - "ArpdYl": "Les événements de la chronologie sont affichés ici au fur et à mesure qu'ils se produisent. Passez la souris sur un événement pour le supprimer.", - "6n0XDG": "Êtes-vous sûr de vouloir supprimer la liste de contrôle ? Toutes les tâches seront supprimées.", - "0q+hj2": "Définir un modèle pour une description concise qui explique chaque run à ses parties prenantes.", - "91Hr5f": "Faites-moi glisser pour réorganiser", - "o+ZEL3": "", - "pK6+CW": "", - "0oLj/t": "Etendre", - "wEQDC6": "", - "sDKojV": "", - "h+e7G+": "", - "5wqhGy": "Afficher les détails du run", - "scYyVv": "", - "jnmORb": "", - "sqNmlF": "", - "5j6GD/": "", - "s3jjqi": "", - "L6k6aT": "...ou commencer avec un modèle", - "wbsq7O": "", - "HSi3uv": "Personne d'assigné", - "x8cvBr": "", - "kGI46P": "", - "2QkJ4s": "Enregistrez les messages importants pour obtenir une image complète qui simplifie les rétrospectives.", - "OK8u0r": "", - "WTQpnI": "", - "Qrl6bQ": "", - "yxguVq": "", - "wylJpv": "", - "sIX63S": "", - "pjt3qA": "", - "OcpRSQ": "", - "NA7Cw1": "Copier le lien vers le playbook", - "N1U/QR": "", - "JeqL8w": "Rétrospective annulée par {name}", - "JXdbo8": "Fait", - "JJNc3c": "Précédent", - "JJMNME": "", - "JCGvY/": "Ce modèle permet de normaliser le format des mises à jour récurrentes qui ont lieu tout au long de chaque run à conserver.", - "QpUBDr": "", - "syEQFE": "", - "lbhO3D": "", - "lQT7iD": "", - "gGcNUr": "", - "g0mp+I": "", - "SXJ98n": "", - "8oCVbz": "", - "OINwWS": "", - "MJ89uW": "Convertir en playbook privé", - "LRFvqz": "", - "HLn43R": "Gérer l'accès", - "EvBQLq": "", - "EWz2w5": "Lancer un Playbook", - "5BUxvl": "Tous les membres de cette équipe peuvent voir ce playbook.", - "3Ls2m+": "Membre du playbook", - "0tznw6": "Convertir en Playbook privé", - "0Vvpht": "Faire le Playbook de Membre", - "g4IF1x": "", - "dSC1YD": "", - "Nh91Us": "", - "N2IrpM": "Confirmer", - "MFpAtm": "", - "Lo10yH": "Canal inconnu", - "QywYDe": "", - "2563nT": "Confirmer la fin du run", - "5Ofkag": "Activer la retrospective", - "viXE32": "", - "Ja1sVR": "Les mises à jour de statut ont été désactivées pour cette exécution du playbook.", - "k1djnL": "", - "iXNbPf": "", - "I5NMJ8": "Plus", - "E0LnBo": "Vous pouvez sélectionner une option ou spécifier une durée personnalisée (\"2 semaines\", \"3 jours 12 heures\", \"45 minutes\" ...)", - "/ZsEUy": "Êtes-vous sûr de vouloir supprimer cette liste de vérification ? Elle sera retirée de cette instance sans affecter le playbook.", - "O8o2lE": "", - "MvEydR": "{name} a publié une mise à jour", - "FEGywG": "Veuillez indiquer une date/heure ultérieure pour le rappel de mise à jour.", - "4vuNrq": "{duration} après le lancement du run", - "/gbqA6": "{duration} avant le lancement du run", - "C6Oghd": "Modifier le résumé du run", - "yqpcOa": "", - "oS0w4E": "", - "cp7KUI": "", - "3MSGcL": "Le nom du canal n'est pas valide.", - "T7Ry38": "", - "T5rX+W": "", - "QUwMsX": "", - "EQpfkS": "Terminé", - "9TTfXU": "Votre administrateur système a été informé.", - "0oL1zz": "Copié !", - "z3B83t": "", - "vndQuC": "", - "vjzpnC": "", - "fpuWL1": "", - "K3r6DQ": "Supprimer", - "zWkvNO": "", - "Vhnd2J": "", - "XXbWAU": "", - "X/koAN": "", - "TBez4r": "", - "M/2yY/": "Personne pour l'instant.", - "K4O03z": "Nouvelle tâche", - "7VTSeD": "", - "15jbT0": "Ajoutez plus à votre chronologie", - "wZ83YL": "", - "wX3k9U": "", - "sVlNlY": "", - "hzt6l8": "", - "5qBEKB": "Qu'est ce qu'un playbook run ?", - "0wJ7N+": "", - "/jUtaM": "RUNS ACTIFS par jour sur les 14 derniers jours", - "z3A0LP": "", - "wL7VAE": "", - "6jDabx": "Donner un feedback", - "6CGo3o": "Statut / Dernière mise à jour", - "36GNZj": "Le playbook {title} a été archivé avec succès.", - "2Qq4YX": "", - "0HT+Ib": "Archivé", - "eLeFE2": "", - "ZWtlyd": "", - "dvhvum": "", - "djALPR": "", - "YMrTRm": "", - "Oo5sdB": "", - "IfxUgC": "Ajouter un résumé du run…", - "IOnm/Z": "Il n'y a pas de résumé du run de disponible.", - "GwtR3W": "Faites glisser et déposez une tâche existante ou cliquez pour créer une nouvelle tâche.", - "GRTyvN": "Basculer la liste des playbooks", - "G/yZLu": "Supprimer", - "HAlOn1": "Nom", - "GxJAK1": "Le playbook que vous demandez est privé ou n'existe pas.", - "DuRxjT": "", - "DtCplA": "{numParticipants, plural, =1 {# participant} other {# participants}}", - "DXACD6": "Publier le rapport rétrospectif et accéder à la chronologie", - "D2CE02": "Saisir un webhook", - "CyGaem": "Nom du run", - "Cy1AK/": "Voir les détails du run", - "A8dbCS": "Playbook non trouvé", - "A21Mgv": "Run terminé", - "9qc7BX": "", - "nmpevl": "", - "k9q07e": "", - "jwimQJ": "", - "iNU1lj": "", - "hfrrC7": "", - "hVFgh4": "", - "guunZt": "", - "b/QBNs": "", - "aYIUar": "", - "WIxhrv": "", - "WAHCT2": "", - "W1Qs5O": "", - "RthEJt": "", - "Lg3I1b": "@{targetUsername}, veuillez fournir une mise à jour.", - "Leh2tk": "", - "I90sbW": "à l'instant", - "I2zEie": "", - "DSVJjB": "Le playbook {playbookTitle} est actuellement en cours d'exécution", - "DCl7Vv": "code en ligne", - "D55vrs": "Votre licence n'a pas pu être générée", - "CkYhdY": "Ajouter la chaîne à une catégorie de la barre latérale", - "CSts8B": "Icône d'équipe", - "CBM4vh": "Timer pour la prochaine update", - "BQtd5I": "Bienvenue dans Playbooks !", - "B487HA": "En cours", - "Auj1ap": "Démarrer votre essai ou mettez votre abonnement à niveau.", - "ApULhK": "Inviter des membres", - "AF9wda": "Cette mise à jour sera enregistrée sur la page d'aperçu{hasBroadcast, select, true { et diffusée sur {broadcastChannelCount, plural, =1 {un canal} d'autres {{broadcastChannelCount, number} canaux}}} d'autres {}}.", - "6uhSSw": "Selectionner un canal", - "5CI3KH": "Contacter le support", - "4ltHYh": "Aller au playbook", - "42qmJ5": "Vous n'avez pas le droit de poster une mise à jour.", - "3Psa+5": "", - "/YZ/sw": "Commencer un essai", - "/MaJux": "Commencer la rétrospective", - "+hddg7": "Ajouter à la chronologie du run", - "t6SiGO": "", - "lZwZi+": "", - "jvo0vs": "", - "gy/Kkr": "", - "g5pX+a": "", - "eiPBw7": "", - "bPLen5": "", - "bLK+Kr": "", - "ieGrWo": "", - "zELxbG": "", - "v1SpKO": "", - "v1DNMW": "", - "fUEpLA": "", - "egvJrY": "", - "aACJNp": "", - "LmhSmU": "Confirmer la suppression de l'entrée", - "AML4RW": "Affectation des tâches", - "9Obw6C": "Filtre", - "4Hrh5B": "", - "3/wF0G": "Commandes slash", - "usa8vQ": "", - "soePYH": "", - "sQu1rA": "", - "waVyVY": "", - "KUr+sG": "Mettre à jour le resumé du run", - "Hzwzgs": "Diffuser les mises à jour dans {oneChannel, plural, one {le canal} other {les canaux}}", - "CjNrqO": "Modèle de rapport rétrospectif", - "8hDbW6": "Envoyer un webhook sortant", - "recCg9": "", - "rX08cW": "", - "jXT2++": "", - "jIIWN+": "", - "QaZNp9": "", - "1MQ3XZ": "", - "xmcVZ0": "", - "uhu5aG": "", - "OHfpS1": "", - "ruJGqS": "", - "Y+U8La": "", - "RO+BaS": "", - "D9IV7i": "Les rétrospectives ont été désactivées pour cette exécution du playbook.", - "S0kWcH": "", - "wbwhbH": "", - "2/2yg+": "Ajouter", - "pKLw8O": "", - "qsr3Zk": "", - "yhU1et": "", - "R/2lqw": "", - "KJu1sq": "Retirer la liste de contrôle", - "9PXW6Q": "Durée / Démarré le", - "qyJtWy": "", - "OsDomv": "", - "2PNrBQ": "", - "/HtNUp": "Sélectionner ou spécifier un {mode, select, DurationValue {time span (\"4 hours\", \"7 days\"...)} DateTimeValue {time (\"in 4 hours\", \"May 1\", \"Tomorrow at 1 PM\"...)} other {time or time span}}", - "cPIKU2": "", - "osuP6z": "", - "W/V6+Y": "", - "kDcpd/": "", - "q0cpUe": "", - "2VrVHu": "Rechercher par nom de run", - "5A46pW": "Ajouter une commande slash", - "wO6NOM": "", - "5ciuDD": "PAS DANS LE CANAL", - "BNB75h": "Un playbook prescrit les listes de contrôle, les automatisations et les modèles pour toute procédure reproductible. {br} Cela aide les équipes à réduire les erreurs, à gagner la confiance des parties prenantes et à devenir plus efficaces à chaque itération.", - "9+Ddtu": "Suivant", - "C1khRR": "Retour aux playbooks", - "x5Tz6M": "", - "Ietscn": "Tâches terminées", - "qp3Fk4": "", - "9tBhzB": "Mettre à jour maintenant", - "X2K92H": "", - "d4g2r8": "", - "Mm1Gse": "Rechercher un membre", - "wsUmh9": "", - "wcWpGs": "", - "w0muFd": "", - "vaYTD+": "", - "uBLF+D": "", - "u4MwUB": "", - "tzMNF3": "", - "twieZh": "", - "tVPYMu": "", - "oVHn4s": "", - "nqVby7": "", - "lrbrjv": "", - "lJyq2a": "", - "kvgvNW": "", - "kXFojL": "", - "hrgo+E": "", - "hXIYHG": "", - "hO9EdA": "", - "fuDLDJ": "", - "edxtzC": "", - "eHAvFf": "", - "e/AZL5": "", - "d9epHh": "", - "bGhCLX": "", - "bE1Cro": "", - "b5FaCc": "", - "ZAJviT": "", - "Z7vWDQ": "", - "Z/hwEf": "", - "YORRGQ": "", - "YKn+7s": "", - "YDuW/T": "", - "XmUdvV": "", - "VmnoW8": "", - "VOzlSL": "", - "V5TY0z": "", - "Ui6GK/": "", - "UbTsGY": "", - "UMoxP9": "", - "TxCTXQ": "", - "TSSNg/": "", - "TJo5E6": "", - "SmAUf9": "", - "SENRqu": "", - "SDSqfA": "", - "Q7hMnp": "", - "Q7aZO4": "", - "Q67RuY": "", - "zz6ObK": "", - "ypIsVG": "", - "o2eHmz": "", - "l7zMH6": "", - "l0hFoB": "", - "jS/UOn": "", - "j7jdWG": "", - "ijAUQf": "", - "gt6BhE": "", - "iDMOiz": "", - "b3TdyZ": "", - "aWpBzj": "", - "ZdWYcm": "", - "JqKASQ": "Ajouter @{displayName} au canal", - "nSFBC2": "", - "m/Q4ye": "", - "zx0myy": "", - "ryrP8K": "", - "rbrahO": "", - "rDvvQs": "", - "q6f8x9": "", - "nkCCM2": "", - "fmylXu": "", - "dsTLW1": "", - "d8KvXJ": "", - "TdTXXf": "En savoir plus", - "vNiZXF": "", - "uny3Zy": "", - "c8hxKk": "", - "b40Pr7": "", - "avPeEI": "", - "TZYiF/": "", - "RoGxij": "", - "R+JQaJ": "", - "ICqy9/": "Listes de contrôle", - "GjCS6U": "Choisissez un modèle", - "RzEVnf": "", - "VZRWFk": "", - "I5DYM+": "", - "1isgPF": "", - "9SIW2x": "Valeur cible pour chaque run", - "LI7YlB": "", - "FGzxgY": "ex. temps d'accusé de réception, temps de résolution", - "6GTzTR": "Consultez à tout moment le contenu de ce playbook", - "HGdWwZ": "Créer et assigner des tâches", - "Pue+oV": "", - "NYTGIb": "", - "q/VD+s": "", - "dZmYk6": "", - "rMhrJH": "", - "vL4++D": "", - "0Xt1ea": "Vous pourrez toujours accéder aux données historiques pour cette mesure.", - "GAuN6w": "Établir des hypothèses", - "uT4ebt": "", - "hw83pa": "", - "vQqT/8": "", - "fhMaTZ": "", - "1ikfp3": "Si vous supprimez cette métrique, les valeurs correspondantes ne seront pas collectées lors des prochains runs.", - "6D6ffM": "Veuillez saisir une durée au format : jj:hh:mm (par exemple, 12:00:00) ou laissez le champ vide.", - "HXvk56": "Publier des mises à jour de statut", - "4BN53Q": "Nous vous montrerons à quel point chaque valeur du run est proche ou éloignée de l'objectif et nous la représenterons également sur un graphique.", - "udrLSP": "", - "wPVxBN": "", - "vJ2SaW": "", - "9m0I/B": "Tenir les parties prenantes informées", - "tbjmvS": "", - "JrZ2th": "Ajouter une métrique", - "y7o4Rn": "", - "wbdGb5": "", - "LDYFkN": "Durée (en jj:hh:mm)", - "lUfDe1": "", - "dxyZg3": "", - "Tt04f1": "", - "R5Zh+l": "", - "8n24G2": "Afficher les détails du run dans un panneau latéral", - "lgZf0l": "", - "ZkhArX": "", - "1QosTr": "Utilisé par", - "/fU9y/": "Vous pouvez consulter les différentes sections du playbook en détail sur cette page.", - "a0hBZ0": "", - "rzbYbE": "", - "mbo96h": "", - "gsMPAS": "", - "f+bqgK": "", - "TxmjKI": "", - "Sx3lHL": "", - "OyZnsJ": "", - "NJ9uPu": "Chiffres clés", - "xvBDOH": "", - "lBqu4h": "", - "bTgMQ2": "", - "MTzF3S": "Etes-vous sûr de vouloir restaurer le playbook {title} ?", - "4cwL43": "", - "4aupaG": "Le playbook {title} a été restauré avec succès.", - "SVwJTM": "", - "9XUYQt": "Importer", - "4alprY": "Modèles de Playbook", - "/urtZ8": "Vos Playbooks", - "4fHiNl": "Dupliquer", - "3PoGhY": "Êtes-vous sûr de vouloir publier ?", - "cEWBE3": "", - "GG1yhI": "Il existe des modèles pour toute une série de cas d'usage et d'événements. Vous pouvez utiliser un playbook tel quel ou le personnaliser, puis le partager avec votre équipe.", - "F4pfM/": "Veuillez saisir un nombre, ou laissez le champ vide.", - "Q3R9Uj": "", - "q/Qo8l": "", - "0EEIkR": "", - "Q5hysF": "", - "QbGfqo": "", - "mVpO8u": "", - "XpDetT": "", - "lbs7UO": "", - "xVyHgP": "", - "efeNi1": "", - "ru+JCk": "", - "awG90C": "", - "NiAH1z": "", - "NMxVd+": "Veuillez indiquer la valeur de la métrique.", - "M4gAc9": "Ajouter une valeur", - "NLeFGn": "à", - "Vf/QlZ": "", - "KXVV4+": "Bienvenue sur la page d'aperçu du playbook !", - "mvZUm3": "", - "ZNNjWw": "", - "69nlA3": "Veuillez saisir une durée au format : jj:hh:mm (par exemple, 12:00:00).", - "fmbSyg": "", - "9a9+ww": "Titre", - "l5/RKZ": "", - "0RlzlZ": "Envoyer un message de bienvenue temporaire à l'utilisateur", - "9j5KzL": "Veuillez saisir un nom de catégorie", - "7P5T3W": "Restaurer la liste de contrôle", - "Ek1Fx2": "Lorsqu'un message contenant ces mots-clés est publié", - "B3Q5mz": "Déclencheur", - "5AJmOz": "Lorsqu'un utilisateur se joint au canal", - "+/x2FM": "Sélectionner un playbook", - "371AC3": "Mettre à jour le résumé du run", - "2Q5PhZ": "Demander à lancer un Playbook", - "MBNMo9": "Actions du canal", - "9trZXa": "Tout le monde dans l'équipe peut voir", - "NFyWnZ": "Travailler plus efficacement", - "MtrTNy": "Demain", - "MHzP9I": "Définir un message pour accueillir les utilisateurs qui rejoignent le canal.", - "JcefuP": "Ajoutez une description (facultatif)", - "DPj6DM": "Sélectionnez Run pour le voir en action.", - "AF7+5o": "Ajouter une date d'échéance", - "+qDKgW": "Voir toutes les mises à jour", - "0QD99o": "Demander à rejoindre le canal", - "/+8SGX": "Affichage de {filteredNum} sur {totalNum} événements", - "DUU48k": "Aucune tâche ne vous est explicitement assignée. Vous pouvez élargir votre recherche à l'aide des filtres.", - "BiQjuS": "Le run a été déplacé vers {channel}", - "AoNLta": "Il n'y a pas de runs terminés liés à ce canal", - "AG7PKJ": "Renommer le run", - "9xs0pp": "Ajouter une valeur...", - "9qqGGd": "Inviter des participants", - "9kQNdp": "Ce playbook est privé.", - "9X3jwi": "{icon} Coût", - "9M92On": "Sélectionner les canaux", - "9AQ5FE": "Résumé du run", - "8FzC0B": "{user} a coché l'élément \"{name}\" de la liste de contrôle", - "8//+Yb": "Lier la liste de contrôle à un autre canal", - "7KMbBa": "Jamais utilisé", - "706Soh": "tâches accomplies", - "6rygzu": "Enlever du run", - "5b1zuB": "Les ajouter au canal du run", - "5ZIN3u": "Mises à jour du statut", - "5HXkY/": "Type: {typeTitle}", - "4mCpAv": "Le changement de propriétaire n'a pas été possible", - "4Iqlfe": "Vous avez rejoint ce run.", - "3qPQMX": "{name} a demandé une mise à jour du statut", - "3hBelc": "Une rétrospective n'est pas prévue.", - "3Yvt4d": "Les playbooks sont des listes de contrôle configurables qui définissent un processus répétable permettant aux équipes d'obtenir des résultats spécifiques et prévisibles", - "2NDgJq": "Dernière mise à jour du statut", - "2BCWLD": "Configurer le canal", - "1prgB2": "Rechercher des personnes", - "1fXVVz": "Date d'échéance...", - "1GOpgL": "Personne assignée...", - "0CeyUV": "Aucun résultat pour \"{searchTerm}\"", - "03oqA2": "Runs en cours", - "/GCoTA": "Effacer", - "//o1Nu": "Désactiver les mises à jour" -} diff --git a/webapp/playbooks/i18n/hr.json b/webapp/playbooks/i18n/hr.json deleted file mode 100644 index d8b0e7fb51c..00000000000 --- a/webapp/playbooks/i18n/hr.json +++ /dev/null @@ -1,841 +0,0 @@ -{ - "TSSNg/": "Tjedno započeta UKUPNA IZVOĐENJA u zadnjih 12 tjedana", - "/jUtaM": "Dnevna AKTIVNA IZVOĐENJA u zadnjih 14 dana", - "CL5OZP": "Samo korisnici koje odabereš moći će uređivati ili pokretati ovaj priručnik.", - "zWkvNO": "Vremenska crta", - "zINlao": "Vlasnik", - "zELxbG": "Spremljene poruke", - "z5RMPO": "Samo ti možeš pristupiti ovom priručniku", - "yhzuSC": "Vrijeme: {time}", - "x5Tz6M": "Izvještaj", - "wbwhbH": "Ime zadatka", - "wbsq7O": "Korištenje", - "waVyVY": "Trenutačno aktivni sudionici", - "wL7VAE": "Radnje", - "wEQDC6": "Uredi", - "w7tf2z": "Objavljeno", - "viXE32": "Privatno", - "vOFN0m": "Objava o stanju izbrisana:", - "v3+TmO": "{members, plural, =0 {No one} =1 {One person} other {# people}} mogu pristupiti ovom priručniku", - "v1SpKO": "Promjene uloge", - "v1DNMW": "Retrospektivu je objavio/la {name}", - "usa8vQ": "Pošalji pozdravnu poruku", - "uhu5aG": "Javno", - "uJ3bRR": "Ovaj predložak pomaže standardizirati format za sažet opis koji objašnjava svako pokretanje svojim sudionicima.", - "t6SiGO": "Trenutačna izvođenja u tijeku", - "syEQFE": "Objavi", - "soePYH": "{num_checklists, plural, =0 {nema popisa zadataka} one {# popis zadataka} few {# popisa zadataka} other {# popisa zadataka}}", - "sQu1rA": "{numTotalRuns, plural, =0 {nijedno izvođenje nije započeto} =1 {# izvođenje je započeto} few {# izvođenja su započeta} other {# izvođenja su započeta}}", - "s3jjqi": "{num_actions, plural, =0 {nema radnji} one {# radnja} few {# radnje} other {# radnji}}", - "recCg9": "Aktualiziranja", - "rX08cW": "Datum mora biti u budućnosti.", - "pKLw8O": "Stvarno želiš izbrisati ovaj događaj? Izbrisani događaji će se trajno ukloniti iz vremenske crte.", - "o2eHmz": "Izvođenje je završio/la {name}", - "nvy0pS": "Kad se pokretanje završi, izvezi kanal", - "lxfpbh": "Vlasnik {reminderEnabled, select, true {će biti upitan da navede aktualiziranje stanja svakih} other {neće biti upitan da navede aktualiziranje stanja}}", - "lbhO3D": "kurziv", - "lZwZi+": "Dan: {date}", - "jvo0vs": "Spremi", - "jq4eWU": "Pristup priručniku", - "jnmORb": "U ovom priručniku", - "jXT2++": "Idi na kanal", - "jS/UOn": "Auktualliziraj predložak", - "jIIWN+": "predformatirano", - "hzt6l8": "Koristi Markdown za stvaranje predloška.", - "hO9EdA": "Pozovi {numInvitedUsers, plural, =0 {nijednog člana} =1 {jednog člana} few {# člana} other {# članova}} u kanal", - "gy/Kkr": "(uređeno)", - "g5pX+a": "Informacije", - "fXGjhC": "Vlasnik je promijenjen od {summary}", - "fUEpLA": "Nema kronoloških događaja koji odgovaraju tim filtrima.", - "eiPBw7": "Interval podsjetnika retrospektive", - "egvJrY": "Zadužena osoba je promijenjena", - "ebkl6I": "Svatko u ovom timu može pristupiti ovom priručniku", - "eHAvFf": "podebljano", - "djXM+y": "Pristupiti mogu samo odabrani korisnici.", - "dcV/DJ": "{timestamp}", - "d9epHh": "Izvezi dnevnik kanala", - "c8hxKk": "Tjedan datuma {date}", - "bPLen5": "Završena izvođenja u zadnjih 30 dana", - "bLK+Kr": "Podsjeća kanal u određenom intervalu da ispuni retrospektivu.", - "bGhCLX": "Kad se objavi aktualiziranje", - "b5FaCc": "Dodaj kanal u kategoriju u bočnoj traci", - "b40Pr7": "Izvjestitelj", - "avPeEI": "Aktualiziraj za prikaz trendova ukupnog broja izvođenja, aktivnih izvođenja i sudionika uključenih u izvođenjima ovog priručnika.", - "aACJNp": "Izvođenje je započeo/la {name}", - "ZwlIYH": "{activeRuns, number} active {activeRuns, plural, one {run} other {runs}}", - "Z/hwEf": "Kanal će se podsjetiti da izvrši retrospektivu {reminderEnabled, select, true {svakih} other {}}", - "YDuW/T": "{num_runs, plural, =0 {Još nije izvedeno} one {# izvođenje} few {# izvođenja ukupno} other {# izvođenja ukupno}}", - "XmUdvV": "Sva potrebna statistika", - "X3DLGJ": "Svatko u ovom radnom prostoru može stvoriti priručnike.", - "W9j0FJ": "{date}", - "VmpFFw": "Nema opisa.", - "Ui6GK/": "Kad se novi član pridruži kanalu", - "UbTsGY": "Započeta izvođenja između {start} i {end}", - "TyrY2b": "Izrada priručnika", - "TvihSy": "Objavi ponovo", - "TZYiF/": "precrtano", - "TJo5E6": "Pregled", - "T7Ry38": "Poruka", - "T5rX+W": "Koliko često treba objavljivati aktualiziranje?", - "SFuk1v": "Dozvole", - "SENRqu": "Pomoć", - "SDSqfA": "Kad izvođenje započne", - "RthEJt": "Retrospektiva", - "RoGxij": "Aktivna izvođenja {date}", - "R+JQaJ": "Članovi kanala", - "QnZAit": "Dodaj opcionalni opis", - "QiKcO7": "Umetni predložak za retrospektivu", - "Q8Qw5B": "Opis", - "Q7aZO4": "{numParticipants, plural, =0 {nema aktivnih sudionika} =1 {# aktivni sudionik} few {# aktivna sudionika} other {# aktivnih sudionika}}", - "OsDomv": "Svi događaji", - "OcpRSQ": "Izbriši unos", - "OINwWS": "Stvori {isPublic, select, true {javni} other {privatni}} kanal", - "NE1OeI": "Svatko iz tima({team}) smije pristupiti.", - "N1U/QR": "Promjene stanja zadatka", - "MvEydR": "{name} je objavio/la aktualiziranje stanja", - "LmhSmU": "Potvrdi brisanje unosa", - "LRFvqz": "Najavi u {oneChannel, plural, one {kanalu} other {kanalima}}", - "KiXNvz": "Izvodi", - "KUr+sG": "Aktualiziraj sažetak izvođenja", - "JeqL8w": "Retrospektivu je prekinuo/la {name}", - "JCGvY/": "Ovaj predložak pomaže standardizirati format za ponavljajuća aktualiziranja koja se dešavaju tijekom svakog izvođenja.", - "IuFETn": "Trajanje", - "ICqy9/": "Popisi zadataka", - "I2zEie": "Proslavi uspjeh i uči iz grešaka s izvještajima o retrospektivi. Filtriraj događaje na vremenskoj crti za pregled procesa, angažman sudionika i svrhe revizije.", - "Hzwzgs": "Šalji aktualiziranja u {oneChannel, plural, one {kanal} other {kanale}}", - "HhLp57": "citat", - "FEGywG": "Odredi budući datum/vrijeme za podsjetnik aktualiziranja.", - "EC5MJD": "Nema novih aktualiziranja.", - "DnBhRg": "Dodaj ljude", - "DXACD6": "Objavi izvještaj o retrospektivi i pristupi vremenskoj crti", - "DCl7Vv": "umetnuti kȏd", - "D3idYv": "Postavke", - "CjNrqO": "Predložak za izvještaj o retrospektivi", - "BD66u6": "Preuzmi CSV koji sadrži sve poruke kanala", - "ArpdYl": "Kronološki događaji prikazani su ovdje redom događanja. Za uklanjanje događaja prijeđi pokazivačem preko njega.", - "AT2QBo": "Samo odabrani korisnici mogu stvoriti priručnike.", - "AS5kar": "Sudionici ({participants})", - "AML4RW": "Dodjele zadataka", - "A3ptul": "Predlošci", - "9uOFF3": "Pregled", - "9Obw6C": "Filtar", - "8hDbW6": "Pošalji izlazni webhook", - "6Lwe7T": "Svatko u timu {team} može pristupiti ovom priručniku", - "5Ot7cd": "Odredi vrstu kanala koji ovaj priručnik stvara.", - "5FRgqE": "Preuzimanje dnevnika kanala", - "4Hrh5B": "{name} je promijenio/la stanje od {summary}", - "47FYwb": "Odustani", - "3rCdDw": "Aktualiziranja stanja", - "1MQ3XZ": "{numActiveRuns, plural, =0 {nema aktivnih izvođenja} =1 {# aktivno izvođenje} few {# aktivna izvođenja} other {# aktivnih izvođenja}}", - "1I48bs": "Predložak za retrospektivu", - "/HtNUp": "Odaberi ili odredi {mode, select, DurationValue {trajanje („4 sata”, „7 dana” …)} DateTimeValue {vrijeme („za 4 sata”, „1. svibnja”, „Sutra u 13 sati” …)} other {vrijeme ili trajanje}}", - "/1FEJW": "Dnevni AKTIVNI SUDIONICI u zadnjih 14 dana", - "+ZIXOR": "Pristup kanalu", - "+QgvjN": "Dodijeli vlasničku ulogu korisiniku", - "+8G9qr": "Standardni tekst za retrospektivu.", - "oVHn4s": "Zadnje aktualiziranje", - "nmpevl": "Odbaci", - "nkCCM2": "Više te se neće podsjećivati.", - "lrbrjv": "Da, pokreni retrospektivu", - "lJyq2a": "Izvođenje nije pronađeno", - "l7zMH6": "Odaberi opciju ili odredi prilagođeno trajanje", - "l0hFoB": "Dodaj opis priručnika …", - "kvgvNW": "Znaj što se dogodilo", - "kGI46P": "Opis zadatka", - "jwimQJ": "U redu", - "jIgqRa": "Vlasnik / Sudionici", - "izWS4J": "Ne prati", - "ijAUQf": "Obavijesti administratora sustava za nadogradnju.", - "ieGrWo": "Prati", - "hrgo+E": "Arhiva", - "hfrrC7": "Inicijali tima", - "hVFgh4": "Uključi završene", - "guunZt": "Dodijeli", - "gt6BhE": "Detalji izvođenja", - "g4IF1x": "Nema izvođenja za ovaj priručnik.", - "fpuWL1": "Izbriši priručnik", - "fV6578": "Dodijeli vlasničku ulogu", - "edxtzC": "Stvori priručnik", - "eLeFE2": "Uredi ime i opis", - "eKv7yX": "Objavi", - "e/AZL5": "Tvoje trideset-dnevno probno razdoblje je počelo", - "dsTLW1": "Uredi zadatak", - "dSC1YD": "Preskoči zadatak", - "b/QBNs": "Rok aktualiziranja", - "aYIUar": "Hvala!", - "aWpBzj": "Prikaži više", - "ZdWYcm": "Ne, preskoči retrospektivu", - "ZWtlyd": "Izvođenje je obnovio/la {name}", - "ZAJviT": "Nismo bili u stanju obavijestiti administratora sustava.", - "Z7vWDQ": "Dogodila se greška", - "YORRGQ": "Objavi aktualiziranje", - "YMrTRm": "Sažetak izvođenja", - "WAHCT2": "Obavijesti administratora sustava", - "W1Qs5O": "Izvođenja", - "W/V6+Y": "Sklopi", - "Vhnd2J": "Uklj./Isklj. opis", - "V5TY0z": "Dodati sudionike?", - "TdTXXf": "Saznaj više", - "TDaF6J": "Odbaci", - "SmAUf9": "Podsjednik će se poslati {timestamp}", - "S0kWcH": "Prekoračen rok aktualiziranja", - "QaZNp9": "Završi izvođenje", - "QUwMsX": "Podsjetnik za ispunjavanje retrospektive", - "Q7hMnp": "Izvodi priručnik", - "Q67RuY": "Pogledaj sva pokretanja", - "Oo5sdB": "Ime priručnika", - "OHfpS1": "Sadrži bilo koju od ovih ključne riječi", - "N2IrpM": "Potvrdi", - "Mm1Gse": "Traži člana", - "MDP9TS": "Ukloni iz priručnika", - "M/2yY/": "Još nitko.", - "Lg3I1b": "@{targetUsername}, navedi aktualiziranje stanja.", - "Leh2tk": "Pritisni ovdje za prikaz svih pokretanja u timu.", - "LVYPbG": "Odredi vlasnika", - "L6k6aT": "… ili započni s predloškom", - "KJu1sq": "Ukloni popis zadataka", - "K4O03z": "Novi zadatak", - "K3r6DQ": "Izbriži", - "JXdbo8": "Završeno", - "JJNc3c": "Prethodno", - "IwY/wg": "Priručnik za svaki proces", - "IfxUgC": "Dodaj sažetak izvođenja …", - "Ietscn": "Završeni zadaci", - "IOnm/Z": "Nema sažetka izvođenja.", - "I90sbW": "upravo sada", - "G/yZLu": "Ukloni", - "EQpfkS": "Završeno", - "B487HA": "U tijeku", - "Auj1ap": "Pokreni probno razdoblje ili nadogradi svoju pretplatu.", - "ApULhK": "Pozovi članove", - "A8dbCS": "Priručnik nije pronađen", - "A21Mgv": "Izvođenje završeno", - "9tBhzB": "Nadogradi sada", - "9qc7BX": "Odgodi", - "9TTfXU": "Tvoj administrator sustava je obaviješten.", - "9PXW6Q": "Trajanje / Započeto", - "91Hr5f": "Povuci me za mijenjanje redoslijeda", - "9+Ddtu": "Dalje", - "6uhSSw": "Odaberi kanal", - "6jDabx": "Pošalji povratne informacije", - "6CGo3o": "Stanje / Zadnje aktualiziranje", - "5wqhGy": "Uklj./Isklj. detalje izvođenja", - "5CI3KH": "Kontaktiraj podršku", - "4ltHYh": "Idi na priručnik", - "42qmJ5": "Nemaš prava za objavljivanje aktualiziranja.", - "3Psa+5": "Dodaj ključne riječi", - "36GNZj": "Priručnik {title} je uspješno arhiviran.", - "2VrVHu": "Traži prema imenu izvođenja", - "15jbT0": "Dodaj više u tvoju vremensku crtu", - "0wJ7N+": "Zadatak", - "0oLj/t": "Rasklopi", - "0HT+Ib": "Arhivirano", - "/4tOwT": "Preskoči", - "zz6ObK": "Obnovi", - "zx0myy": "Sudionici", - "z3B83t": "Traži priručnik", - "z3A0LP": "Zadnje izvođenje je bilo {relativeTime}", - "yxguVq": "Odbaci promjene", - "yqpcOa": "Koristi", - "ypIsVG": "Obnovi zadatak", - "yhU1et": "Zadaci", - "xmcVZ0": "Pretraži", - "wsUmh9": "Tim", - "wZ83YL": "Ne sada", - "wX3k9U": "Bezimen priručnik", - "vir0m9": "Neispravno ime kategorije.", - "v8ZnNc": "Odaberi tim", - "uny3Zy": "Priručnici", - "uBLF+D": "Što je priručnik?", - "u4MwUB": "Spremi povijest tvog priručnika izvođenja", - "tzMNF3": "Stanje", - "sqNmlF": "Preskoči retrospektivu", - "rbrahO": "Zatvori", - "rDvvQs": "{completed, number} / {total, number} završeno", - "qyJtWy": "Prikaži manje", - "q6f8x9": "Promjena od zadnjeg aktualiziranja", - "prYDT6": "Kanal objavljivanja", - "pjt3qA": "Novi popis zadataka", - "k1djnL": "Izbriši popis", - "j7jdWG": "Pretvori u komercijalno izdanje.", - "iXNbPf": "Preimenuj", - "iNU1lj": "Izvođenje koje tražiš je privatno ili ne postoji.", - "iDMOiz": "ČLANOVI KANALA", - "hXIYHG": "Za podržavanje izvoza kanala, instaliraj i aktiviraj dodatak za izvoz kanala", - "fuDLDJ": "Stvori kanal", - "dvhvum": "(Opcionalno) Opiši kako se ovaj priručnik treba koristiti", - "d4g2r8": "Izbrisano: {timestamp}", - "cp7KUI": "Priručnik", - "cPIKU2": "Praćenje", - "bE1Cro": "Samo moja izvođenja", - "YKn+7s": "Ovaj kanal ne izvodi nijedan priručnik.", - "Y+U8La": "Stvarno želiš izbrisati priručnik {title}?", - "XXbWAU": "Odaberi ovu opciju za automatsko primanje aktualiziranja kad se ovaj priručnik izvodi.", - "X2K92H": "Ime popisa", - "WbsomC": "Objavi retrospektivu", - "WTQpnI": "Počni sada raditi koristeći priručnike", - "WIxhrv": "Ime izvođenja mora sadržati barem dva znaka", - "VmnoW8": "Za više informacija provjeri dnevnike sustava.", - "UMoxP9": "Predložak imena kanala (opcionalno)", - "TxCTXQ": "Stvarno želiš završiti izvođenje?", - "RO+BaS": "Kopiraj poveznicu za izvođenje", - "QywYDe": "Također označi izvođenje kao završeno", - "O8o2lE": "Dodaj kanal kategoriji", - "Nh91Us": "{from, number} – {to, number} od ukupno {total, number}", - "NA7Cw1": "Kopiraj poveznicu na priručnik", - "JqKASQ": "Kanalu dodaj @{displayName}", - "GxJAK1": "Priručnik koji tražiš je privatan ili ne postoji.", - "GwtR3W": "Povuci i ispusti postojeći zadatak ili pritisni za stvaranje novog zadatka.", - "GRTyvN": "Uključi/Isključi popis priručnika", - "E0LnBo": "Možeš odabrati opciju ili odrediti prilagođeno trajanje („2 tjedna”, „3 dana 12 sati”, „45 minuta”, …)", - "DSVJjB": "Trenutačno se izvodi priručnik {playbookTitle}", - "D9IV7i": "Retrospektive su deaktivirane za ovo izvođenje priručnika.", - "D55vrs": "Nije bilo moguće generirati tvoju licencu", - "D/wCS9": "Stvarno želiš objaviti retrospektivu?", - "CyGaem": "Ime izvođenja", - "Cy1AK/": "Prikaži detalje izvođenja", - "CkYhdY": "Dodaj kanal kategoriji u bočnoj traci", - "CBM4vh": "Timer za sljedeće aktualiziranje", - "C9NScU": "Prebaci kontrolu svom timu", - "C6Oghd": "Uredi sažetak izvođenja", - "6n0XDG": "Stavrno želiš ukloniti popis? Svi zadaci će se ukloniti.", - "5qBEKB": "Što su izvođenja priručnika?", - "5ciuDD": "NIJE U KANALU", - "5Ofkag": "Aktiviraj retrospektivu", - "4vuNrq": "{duration} nakon početka izvođenja", - "3MSGcL": "Ime kanala je neispravno.", - "2Qq4YX": "Stvarno želiš odbaciti tvoje promjene?", - "2PNrBQ": "Izvezi kanal tvog pokretanja priručnika i spremi ga za kasniju analizu.", - "2563nT": "Potvrdi kraj izvođenja", - "2/2yg+": "Dodaj", - "0oL1zz": "Kopirano!", - "/gbqA6": "{duration} prije početka izvođenja", - "vjzpnC": "Nema priručnika koji odgovaraju tim filtrima.", - "sVlNlY": "Struktura svakog tima je drugačija. Možeš upravljati korisnicima koji mogu stvarati priručnike.", - "TBez4r": "Nema priručnika za prikaz. Nemaš dozvole za izradu priručnika u ovom radnom prostoru.", - "Rgo4VW": "Svatko u ovom radnom prostoru može stvoriti priručnike. Administratori sustava mogu promijeniti ovu postavku.", - "R4vA+C": "Samo dolje navedeni korisnici mogu stvoriti priručnike. Ovi korisnici, kao i administratori sustava, mogu promijeniti ovu postavku.", - "BQtd5I": "Priručnici – dobrodošlica!", - "/ZsEUy": "Stvarno želiš izbrisati ovaj popis? Uklonit će se iz ovog izvođenja, ali neće utjecati na priručnik.", - "Qrl6bQ": "Optimiraj tok tvojih postupaka pomoću priručnika", - "C1khRR": "Narag na priručnike", - "CSts8B": "Ikona tima", - "DuRxjT": "Stvori priručnik", - "HSi3uv": "Nema zadužene osobe", - "/YZ/sw": "Pokreni probno razdoblje", - "/MaJux": "Pokreni retrospektivu", - "+hddg7": "Dodaj u vremensku crtu izvođenja", - "+Tmpup": "Aktualiziranja ćeš automatski primati kad se ovaj priručnik izvodi.", - "HAlOn1": "Ime", - "I5NMJ8": "Više", - "Ja1sVR": "Aktualiziranja stanja su deaktivirana za ovo izvođenje priručnika.", - "Lo10yH": "Nepoznat kanal", - "Mu2aDs": "Svi članovi tima ({team}) imaju pravo pristupa.", - "MrJPOh": "Aktiviraj aktualiziranja stanja", - "sIX63S": "Tvoj administrator sustava je obaviješten", - "MhKICa": "Tvoja pretplata dozvoljava jedan priručnik po timu. Nadogradi svoju pretplatu i izradit više priručnika s jedinstvenim radnim tokovima za svaki tim.", - "x8cvBr": "Pirkaži pregled izvođenja", - "wO6NOM": "Stvarno želiš obnoviti ovaj zadatak? Ovaj zadatak će biti dodan u ovo pokretanje", - "twieZh": "Idi na pregled izvođenja", - "scYyVv": "Želitš li ispuniti izvještaj o retrospektivi?", - "ryrP8K": "Upravljaj dozvolama osoba koje mogu vidjeti, mijenjati i izvoditi ovaj priručnik.", - "qp3Fk4": "Priručnik predstavlja tijek rada koji bi tvoji timovi i alati trebali slijediti, uključujući sve popise zadataka, radnje, predloške i retrospektive.", - "q0cpUe": "Dodaj popis", - "oS0w4E": "Standardni timer aktualiziranja", - "nSFBC2": "+ Dodaj zadatak", - "m/Q4ye": "Preimenuj popis", - "k9q07e": "Šalji aktualiziranje na druge kanale", - "0tznw6": "Pretvori u privatni priručnik", - "0Vvpht": "Postavi člana priručnika", - "wylJpv": "Svatko u timu {team} može vidjeti ovaj priručnik.", - "vaYTD+": "Postoji {outstanding, plural, =1 {# nedovršeni zadatak} few {# nedovršena zadatka} other {# nedovršenih zadataka}}. Stvarno želiš završiti izvođenje?", - "pK6+CW": "@{displayName} nije član kanala [{runName}]({overviewUrl}). Želiš li ih dodati ovom kanalu? Imat će pristup cijeloj povijesti poruka.", - "o+ZEL3": "Objavljeno {timestamp}", - "g0mp+I": "Kad pretvoriš u privatni priručnik, čuva se povijest članstva i pokretanja. Ovo je trajna promjena i ne može se poništiti. Stvarno želiš pretvoriti {playbookTitle} u privatni priručnik?", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {izvođenje} few {izvođenja} other {izvođenja}} u tijeku", - "d8KvXJ": "Tvoja licenca probnog razdoblja istječe {expiryDate}. Za izbjegavanje poremećaja, licencu možeš kupiti u bilo kojem trenutku putem korisničkog portala.", - "QpUBDr": "{members, plural, =0 {Nitko ne može} =1 {Jedna osoba može} few {# osobe mogu} other {# osoba mogu}} pristupiti ovom priručniku.", - "JJMNME": "{withRunName, select, true {@{authorUsername} je objavio/la aktualiziranje za [{runName}]({overviewURL})} other {@{authorUsername} je objavio/la aktualiziranje}}", - "BNB75h": "Priručnik propisuje popise zadataka, automatizacije i predloške za sve ponavljajuće postupke. {br} Pomaže timovima da smanje greške, steknu povjerenje sudionika i postanu učinkovitiji sa svakim ponavljanjem.", - "AF9wda": "Ovo aktualiziranje će se spremiti na stranici pregleda{hasBroadcast, select, true { i emitirati na {broadcastChannelCount, plural, =1 {jednom kanalu} other {{broadcastChannelCount, number} kanala}}} other {}}.", - "h+e7G+": "Zatraži izvođenje ovog priručnika kad poruka sadrži {numKeywords, select, 1 {ključnu riječ} other {jednu ili više ključnih riječi}}", - "nqVby7": "{numTasksChecked, number} od {numTasks, number} {numTasks, plural, =1 {zadatka} few {zadatka} other {zadataka}} provjereno", - "kDcpd/": "{numKeywords, plural, few {# ključne riječi} other {# ključnih riječi}}", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {zadatak} few {zadatka} other {zadataka}}", - "DtCplA": "{numParticipants, plural, =1 {# sudionik} other {# sudionika}}", - "5j6GD/": "{numParticipants, plural, =0 {nema sudionika} =1 {# sudionik} few {# sudionika} other {# sudionika}}", - "2QkJ4s": "Spremi važne poruke za dobivanje potpune slike koja optimira tok retrospektiva.", - "tVPYMu": "Administrator priručnika", - "sDKojV": "Arhiviraj priručnik", - "ruJGqS": "Pristup priručniku", - "osuP6z": "Povuci za mijenjanje redoslijeda popisa zadataka", - "lQT7iD": "Stvori priručnik", - "gGcNUr": "Nemaš dozvole", - "fmylXu": "Zatraži pokretanje priručnika kad korisnik objavi poruku", - "R/2lqw": "Odaberi predložak", - "MJ89uW": "Pretvori u privatni priručnik", - "HLn43R": "Upravljaj pristupom", - "EvBQLq": "Postavi administratorom priručnika", - "EWz2w5": "Izvođenje priručnika", - "9kCT7Q": "Stvori retrospektive s vremenskom crtom koja automatski prati ključne događaje i poruke, tako da ih timovi imaju nadohvat ruke.", - "8oCVbz": "Stvarno želiš objaviti", - "7VTSeD": "Stvarno želiš preskočiti ovaj zadatak? Uklonit će se iz ovog pokretanja, ali neće utjecati na priručnik.", - "5BUxvl": "Svatko u ovom timu može vidjeti ovaj priručnik.", - "3Ls2m+": "Član priručnika", - "vndQuC": "Naredba nakon kose crte izvršena", - "ObmjTB": "Naredba nakon kose crte", - "5A46pW": "Dodaj naredbu nakon kose crte", - "3/wF0G": "Naredbe nakon kose crte", - "X/koAN": "Nevažeći unos: maksimalan broj dopuštenih webhook-ova je 64", - "wcWpGs": "Nevažeći webhook URL-ovi", - "w0muFd": "Pošalji izlazni webhook (jedan po retku)", - "vNiZXF": "Trenutačno nema tekućih izvođenja. Izvedi priručnik za počinjanje određivanja toka rada za tvoj tim i alate.", - "kXFojL": "Priručnik možeš izraditi i unaprijed kako bi bio dostupan kad ti zatreba.", - "b3TdyZ": "Pritiskom na Pokreni probno razdoblje, prihvaćam Sporazum o evaluaciji Mattermost softvera, Pravila o privatnosti i primanje e-pošte o proizvodu.", - "VOzlSL": "Izvođenje priručnika određuje tokove rada za tvoj tim i alate.", - "SXJ98n": "Nakon objavljivanja, izvještaj o retrospektivi nećeš moći promijeniti. Želiš li objaviti izvještaj o retrospektivi?", - "OK8u0r": "Stvori priručnik za određivanje toka rada koji bi tvoji timovi i alati trebali slijediti, uključujući sve popise zadataka, radnje, predloške i retrospektive.", - "D2CE02": "Upiši webhook", - "0q+hj2": "Odredi predložak za sažeti opis koji objašnjava svako izvođenje svojim sudionicima.", - "qsr3Zk": "Aktualiziraj sažetak izvođenja", - "FXCLuZ": "Ukupno {total, number}", - "3PoGhY": "Stvarno želiš objaviti?", - "4fHiNl": "Dupliciraj", - "/urtZ8": "Tvoji priručnici", - "4alprY": "Predlošci priručnika", - "SVwJTM": "Izvezi", - "9XUYQt": "Uvezi", - "4aupaG": "Priručnik {title} je uspješno obnovljen.", - "vQqT/8": "", - "9m0I/B": "Obavještavaj sudionike o aktualnom stanju", - "8n24G2": "Prikaži pojedinosti izvođenja u bočnoj ploči", - "R5Zh+l": "Ovo omogućuje isprobati primjer priručnika prije ulaganja vremena za izradu vlastitog.", - "lgZf0l": "Počni raditi s priručnicima", - "q/Qo8l": "Privatni priručnici dostupni su samo u Mattermost Enterpriseu", - "hw83pa": "Prati ključne mjerne podatke i mjeri vrijednost", - "TxmjKI": "Opiši čemu služi ovaj mjerni podatak", - "mbo96h": "Konfiguriraj prilagođene mjerne podatke za ispunjavanje s izvještajem retrospektive", - "GG1yhI": "Postoje predlošci za niz slučajeva korištenja i događaja. Priručnik možeš koristiti na način kakav je ili ga prilagoditi – zatim ga podijeliti s tvojim timom.", - "fhMaTZ": "Kratki uvod u rad programa", - "1QosTr": "Korišten od", - "Q5hysF": "Uradi više s priručnicima", - "Q3R9Uj": "Ovdje dokumentiraj korake za cijeli proces. Svaki zadatak dodijeli odgovornim pojedincima i po želji dodaj vremenske okvire ili povezane radnje.", - "/fU9y/": "Na ovoj stranici možeš detaljno provjeriti različite odjeljke priručnika.", - "I5DYM+": "Uči i promisli", - "y7o4Rn": "Stvarno želiš izbrisati?", - "OyZnsJ": "po izvođenju", - "uT4ebt": "npr. broj resursa, kupci na koje se odnosi", - "rzbYbE": "Cilj", - "HXvk56": "Objavi aktualiziranja stanja", - "dZmYk6": "Priručnik je uspješno dupliciran", - "Pue+oV": "", - "GAuN6w": "Postavi pretpostavke", - "cEWBE3": "Ocijeni svoje procese koristeći retrospektivu za preciziranje i poboljšavanje tijekom svakog izvođenja.", - "lUfDe1": "Izvezi kanal izvođenja priručnika i spremi ga za kasniju analizu.", - "0Xt1ea": "I dalje ćeš moći pristupiti povijesnim podacima za ovaj mjerni podatak.", - "wbdGb5": "Dodijeli, označi ili preskoči zadatke kako bi timu bilo jasno kako zajedno dostignuti cilj.", - "vJ2SaW": "Automatiziraj aspekte svog priručnika, kao što su slanja poruka dobrodošlice, pozivanje ključnih članova i stvaranje kanala za aktualiziranje.", - "udrLSP": "Koristi mjerne podatke za razumijevanje mustra i napredaka izvođenja te prati preformancu.", - "ZkhArX": "Započnimo!", - "RzEVnf": "Priručnici čine važne postupke ponovljivima i razumljivima. Priručnik se može izvesti više puta, a svako izvođenje ima svoj zapis i retrospektivu.", - "dxyZg3": "Pusti me da istražim", - "a0hBZ0": "Izbriši mjerni podatak", - "1isgPF": "", - "GjCS6U": "Odaberi predložak", - "FGzxgY": "npr. vrijeme za potvrdu, vrijeme za rješavanje", - "F4pfM/": "Upiši broj ili ostavi polje prazno.", - "6GTzTR": "Pogledaj u bilo kojem trenutku što se nalazi u ovom priručniku", - "0EEIkR": "", - "mVpO8u": "Ovo već poznaš?", - "NYTGIb": "Razumijem", - "rMhrJH": "Dodaj naslov za svoj mjerni podatak.", - "Sx3lHL": "Cijeli broj", - "NJ9uPu": "Ključni mjerni podaci", - "tbjmvS": "Mjerni podatak s istim imenom već postoji. Dodaj jedinstveno ime za svaki mjerni podatak.", - "gsMPAS": "Dolari", - "f+bqgK": "Ime mjernog podatka", - "bTgMQ2": "Ovaj priručnik je arhiviran.", - "VZRWFk": "npr. trošak, kupovine", - "LI7YlB": "Dodaj pojedinosti o ovom mjernom podatku i kako ga treba ispuniti. Ovaj će opis biti dostupan na retrospektivnoj stranici za svako izvođenje gdje će se unijeti vrijednosti za te mjerne podatke.", - "LDYFkN": "Trajanje (dd:hh:mm)", - "9SIW2x": "Ciljana vrijednost za svako izvođenje", - "6D6ffM": "Upiši trajanje u formatu: dd:hh:mm (npr. 12:00:00) ili ostavi polje prazno.", - "4BN53Q": "Pokazat ćemo ti koliko je blizu ili daleko od cilja vrijednost svakog izvođenja i također je prikazati u dijagramu.", - "xvBDOH": "Stvarno želiš arhivirati priručnik {title}?", - "lBqu4h": "Obnovi priručnik", - "MTzF3S": "Stvarno želiš obnoviti priručnik {title}?", - "4cwL43": "S arhiviranim", - "QbGfqo": "Šalji sudionicima i zadrži dokument za retrospektivu sa samo jednom objavom.", - "vL4++D": "Prati napredak i vlasništvo", - "q/VD+s": "Postavi mjerače vremena i sastavi predložak za aktualiziranje stanja kako bi sudionici uvijek bili informirani o tijeku razvoja.", - "Tt04f1": "Pogledaj tko je uključen i što se mora obaviti bez napuštanja razgovora.", - "1ikfp3": "Ako izbrišeš ovaj mjerni podatak, vrijednosti za njega neće se prikupljati za nijedno buduće izvođenje.", - "wPVxBN": "", - "JrZ2th": "Dodaj mjerni podatak", - "XpDetT": "Ove savjete više nemoj prikazivati.", - "HGdWwZ": "Stvori i dodijeli zadatke", - "fmbSyg": "Dodaj vrijednost (dd:hh:mm)", - "NMxVd+": "Upiši vrijednost za mjerni podatak.", - "NLeFGn": "do", - "mvZUm3": "Ovdje možeš detaljno istražiti komponente svog priručnika. Odaberi „Uredi” za prilagođavanje priručnika tako da odgovara tvojim procesima i modelima.", - "xVyHgP": "Pokreni testnto izvođenje", - "NiAH1z": "Ciljana vrijednost", - "M4gAc9": "Dodaj vrijednost", - "awG90C": "Cilj po izvođenju", - "Vf/QlZ": "Raspon vrijednosti", - "9a9+ww": "Naslov", - "ZNNjWw": "Upiši broj.", - "KXVV4+": "Dobro došao/la na stranicu pregleda priručnika!", - "ru+JCk": "Prosječna vrijednost", - "l5/RKZ": "Za ovaj priručnik nema dovršenih izvođenja.", - "69nlA3": "Upiši trajanje u formatu: dd:hh:mm (npr. 12:00:00).", - "lbs7UO": "po izvođenju u zadnjih 10 izvođenja", - "efeNi1": "Prosječna vrijednost od 10 izvođenja", - "u7qh13": "Jesi li spreman/na izvesti u svoj priručnik?", - "p1I/Fx": "Automatski smo kreirali tvoje izvođenje", - "ao44YC": "Konfiguriraj mjerne podatke", - "MBNMo9": "Radnje kanala", - "B3Q5mz": "Okidač", - "5AJmOz": "Kad se korisnik pridruži kanalu", - "Y4MU/9": "Odaberi Pokreni testno izvođenje za prikaz kako radi.", - "DPj6DM": "Odaberi Izvedi za prikaz kako radi.", - "0RlzlZ": "Pošalji korisniku privremenu poruku dobrodošlice", - "c23IHq": "Radnje kanala omogućuju automatizaciju aktivnosti za ovaj kanal", - "RUlvbf": "Isprobaj svoj novi priručnik!", - "MHzP9I": "Definiraj poruku dobrodošlice za korisnike koji se pridruže kanalu.", - "hCMWC+": "Počni praćenje za {followers, plural, =0 {nijedan korisnik} =1 {jedan korisnik} few {# korisnika}} other {# korisnika}}", - "u4L4yd": "Imaš nespremljene promjene", - "e3z3P8": "Odbaci i napusti", - "dCtjdj": "Jesi li spreman/na izvesti svoj priručnik?", - "Z3ybv/": "Dodaj kanal u kategoriju bočne trake za korisnika", - "Ob5cSv": "Tvoje promjene se neće spremiti ako napustiš ovu stranicu. Stvarno želiš odbaciti promjene i napustiti stranicu?", - "Ek1Fx2": "Kad se objavi poruka s ovim ključnim riječima", - "9j5KzL": "Upiši ime kategorije", - "2Q5PhZ": "Potvrdi izvođenje priručnika", - "+/x2FM": "Odaberi priručnik", - "+PMJAg": "Počni sljedeće za {followers, plural, =1 {jednog korisnika} few {# korisnika} other {# korisnika}}", - "aEhjYg": "Struktura", - "Ppx673": "Izvještaji", - "zWgbGg": "Danas", - "mLrh+0": "Nema datuma roka", - "iMjjOH": "Sljedeći tjedan", - "MtrTNy": "Sutra", - "I7+d55": "Odredi datum/vrijeme („za 4 sata”, „1. svibnja” …)", - "AF7+5o": "Dodaj datum roka", - "MbapTE": "{num} {num, plural, =1 {zadatak} few {zadatka} other {zadataka}} s prekoračenim rokom", - "mw9jVA": "Dodaj naslov", - "lglICE": "Dodaj opis (opcionalno)", - "W0aij2": "Dodijeli …", - "NFyWnZ": "Povećaj efektivnot tvog rada", - "lkv547": "Datum dospijeća (dostupno u profesionalnom planu)", - "TTIQ6E": "Dodijeli zadacima rokove kako bi osobe kojima su dodijeljeni zadaci mogli odrediti prioritete i obaviti stvari.", - "RQl8IW": "Odgodi za …", - "oAJsne": "Javni priručnik", - "mm5vL8": "Samo pozvani članovi", - "lyXljU": "Dupliciraj zadatak", - "lJ48wN": "Privatni priručnik", - "Xgxruo": "Preskoči popis zadataka", - "OqCzNb": "Dodaj zadatak", - "JcefuP": "Dodaj opis (opcionalno)", - "9trZXa": "Svatko u timu može vidjeti", - "7P5T3W": "Obnovi popis zadataka", - "371AC3": "Aktualiziraj sažetak izvođenja", - "UlJJ1i": "Dodaj naredbu nakon kose crte", - "oBeKB4": "Rok: {date}", - "g9pEhE": "Rok", - "v5/Cox": "Dupliciraj popis zadataka", - "mCrdeS": "Ukupni broj izvođenja priručnika", - "k12r+v": "Dodaj predložak za sažetak izvođenja …", - "cyR7Kh": "Natrag", - "XF8rrh": "Kopiraj poveznicu u „{name}”", - "RrCui3": "Sažetak", - "MyIJbr": "Sadržaj", - "IxtSML": "Dodaj popis zadataka", - "CwwzAU": "Dodaj ime popisa zadataka", - "5ZIN3u": "Aktualiziranja stanja", - "4GjZsL": "Ukupni broj priručnika", - "kYCbJE": "Dodaj vremenski okvir", - "c6LNcW": "Izbriši zadatak", - "x1phlu": "Bez vremenskog okvira", - "sGJpuF": "Dodaj opis …", - "OuZhcQ": "Odredi trajanje („8 sati”, „3 dana” …)", - "OKhRC6": "Dijeli", - "LcC/pi": "Pošalji pozdravnu poruku …", - "F9LrJA": "Filtriraj unose", - "DaHpK1": "Traži kanal", - "9kQNdp": "Ovaj priručnik je privatan.", - "3hBelc": "Retrospektiva se ne očekuje.", - "XRyRzf": "Aktualiziranja stanja se ne očekuju.", - "Brya9X": "Dodaj predložak sažetka izvođenja …", - "28FTjr": "Radnje izvođenja omogućuju automatiziranje aktivnosti za ovaj kanal", - "j940pJ": "Ovo aktualiziranje će se spremiti u stranici pregleda.", - "YQOmSf": "Upiši jedan webhook po retku", - "sX5Mn5": "Upiši jedan webhook po retku", - "HvAcYh": "{text}{rest, plural, =0 {} one { i drugi} few { i {rest} druga} other { i {rest} drugih}}", - "/RnCQb": "Pošalji izlazni webhook", - "giM/X9": "Aktualiziranje stanja se očekuju svakih . Nova aktualiziranja će se objaviti na {channelCount, plural, =0 {nijednom kanalu} one {# kanalu} few {# kanala} other {# kanala}} i {webhookCount, plural, =0 {nijedan izlazni webhook} one {# izlazni webhook} one {# izlazna webhooka} other {# izlazna webhooka}} .", - "aZGAOI": "Dodaj predložak za aktualiziranje stanja …", - "kV5GkX": "Kad se objavi aktualiziranje stanja", - "kkw4kS": "Ovo će se aktualiziranje poslati na {hasChannels, select, true {{broadcastChannelCount, plural, =1 {jedan kanal} few {{broadcastChannelCount, number} kanala} other {{broadcastChannelCount, number} kanala}}} other {}}{hasFollowersAndChannels, select, true { i } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {jednu izravnu poruku} few {{followersChannelCount, number} izravne poruke} other {{followersChannelCount, number} izravnih poruka}}} other {}}.", - "mkLeuq": "Šalji aktualiziranje na odabrane kanale", - "zl6378": "Konfiguriraj mjerne podatke u retrospektivi", - "yllba1": "Ovaj se arhivirani priručnik ne može preimenovati.", - "xHNF7i": "Radnje izvođenja", - "xEQYo5": "Konfiguriraj prilagođene mjerne podatke za ispunjavanje s izvještjem retrospektive.", - "TD8WrM": "Dupiciranje je za ovaj tim deaktivirano.", - "OQplDX": "Aktualiziranje stanja se očekuju svakih . Nova aktualiziranja će se slati na {channelCount, plural, =0 {nijedan kanal} one {# kanal} few {# kanala} other {# kanala}} i {webhookCount, plural, =0 {nijedan izlazni webhook} one {# izlazni webhook} few {# izlazna webhooka} other {# izlaznih webhooka}}.", - "vSMfYU": "Podaci izvođenja", - "oL7YsP": "Zadnja promjena {timestamp}", - "Z2Hfu4": "Dodaj sažetak izvođenja", - "o6N9pU": "Pokreni radnje", - "iigkp8": "Vrijeme za dovršavanje?", - "hjteuA": "Ovdje će se prikazati svi priručnici kojima možeš pristupiti", - "ZJS10z": "Još nije objavljeno nijedno aktualiziranje", - "Q15rLN": "Zatraži aktualiziranje …", - "GDCpPr": "Nedavno aktualiziranje stanja", - "+qDKgW": "Prikaži sva aktualiziranja", - "opn6uf": "Prikaži vremensku crtu", - "lbr3Lq": "Kopiraj poveznicu", - "bf5rs0": "Prikaži informacije", - "kEMvwX": "Nema izvođenja koja odgovaraju tim filtrima.", - "GXjP8g": "Sva izvođenja kojima možeš pristupiti prikazat će se ovdje", - "ocYb9S": "Ključni mjerni podaci", - "nc8QpJ": "Nedavne aktivnosti", - "m/KtHt": "Nemaš dozvole za mijenjanje vlasnika", - "RnOiCg": "Nije bilo moguće {isFollowing, select, true {ne pratiti} other {pratiti}} izvođenje", - "4mCpAv": "Nije biilo moguće prominijeti vlasnika", - "CFysvS": "Stvori padajući izbornik priručnika", - "VpQKQE": "{displayName} nije sudionik izvođenja. Želiš li ih učiniti sudionikom? Imat će pristup cjelokupnoj povijesti poruka u kanalu izvođenja.", - "Jli9m7": "Poruka će se poslati kanalu za izvođenje, zahtjevajući slanje aktualiziranja.", - "lr1CUA": "Pregledaj priručnike", - "jboo9u": "Zatraži aktualiziranje", - "Xx0WZV": "Pošalji poruku", - "Ul0aFX": "Uvezi priručnik", - "UePrSL": "{num} {num, plural, one {sudionik} other {sudionika}}", - "UMFnWV": "Prikaži retrospektivu", - "RCT0Px": "Dodaj {displayName} kanalu", - "P9PKvb": "Kanalu izvođenja je poslana poruka.", - "NGqzDU": "Potvrdi zahtjev za aktualiziranje", - "LfhTNW": "Pretraži ili stvori priručnike i izvođenja", - "KeO51o": "Kanal", - "JvEwg/": "Nije bilo moguće zatražiti aktualiziranje", - "GVpA4Q": "Stvori novi priručnik", - "9xs0pp": "Dodaj vrijednost …", - "/qDObA": "Pretraži izvođenja", - "/+8SGX": "Prikaz {filteredNum} od {totalNum} događaja", - "ch4Vs1": "Zatraži aktualiziranje za izvođenja priručnika jednim pritiskom miša i primaj obavijesti kad se objavi aktualiziranje. Isprobaj funkciju pokretanjem besplatne probne verzije od 30 dana.", - "zW/5AB": "Profesionalna značajka Ovo je značajka koja se naplaćuje, dostupna s besplatnim probnim razdobljem od 30 dana", - "vDvWJ6": "Pokušaj zatražiti aktualiziranje s besplatnom probom", - "pFK6bJ": "Prikaži sve", - "lKeJ+i": "Nema sažetka", - "hIWK05": "Kanalu izvođenja će se poslati poruka sa zahtjevom da te se doda kao sudionika.", - "U8u4uF": "Uključi se", - "J2NmIY": "Potvrdi uključivanje", - "MD6oav": "Nije bilo moguće zatražiti uključivanje", - "3O8M5M": "Poslan je zahtjev za izvođenje kanala.", - "u6Fyic": "Tvoj zahtjev je poslan na kanal izvođenja.", - "pzTOmv": "Pratitelji", - "pXWclp": "Tvoj zahtjev za sudjelovanjem bit će poslan na kanal izvođenja.", - "PdRg+3": "Prikaži sve …", - "P6NEL/": "Naredba …", - "Nf9oAA": "Pridružit ćeš se ovom izvođenju.", - "5PpBsd": "Tvoj zahtjev nije bio uspješan.", - "4Iqlfe": "Pridružio/la si se ovom izvođenju.", - "1fXVVz": "Datum roka …", - "1GOpgL": "Zadužena osoba …", - "xfnuXm": "Sudjeluj", - "wRM2AO": "Zahtjev za aktualiziranjem nije uspio.", - "wGp7l3": "{icon} Dolari", - "s+rSpl": "{icon} Cijeli broj", - "ojQue/": "{icon} Trajanje (dd:hh:mm)", - "mNgqXf": "Za otključavanje ove funkcije:", - "b+DwLA": "Zatraži sudjelovanje u ovom izvođenju.", - "SMrXWc": "Favoriti", - "PoX2HN": "Pošalji zahtjev", - "PWmZrW": "Prikaži sva izvođenja", - "PW+sL4": "--", - "Gwmqz5": "Zatraži aktualiziranje", - "CV1ddt": "Sudjeluj u izvođenju", - "B9z0uZ": "Tvoj zahtjev za pridruđivanje izvođenju nije uspio.", - "AH+V3r": "Postani sudionik izvođenja.", - "5HXkY/": "Vrsta: {typeTitle}", - "CUhlqp": "slika proizvoda vježbi", - "j2VYGA": "Prikaži sve priručnike", - "qp5G0Z": "Pristup funkcijama retrospektive zahtijeva nadogradnju.", - "ePhhuK": "Tvoj zahtjev je poslan kanalu izvođenja.", - "OfN7IN": "Kanalu izvođenja će se poslati zahtjev za aktualiziranjem stanja.", - "KzHQCQ": "Nema završenih izvođenja koja odgovaraju tim filtrima.", - "3zF589": "Resetetiraj na sve {filterName}", - "+6DCr9": "Kao sudionik, možeš objavljivati aktualiziranja stanja, dodjeliti i dovršiti zadatke i izvoditi retrospektive.", - "wBZz47": "Napustio/la si izvođenje.", - "mttASm": "Napusti i prestani pratiti izvođenje", - "lpWBJE": "Potvrdi napuštanje i prestanak praćenja", - "hnYSP3": "Kad napustiš i prestaneš pratiti izvođenje, izvođenje se uklanja iz lijeve bočne trake. Izvođenje možeš ponovo pronaći pregledom svih izvođenja.", - "gfUBRi": "Odredi novog vlasnika prije napuštanja izvođenja.", - "XS4umx": "{name} je propustio jedno aktualiziranje stanja", - "SK5APX": "Nije bilo moguće napustiti izvođenje.", - "Mjq//Y": "Ukloni iz favorita", - "AhY0vJ": "Napusti i nemoj više pratiti", - "5Hzwqs": "Favorit", - "iEtImk": "Kada napustiš{isFollowing, select, true { i prekineš pratiti izvođenje} other { izvođenje}}, ono se uklanja iz lijeve bočne trake. Možeš ga ponovo pronaći prikazom svih izvođenja.", - "fnihsY": "Napusti", - "cnfVhV": "Napusti {isFollowing, select, true { i prekini pratiti } other {}}izvođenje", - "Suyx6A": "Uvoz priručnika nije uspio. Provjeri ispravnost JSON-a i pokušaj ponovo.", - "QegBKq": "Pridruži se priručniku", - "Q4sutg": "Potvrdi napuštanje{isFollowing, select, true { i prekid praćenja} other {}}", - "P6PLpi": "Pridruži se", - "FgydNe": "Prikaži", - "qGlwfc": "Pokreni izvođenje", - "j2FnDV": "Stvorit će se kanal s tim imenom", - "vqmRBs": "Potvrdi ponovno pokretanje izvođenja", - "k5EChD": "Stvarno želiš ponovo pokrenuti izvođenje?", - "iQhFxR": "Zadnje korišteno", - "Zg0obP": "Ponovo pokreni izvođenje", - "KjNfA8": "Neispravno vrijeme trajanja", - "03oqA2": "Aktivna izvođenja", - "XnICdK": "Nije bilo moguće pridružiti se izvođenju", - "unwVil": "Zahtjev za pridruživanje kanalu nije uspio.", - "ZRv7Dm": "Zahtjev za pridruživanje", - "M9tXoZ": "Zahtjev za pridruživanje će se poslati kanalu izvođenja.", - "0QD99o": "Zatraži pridruživanje kanalu", - "FLG4Iu": "Odredi vlasnika izvođenja", - "fVMECF": "Sudionik", - "q48ca7": "Pošalji povratne informacije za Playbooks.", - "bCmvTY": "Pošalji povratne informacije", - "6rygzu": "Ukloni iz izvođenja", - "0Azlrb": "Upravljaj", - "/GCoTA": "Isprazni", - "w4Nhhb": "Dodaj sudionika", - "jrOlPO": "Primaj obavijesti o aktualiziranju stanja izvođenja", - "cUCiWw": "Postani sudionik", - "1OVPiC": "Postani sudionik izvođenja. Kao sudionik, možeš objavljivati aktualiziranja stanja, dodjeljivati i završavati zadatke i izvoditi retrospektive.", - "jAo8dd": "Aktualiziranja stanja izvođenja je deaktivirao/la {name}", - "b8Gps8": "Aktualiziranja stanja izvođenja je aktivirao/la {name}", - "WFA0Cg": "Stvarno želiš aktivirati aktualiziranja stanja za ovo izvođenje?", - "ieL3dC": "Postavi radnje kanala", - "Z18I+c": "Radnje kanala omogućuju automatiziranje aktivnosti za kanal", - "Y1EoT/": "Kad sudionik napusti izvođenje", - "wCDmf3": "Aktiviraj aktualiziranja", - "utHl3F": "Dodaj osobe u {runName}", - "qDxsQH": "Postani sudionikom za interakciju s ovim izvođenjem", - "nsd54s": "Potvrdi deaktiviranje aktualiziranja stanja", - "lqzBNa": "Ukloni ih iz kanala izvođenja", - "l/W5n7": "Sudionici će se također dodati na kanal povezan s ovim izvođenjem", - "ha1TB3": "Kad se sudionik priduži izvođenju", - "cpGAhx": "Stvarno želiš deaktivirati aktualiziranja stanja za ovo izvođenje?", - "WC+NOj": "Također dodaj osobe na kanal povezan s ovim izvođenjem", - "1OluNs": "Potvrdi aktiviranje aktualiziranja stanja", - "H7IzRB": "Deaktiviraj aktualiziranja stanja", - "9qqGGd": "Pozovi sudionike", - "5b1zuB": "Dodaj ih kanalu izvođenja", - "1prgB2": "Traži osobe", - "//o1Nu": "Deaktiviraj aktualiziranja", - "ecS/qx": "{name} je dodao/la {num} sudionika u izvođenje", - "VM75su": "{name} je uklonio/la {num} sudionika iz izvođenja", - "TnUG7m": "Nemaš nijedan neobavljen dodijeljeni zadatak.", - "SRqpbI": "{assignedNum, plural, =0 {Nema dodijeljenih zadataka} few {# dodijeljena} other {# dodijeljenih}}", - "CgAtTJ": "{overdueNum, plural, =0 {} few {# prekoračena roka} other {# prekoračenih rokova}}", - "grv9Fm": "Odaberi za prikazivanje popisa zadataka.", - "WFd88+": "Pokaži pregledane zadatke", - "DUU48k": "Ne postoji nijedan tebi izričito dodijeljeni zadatak. Proširi pretraživanje pomoću filtara.", - "zSOvI0": "Filtri", - "u/yGzS": "{name} je dodao/la @{user} u izvođenje", - "t6lwwM": "{requester} je uklonio/la {users} iz izvođenja", - "qxYWTy": "Pokaži sve zadatke izvođenja koja posjedujem", - "jfpnye": "@{user} je napustio/la izvođenje", - "feNxoJ": "{requester} je dodao/la {users} u izvođenje", - "YBvwXR": "Nema dodijeljenih zadataka", - "SwlL5j": "@{user} se pridružio/la izvođenju", - "RXjd3Q": "{name} je ukolonio/la @{user} iz izvođenja", - "I0NIMp": "Tvoji zadaci", - "fBG/Ge": "Trošak", - "9X3jwi": "{icon} trošak", - "meD+1Q": "SUDIONICI IZVOĐENJA", - "lqceIp": "ili uvezi priručnik", - "iH5e4J": "Također ćeš biti dodan/a na kanal povezan s ovim izvođenjem.", - "dK2JKl": "Poveži na postojeći kanal", - "VjJYEV": "npr. utjecaj na prodaju, kupnje", - "UAS7Bn": "Zatraži pristup kanalu povezan s ovim izvođenjem", - "NGKqOC": "Također me dodaj u kanal povezan s ovim izvođenjem", - "L6vn9U": "Sudionici izvođenja", - "IdTL+v": "Stvori kanal izvođenja", - "Gg/nch": "NE SUDJELUJE", - "BJNrYQ": "Kao sudionik, moći ćeš aktualizirati sažetak rada, označiti zadatke, objaviti aktualiziranja stanja i urediti retrospektivu.", - "36NwLv": "Upravljaj popisom sudionika izvođenja", - "2BCWLD": "Konfiguriraj kanal", - "ORJ0Hb": "{outstanding, plural, =1 {Postoji # neobavljen zadatak} few {Postoje # neobavljena zadatka} other {Postoji # neobavljenih zadataka}}. Stvarno želiš završiti izvođenje za sve sudionike?", - "a2r7Vb": "Privatni kanal", - "VA1Q/S": "Javni kanal", - "AG7PKJ": "Preimenuj izvođenje", - "0boT49": "Stvarno želiš završiti izvođenje za sve sudionike?", - "zxj2Gh": "Zadnji put aktualizirano {time}", - "yP3Ud4": "Nema izvođenja u tijeku koji su povezani s ovim kanalom", - "tqAmbk": "Izvođenja u tijeku", - "prs4kX": "Kad se objavi poruka s određenim ključnim riječima", - "m8hzTK": "Zadnji put korišteno {time}", - "kQAf2d": "Odaberi", - "gS1i4/": "Označi zadatak kao obavljen", - "gGtlrk": "Tvoji priručnici", - "fvNMLo": "Radnje za zadatak", - "cGCoJe": "Autor objave", - "Z1sgPO": "Pogledaj završena izvođenja", - "RgQwWr": "Razvrstaj izvođenja prema", - "RC6rA2": "Nedavno stvoreni", - "Q/t0//": "Završena izvođenja", - "NNksk4": "Abecednim redom", - "GZoWl1": "Automatiziraj aktivnosti za ovaj zadatak", - "AoNLta": "Nema dovršenih izvođenja povezanih s ovim kanalom", - "95v+5O": "{actions, plural, =0 {Radnje za zadatak} one {# radnja} few {# radnje} other {# radnji}}", - "3sXVwy": "Radnje za zadatak …", - "2NDgJq": "Zadnje aktualiziranje stanja", - "3Yvt4d": "Priručnici su konfigurabilni popisi koji definiraju ponovljiv proces za timove kako bi postigli specifične i predvidljive rezultate", - "Wy3sw+": "{count, plural, =1{1 izvođenje u tijeku} =0 {Nema izvođenje u tijeku} few {# izvođenja u tijeku} other {# izvođenja u tijeku}}", - "zscc/+": "{outstanding, plural, =1 {Postoji # neobavljen zadatak} few {Postoje # neobavljena zadatka} other {Postoji # neobavljenih zadataka}}. Stvarno želiš završiti izvođenje {runName} za sve sudionike?", - "bEoDyV": "@{authorUsername} je objavio/la aktualiziranje za [{runName}]({overviewURL})", - "ZSa3cf": "@{targetUsername}, aktualiziraj stanje za [{runName}]({playbookURL}).", - "W1EKh5": "Stvori novi priručnik", - "SRbTcY": "Drugi priručnici", - "LKu0ex": "Stvarno želiš završiti izvođenje {runName} za sve sudionike?", - "L1tFef": "Provjeri pravopis ili pokušaj jednu drugu pretragu", - "KQunC7": "Korišteno u ovom kanalu", - "HfjhwE": "Traži priručnike", - "EVSn9A": "Pokreni izvođenje", - "9AQ5FE": "Sažetak izvođenja", - "7KMbBa": "Nikada korišteno", - "0CeyUV": "Nema rezultata za „{searchTerm}”", - "IE2BzH": "Postoje korisnici kojima je unaprijed dodijeljen jedan ili više zadataka. Deaktiviranjem pozivnica izbrisat ćeš sve unaprijed dodijeljene zadatke.{br}{br}Stvarno želiš deaktivirati pozivnice?", - "DQn9Uj": "Korisniku {name} je unaprijed dodijeljen jedan ili više zadataka. Ako ovog korisnika ne pozoveš automatski, izbrisat ćeš njegove unaprijed dodijeljene zadatke.{br}{br}Stvarno želiš prestati pozivati ovog korisnika kao člana izvođenja?", - "Bgt0C8": "Ovo aktualiziranje izvođenja {runName} će se prenijeti na {hasChannels, select, true {{broadcastChannelCount, plural, =1 {jednom kanalu} few {{broadcastChannelCount, number} kanala} other {{broadcastChannelCount, number} kanala}}} other {}}{hasFollowersAndChannels, select, true { i } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {jednu izravnu poruku} few {{followersChannelCount, number} izravne poruke} other {{followersChannelCount, number} izravnih poruka}}} other {}}.", - "BiQjuS": "Izvođenje premješteno u {channel}", - "9w0mDI": "Potvrdi uklanjanje unaprijed zaduženog člana", - "uCS6py": "Nemaš dozvole za uvid u ovaj priručnik", - "l3QwVw": "Odaberi kanal", - "ksG35Q": "Nemaš dozvole za izradu priručnika u ovom radnom prostoru.", - "k7Nzfi": "Deaktiviraj poziv", - "fwW0T1": "Potvrdi uklanjanje unaprijed zaduženih članova", - "YKLHXL": "Prikaži izvođenja u tijeku", - "TP/O/b": "Ukloni korisnika", - "QvEO6m": "Nemaš dozvole za mijenjanje ovog izvođenja", - "QJTSaI": "Poveži izvođenje s jednim drugim kanalom", - "mILd++": "Ime izvođenja ne smije sadržati više od {maxLength} znakova", - "MieztS": "Ispusti izvoznu datoteku priručnika za uvoz.", - "uYrkxy": "Datoteka mora biti valjani JSON predložak priručnika.", - "m4vqJl": "Datoteke", - "Zbk+OU": "Veličina datoteke premašuje ograničenje od 5 MB.", - "HGSVzc": "Nije moguće uvesti više datoteka odjednom.", - "LaseGE": "Nemaš prava za uređivanje ovog popisa zadataka", - "Edy3wX": "Popis zadataka premješten u {channel}", - "8//+Yb": "Poveži popis zadataka s jednim drugim kanalom", - "706Soh": "obavljeni zadaci", - "vjb+hS": "{user} je obnovio/la element popisa zadataka „{name}”", - "OqWwvQ": "{user} je odznačio/la element popisa zadataka „{name}”", - "DKiv0o": "{user} je preskočio/la element popisa zadataka „{name}”", - "8FzC0B": "{user} je označio/la element popisa zadataka „{name}” kao obavljen", - "3qPQMX": "{name} je zatražio/la aktualiziranje stanja", - "XHJUSG": "Automatski prati izvođenja", - "DqTQOp": "Jednom", - "9M92On": "Odaberi kanale", - "N7Ln74": "Ponovi", - "DoskyC": "Svi timovi", - "9S+ZiL": "Nijedan tim nije odabran" -} diff --git a/webapp/playbooks/i18n/hu.json b/webapp/playbooks/i18n/hu.json deleted file mode 100644 index d11f68071dd..00000000000 --- a/webapp/playbooks/i18n/hu.json +++ /dev/null @@ -1,729 +0,0 @@ -{ - "5A46pW": "Perjeles parancs hozzáadása", - "47FYwb": "Mégsem", - "3rCdDw": "Állapot frissítések", - "1MQ3XZ": "{numActiveRuns, plural, =0 {nincs aktív futás} other {# aktív futás}}", - "1I48bs": "Visszatekintő sablon", - "/jUtaM": "AKTÍV FUTÁSOK naponta az elmúlt 14 napban", - "/1FEJW": "AKTÍV RÉSZTVEVŐK naponta az elmúlt 14 napban", - "+ZIXOR": "Csatorna hozzáférés", - "+QgvjN": "A tulajdonosi jogkör hozzárendelése ehhez", - "+8G9qr": "Alapértelmezett szöveg a visszatekintőre.", - "5Ot7cd": "Csatorna típusának kiderítése amit ez a forgatókönyv létrehoz.", - "CL5OZP": "Csak az Ön által megadott felhasználók fogják tudni szerkeszteni és futtatni ezt a forgatókönyvet.", - "AT2QBo": "Csak a kiválasztott felhasználók tudnak létrehozni forgatókönyveket.", - "X3DLGJ": "Mindenki ebben a munkaterületben létrehozhat forgatókönyveket.", - "ebkl6I": "Mindenki ebben a csapatban eléri ezt a forgatókönyvet", - "v3+TmO": "{members, plural, =0 {Senki sem} =1 {Egy személy} other {# személy}} éri el ezt a forgatókönyvet", - "z5RMPO": "Csak Ön éri el ezt a forgatókönyvet", - "jq4eWU": "Forgatókönyv hozzáférés", - "jnmORb": "Ebben a forgatókönyvben", - "TyrY2b": "Forgatókönyv létrehozás", - "6Lwe7T": "Mindenki a {team} csapatban hozzáfér ehhez a forgatókönyvhöz", - "5FRgqE": "Csatorna naplózásának letöltése", - "eiPBw7": "Visszatekintő emlékeztetési időköz", - "bLK+Kr": "Adott időközönként emlékezteti a csatornát, hogy töltsék ki a visszatekintőt.", - "c8hxKk": "{date} hete", - "hXIYHG": "Telepítse és engedélyezze a Csatorna Exportáló bővítményt, hogy támogassa a csatorna exportálást", - "nvy0pS": "Amikor egy futás befejeződött, exportálja a csatornát", - "oS0w4E": "Alapértelmezett frissítési ütemezés", - "djXM+y": "Csak a kiválasztott felhasználóknak van hozzáférése.", - "d9epHh": "Csatorna naplójának exportálása", - "bPLen5": "Futások amik befejeződtek az elmúlt 30 napban", - "bGhCLX": "Amikor egy frissítés elküldésre kerül", - "ZwlIYH": "{activeRuns, number} aktív futás", - "YDuW/T": "{num_runs, plural, =0 {Még nem futott} one {# futás} other {# összes futás}}", - "XmUdvV": "Az összes statisztika amire szüksége van", - "VmpFFw": "Jelenleg nincs leírás.", - "Ui6GK/": "Amikor egy új résztvevő csatlakozik a csatornához", - "zy3cJT": "Azonnal indítsa el ezt a forgatókönyvet amikor a felhasználó üzenetet küld ami tartalmazza a kulcsszót", - "zINlao": "Tulajdonos", - "wbwhbH": "Feladat neve", - "wbsq7O": "Felhasználás", - "waVyVY": "Jelenleg aktív résztvevők", - "wL7VAE": "Műveletek", - "wEQDC6": "Szerkesztés", - "viXE32": "Privát", - "usa8vQ": "Üdvözlő üzenet küldése", - "uhu5aG": "Nyilvános", - "t6SiGO": "Futás folyamatban", - "soePYH": "{num_checklists, plural, =0 {nincs teendőlista} other {# teendőlista}}", - "sQu1rA": "{numTotalRuns, plural, =0 {nem indult el futás} other {# futás indult el}}", - "s3jjqi": "{num_actions, plural, =0 {nincs művelet} other {# művelet}}", - "recCg9": "Frissítések", - "rX08cW": "A dátumnak a jövőben kell lennie.", - "lbhO3D": "dőlt", - "lZwZi+": "Nap: {date}", - "hO9EdA": "Hívjon meg {numInvitedUsers, plural, =0 {egy tagot sem} =1 {egy tagot} other {# tagot}} a csatornába", - "dcV/DJ": "{timestamp}", - "hzt6l8": "Használjon Markdownt a sablon létrehozásához.", - "jvo0vs": "Mentés", - "jXT2++": "Ugrás a csatornára", - "jS/UOn": "Sablon frissítése", - "jIIWN+": "előformázott", - "gy/Kkr": "(szerkesztett)", - "g5pX+a": "Névjegy", - "eHAvFf": "vastag", - "b40Pr7": "Bejelentő", - "TZYiF/": "áthúzott", - "TSSNg/": "ÖSSZES FUTÁS elindítva hetente az elmúlt 12 hétben", - "TJo5E6": "Előnézet", - "T7Ry38": "Üzenet", - "T5rX+W": "Milyen gyakran legyen egy frissítés elküldve?", - "SFuk1v": "Engedélyek", - "SENRqu": "Súgó", - "SDSqfA": "Amikor egy futás elindul", - "RthEJt": "Visszatekintő", - "RoGxij": "Futások aktívok a {date} napon", - "R+JQaJ": "Csatorna tagjai", - "QnZAit": "Adjon meg opcionális leírást", - "QiKcO7": "Adja meg a visszatekintő sablont", - "Q8Qw5B": "Leírás", - "Q7aZO4": "{numParticipants, plural, =0 {nincsenek aktív résztvevők} other {# aktív résztvevő}}", - "ObmjTB": "Perjeles parancs", - "OINwWS": "Egy {isPublic, select, true {nyilvános} other {privát}} csatorna létrehozása", - "NE1OeI": "Mindenkinek a csapatban ({team}) van hozzáférése.", - "MFpAtm": "{numTasks, number} feladat", - "LRFvqz": "Bejelentés a {oneChannel, plural, one {csatornában} other {csatornákban}}", - "KiXNvz": "Indítás", - "KUr+sG": "Futás összefoglaló frissítése", - "IuFETn": "Időtartam", - "ICqy9/": "Teendőlista", - "Hzwzgs": "Tegye közzé a frissítéseket a {oneChannel, plural, one {csatornában} other {csatornákban}}", - "HhLp57": "idézet", - "EC5MJD": "Nincsenek elérhető frissítések.", - "DnBhRg": "Személy hozzáadása", - "DCl7Vv": "soron belüli kód", - "D3idYv": "Beállítások", - "CjNrqO": "Visszatekintő jelentés sablon", - "BD66u6": "CSV letöltése ami a csatorna összes üzenetét tartalmazza", - "AS5kar": "Résztvevők ({participants})", - "A3ptul": "Sablonok", - "9uOFF3": "Áttekintés", - "8hDbW6": "Küldjön egy kimenő webhorog hívást", - "/HtNUp": "Válasszon ki vagy {mode, select, DurationValue {adjon meg egy időtartamot (\"4 hours\", \"7 days\"...)} DateTimeValue {adja meg az időpontot (\"in 4 hours\", \"May 1\", \"Tomorrow at 1 PM\"...)} other {adja meg az időpontot vagy időtartamot}}", - "AML4RW": "Feladat kiosztások", - "LmhSmU": "Tétel törlésének jóváhagyása", - "MvEydR": "{name} közzétett egy állapot frissítést", - "N1U/QR": "Feladat állapot változások", - "TvihSy": "Újra közzététel", - "b5FaCc": "A csatorna hozzáadása az oldalsáv kategóriákhoz", - "vndQuC": "Perjeles parancs végrehajtva", - "yhzuSC": "Idő: {time}", - "zELxbG": "Mentett üzenetek", - "zWkvNO": "Idővonal", - "x5Tz6M": "Jelentés", - "w7tf2z": "Közzétett", - "syEQFE": "Közzététel", - "W9j0FJ": "{date}", - "OsDomv": "Minden esemény", - "OcpRSQ": "Tétel törlése", - "9Obw6C": "Szűrő", - "3/wF0G": "Perjel parancsok", - "o2eHmz": "{name} befejezte a futást", - "fXGjhC": "Tulajdonos meg lett változtatva, régi: {summary}", - "egvJrY": "Hozzárendelt meg lett változtatva", - "Z/hwEf": "A csatorna emlékeztetve lesz, hogy futtassa a visszatekintőt {reminderEnabled, select, true {minden} other {}}", - "4Hrh5B": "{name} megváltoztatta az állapotot, régi: {summary}", - "ArpdYl": "Idővonal események itt fognak megjelenni amint bekövetkeznek. Vigye fölé az egeret az eltávolításhoz.", - "DXACD6": "Visszatekintő jelentés közzététele és az idővonal megtekintése", - "FEGywG": "Kérem adjon meg egy jövőbeni dátumot a frissítési emlékeztetőnek.", - "JeqL8w": "{name} megszakította a visszatekintőt", - "UbTsGY": "A futás el lett indítva {start} és {end} között", - "aACJNp": "{name} elindította a futást", - "fUEpLA": "Nincsennek a szűrésnek megfelelő Idővonal események.", - "pKLw8O": "Biztosan törölni szeretné ezt az eseményt? A törölt események véglegesen törlésre kerülnek az idővonalról.", - "v1DNMW": "Visszatekintő közzé lett téve {name} által", - "v1SpKO": "Jogosultság módosítás", - "vOFN0m": "Állapot üzenet törölve:", - "izWS4J": "Követés visszavonása", - "ieGrWo": "Követés", - "tzMNF3": "Állapot", - "sGJpuF": "Leírás hozzáadása…", - "rbrahO": "Bezárás", - "oVHn4s": "Utolsó frissítés", - "kGI46P": "Feladat leírása", - "jwimQJ": "Ok", - "guunZt": "Hozzárendelés", - "eKv7yX": "Küldés", - "dsTLW1": "Feladat szerkesztése", - "c6LNcW": "Feladat törlése", - "aYIUar": "Köszönjük!", - "Z7vWDQ": "Hiba lépett fel", - "W/V6+Y": "Összecsuk", - "QaZNp9": "Futás befejezése", - "N2IrpM": "Jóváhagyás", - "L6k6aT": "...vagy kezdjen egy sablonból", - "K4O03z": "Új feladat", - "K3r6DQ": "Törlés", - "JXdbo8": "Kész", - "JJNc3c": "Előző", - "Ietscn": "Feladat befejezve", - "I90sbW": "éppen most", - "HAlOn1": "Név", - "G/yZLu": "Eltávolítás", - "CSts8B": "Csapat ikon", - "CBM4vh": "Időzítő a következő frissítésre", - "B487HA": "Folyamatban", - "ApULhK": "Tagok meghívása", - "9+Ddtu": "Következő", - "6jDabx": "Visszajelzés adása", - "5CI3KH": "Kapcsolatfelvétel a támogatással", - "3Psa+5": "Kulcsszavak hozzáadása", - "0wJ7N+": "Feladat", - "VOzlSL": "A forgatókönyvek futtatásával összehangolhatja a munkafolyamatokat a csapata és az eszközei számára.", - "b3TdyZ": "A Próbaidőszak kezdése gombra kattintva elfogadom a Mattermost szoftverértékelési megállapodást, az Adatvédelmi szabályzatot és a termékről szóló e-mailek fogadását.", - "z3A0LP": "Utolsó futás {relativeTime}", - "yqpcOa": "Használ", - "vNiZXF": "Jelenleg nincsenek futások folyamatban. Futtasson egy forgatókönyvet, hogy elkezdhesse a munkafolyamatok összehangolását a csapata és az eszközei számára.", - "uJ3bRR": "Ez a sablon segít egységesíteni a tömör leírás formátumát, amely minden egyes futást elmagyaráz az érdekeltek számára.", - "d8KvXJ": "Az próbaidőszak {expiryDate} napon fog lejárni. A fennakadások elkerülése érdekében bármikor vásárolhat licencet a Vevői portálon keresztül.", - "avPeEI": "Emelje meg az összes futás, az aktív futások és ezen forgatókönyv futásaiban részt vevő résztvevők trendjeinek megtekintéséhez.", - "X/koAN": "Érvénytelen bejegyzés: a maximálisan megengedett webhorgok száma 64", - "WTQpnI": "Tegyen lépéseket a forgatókönyvek használatával", - "VmnoW8": "Kérem tekintse meg a rendszer naplózást további információért.", - "TDaF6J": "Elutasítás", - "TdTXXf": "Tudjon meg többet", - "TBez4r": "Nincsenek forgatókönyvek amiket megtekinthet. Nincsen jogosultsága forgatókönyvet létrehozni ebben a munkaterületben.", - "SmAUf9": "Az emlékeztető el lesz küldve {timestamp}", - "S0kWcH": "A frissítés késésben van", - "Rgo4VW": "Ezen a munkaterületen mindenki létrehozhat forgatókönyveket. A rendszergazdák megváltoztathatják ezt a beállítást.", - "qp3Fk4": "A forgatókönyv egy munkafolyamat, amit a csapatainak és az eszközöknek követniük kell, beleértve a feladatlistáktól, műveletektől, sablonoktól és visszatekintésektől kezdve midnent.", - "OK8u0r": "Készítsen forgatókönyvet, amely előírja a munkafolyamatot amelyet a csapatoknak és az eszközöknek követniük kell, beleértve a feladatlistáktól, műveletektől, sablonoktól és visszatekintésektől kezdve mindent.", - "MhKICa": "Az előfizetése egy forgatókönyvet engedélyez csapatonként. Emelje meg előfizetését, és hozzon létre több forgatókönyvet egyedi munkafolyamatokkal minden csapat számára.", - "JCGvY/": "Ez a sablon segít egységesíteni az ismétlődő frissítések formátumát, amelyek minden egyes futás során megtörténnek.", - "BNB75h": "A forgatókönyv előírja a feladatlistákat, automatizálásokat és sablonokat minden megismételhető eljáráshoz. {br} Segít a csapatoknak a hibák csökkentésében, az érdekelt felek bizalmának kiépítésében, és minden egyes iterációval hatékonyabbá válik.", - "I2zEie": "Ünnepelje a sikereket és tanuljon a hibákból a visszatekintő jelentésekkel. Szűrje az idővonal eseményeit a folyamatok felülvizsgálatához, az érdekelt felek bevonásához és az auditáláshoz.", - "9kCT7Q": "Könnyítse meg a visszatekintéseket egy idővonal segítségével, amely automatikusan nyomon követi a legfontosabb eseményeket és üzeneteket, hogy a csapatok számára azonnal kéznél legyen.", - "hfrrC7": "Csapat kezdőbetűi", - "ijAUQf": "Értesítse a rendszergazdát a megemeléshez.", - "iNU1lj": "A futás amire hivatkozik privát vagy nem létezik.", - "kXFojL": "Létrehozhat forgatókönyvet az idő előrehaladtával is, szóval elérhető lesz, amikor épp szüksége lesz rá.", - "k9q07e": "Tegyen közzé frissítést másik csatornákba", - "kvgvNW": "Tudja meg mi történt", - "lxfpbh": "A tulajdonos {reminderEnabled, select, true {kérve lesz, hogy küldjön állapot frissítést minden} other {nem lesz kérve, hogy küldjön állapot frissítést}}", - "nqVby7": "{numTasksChecked, number} / {numTasks, number} feladat kiválasztva", - "ryrP8K": "Kezelje a jogosultságokat, hogy kinek legyen joga megtekinteni, módosítani és futtatni ezt a forgatókönyvet.", - "sVlNlY": "Minden csapat felépítése különböző. Kezelheti, hogy melyik felhasználónak legyen joga forgatókönyvet indítani.", - "scYyVv": "Szeretne kitölteni egy visszatekintő jelentést?", - "u4MwUB": "Forgatókönyv futási előzményének mentése", - "w0muFd": "Kimenő webhorog küldése (soronként egy)", - "wcWpGs": "Érvénytelen webhorog URL-ek", - "x8cvBr": "Futás áttekintő megtekintése", - "zx0myy": "Résztvevők", - "yxguVq": "Módosítások eldobása", - "yhU1et": "Feladatok", - "xmcVZ0": "Keresés", - "wsUmh9": "Csapat", - "wZ83YL": "Ne most", - "vir0m9": "Érvénytelen kategória név.", - "v8ZnNc": "Csapat kiválasztása", - "uny3Zy": "Forgatókönyvek", - "uBLF+D": "Mi egy forgatókönyv?", - "twieZh": "Ugrás a futás áttekintőbe", - "sqNmlF": "Visszatekintő kihagyása", - "sIX63S": "A rendszergazda értesítve lett", - "rDvvQs": "{completed, number} / {total, number} kész", - "qyJtWy": "Mutasson kevesebbet", - "q6f8x9": "Módosítások az utolsó frissítés óta", - "prYDT6": "Értesítési csatorna", - "pjt3qA": "Új feladatlista", - "nmpevl": "Figyelmen kívül hagyás", - "nkCCM2": "Nem lesz többet emlékeztetve.", - "lrbrjv": "Igen, visszatekintő indítása", - "lJyq2a": "Futás nem található", - "jIgqRa": "Tulajdonos / Résztvevők", - "j7jdWG": "Átalakítás üzleti kiadássá.", - "ZAJviT": "Nem sikerült értesíteni a rendszergazdát.", - "ZdWYcm": "Nem, visszatekintő kihagyása", - "dIwav9": "Biztosan törölni szeretné ezt a feladatot? El lesz távolítva ebből a futásból, de a forgatókönyvet nem fogja érinteni.", - "e/AZL5": "A 30 napos próbaidőszaka elkezdődött", - "fV6578": "Tulajdonosi jogkör hozzárendelése", - "fmylXu": "Indítsa el a forgatókönyvet ha egy felhasználó közzétesz egy üzenetet", - "g4IF1x": "Ennek a forgatókönyvnek nincsenek futásai.", - "gt6BhE": "Futás részletei", - "hVFgh4": "Beleértve a befejezetteket", - "fpuWL1": "Forgatókönyv törlése", - "fdQDz+": "A {title} forgatókönyv sikeresen törölve lett.", - "edxtzC": "Forgatókönyv létrehozása", - "bE1Cro": "Csak a saját futásaim", - "b/QBNs": "Frissítési határidő", - "aWpBzj": "Több mutatása", - "YORRGQ": "Frissítés közzététele", - "YKn+7s": "Ez a csatorna nem futtat egy forgatókönyvet sem.", - "Y+U8La": "Biztosan törölni szeretné a {title} forgatókönyvet?", - "WIxhrv": "A futás nevének legalább két karakter hosszúnak kell lennie", - "WAHCT2": "Rendszergazda értesítése", - "W1Qs5O": "Futások", - "V5TY0z": "Hozzáad résztvevőket?", - "AF9wda": "Ez a frissítés el lett mentve az áttekintő oldalra{hasBroadcast, select, true { és közzé lett téve {broadcastChannelCount, plural, =1 {egy csatornában} other {{broadcastChannelCount, number} csatornában}}} other {}}.", - "Qrl6bQ": "Könnyítse meg folyamatait forgatókönyvekkel", - "QVQrgH": "Miután megszüntette a saját hozzáférését ehhez a forgatókönyvhöz, nem fogja tudni visszarakni saját magát. Biztos benne, hogy el szeretné végezni ezt a műveletet?", - "R4vA+C": "Csak az alábbi felhasználók hozhatnak létre forgatókönyvet. Ezek a felhasználók - csak úgy mint a rendszergazdák - megváltoztathatják ezt a beállítást.", - "QUwMsX": "Emlékeztető, hogy legyen kitöltve a visszatekintő", - "GxJAK1": "A kért forgatókönyv privát vagy nem létezik.", - "KJu1sq": "Feladatlista eltávolítása", - "MDP9TS": "Eltávolítás a forgatókönyvről", - "Mm1Gse": "Keresés résztvevő után", - "OHfpS1": "Tartalmazza ezen kulcsszavak bármelyikét", - "Q7hMnp": "Forgatókönyv indítása", - "Q67RuY": "Összes futás megtekintése", - "Nh91Us": "{from, number}–{to, number} / {total, number} összesenből", - "M/2yY/": "Még senki sem.", - "Lg3I1b": "@{targetUsername}, kérem adjon meg állapot frissítést.", - "Leh2tk": "Kattintson ide a csapat összes futásának megtekintéséhez.", - "LVYPbG": "Tulajdonos hozzárendelése", - "JJMNME": "{withRunName, select, true {@{authorUsername} küldött egy frissítést a [{runName}]({overviewURL}) futásba} other {@{authorUsername} küldött egy frissítést}}", - "J1G4S4": "Még nincsennek forgatókönyvek meghatározva.", - "IwY/wg": "Egy forgatókönyv minden folyamat számára", - "HSi3uv": "Nincs hozzárendelve", - "GRTyvN": "Forgatókönyv lista kapcsolása", - "+hddg7": "Hozzáadás a futás idővonalához", - "/YZ/sw": "Próbaidőszak kezdése", - "2QkJ4s": "Mentse el a fontos üzeneteket, hogy teljes képet kapjon, ami megkönnyíti a visszatekintéseket.", - "2PNrBQ": "A forgatókönyv futás csatornájának kiexportálása későbbi elemzés céljából.", - "15jbT0": "Továbbiak hozzáadása az idővonalra", - "4ltHYh": "Ugrás a forgatókönyvre", - "5wqhGy": "Futás részleteinek kapcsolása", - "6n0XDG": "Biztosan el szeretné távolítani a feladat listát? Az összes feladat el lesz távolítva.", - "9tBhzB": "Emelje meg most", - "C9NScU": "Tegye meg a csapatát irányítónak", - "CkYhdY": "Csatorna hozzáadása az oldasáv kategóriákhoz", - "Cy1AK/": "Futás részleteinek megtekintése", - "DtCplA": "# résztvevő", - "GwtR3W": "Fogjon meg és dobja be egy meglévő feladatot vagy kattintson és hozzon létre egy újat.", - "DuRxjT": "Forgatókönyv létrehozása", - "DSVJjB": "Jelenleg a {playbookTitle} forgatókönyv fut", - "D55vrs": "A licensze nem generálható le", - "D2CE02": "Webhorog megadása", - "CyGaem": "Futás neve", - "C1khRR": "Vissza a forgatókönyvek alkalmazásba", - "BQtd5I": "Üdvözlet a Forgatókönyvek alkalmazásban!", - "Auj1ap": "Próbaidőszak kezdése vagy az előfizetés megemelése.", - "A8dbCS": "Forgatókönyv nem található", - "A21Mgv": "Futás befejeződött", - "9qc7BX": "Altatás", - "9TTfXU": "A rendszergazda értesítve lett.", - "9PXW6Q": "Időtartam / Kezdés ideje", - "91Hr5f": "Fogj meg az átrendezéshez", - "6uhSSw": "Csatorna kiválasztása", - "6CGo3o": "Állapot / Legutóbbi frissítés", - "5qBEKB": "Mik a forgatókönyv futások?", - "5j6GD/": "{numParticipants, plural, =0 {nincs résztvevő} other {# résztvevő}}", - "42qmJ5": "Önnek nincs jogosultsága egy frissítést beküldeni.", - "2VrVHu": "Keresés a futás neve alapján", - "2Qq4YX": "Biztosan el szereetné dobni a változtatásait?", - "0oLj/t": "Kibővítés", - "/MaJux": "Visszatekintő indítása", - "E0LnBo": "Válasszon az egyik lehetőségből vagy adjon meg egyéni időtartamot (\"2 weeks\", \"3 days 12 hours\", \"45 minutes\", ...)", - "l7zMH6": "Válasszon az egyik lehetőségből vagy adjon meg egyéni időtartamot", - "dvhvum": "(Nem kötelező) Magyarázza el hogyan kell ezt a forgatókönyvet használni", - "IOnm/Z": "Nincs elérhető futás összefoglaló.", - "IfxUgC": "Futás összefoglaló megadása…", - "djALPR": "{activeRuns, number} futás van folyamatban", - "l0hFoB": "Forgatókönyv leírás megadása...", - "wX3k9U": "Névtelen forgatókönyv", - "eLeFE2": "Név és leírás szerkesztése", - "YMrTRm": "Futás összefoglaló", - "Oo5sdB": "Forgatókönyv neve", - "ZWtlyd": "Futás helyreállítva {name} által", - "36GNZj": "A {title} forgatókönyv sikeresen archiválódott.", - "xvBDOH": "Biztosan archiválni szeretné a {title} forgatókönyvet?", - "sDKojV": "Forgatókönyv archiválása", - "hrgo+E": "Archív", - "EQpfkS": "Befejeződött", - "0HT+Ib": "Archivált", - "XXbWAU": "Jelölje ki ezt, ha szeretne automatikusan értesítéseket kapni amikor ez a forgatókönyv fut.", - "+Tmpup": "Ön automatikusan értesítéseket kap amikor ez a forgatókönyv fut.", - "h+e7G+": "Kérdezze meg, hogy futtassa-e ezt a forgatókönyvet amikor az üzenet {numKeywords, select, 1 {tartalmazza a kulcsszót} other {tartalmaz egy vagy többet ezek közül}}", - "kDcpd/": "{numKeywords, plural, other {# kulcsszó}}", - "zz6ObK": "Helyreállítás", - "ypIsVG": "Feladat helyreállítása", - "wO6NOM": "Biztosan helyre kívánja állítani ezt a feladatot? Ez a feladat hozzá lesz adva ehhez a futáshoz", - "dSC1YD": "Feladat kihagyása", - "7VTSeD": "Biztosan kihagyja ezt a feladatot? Ugyan a mostani futásból ki lesz vágva, de a forgatókönyvet nem fogja befolyásolni.", - "/4tOwT": "Kihagyás", - "Vhnd2J": "Leírás kapcsolása", - "vjzpnC": "Nincs a keresése feltételeknek megfelelő forgatókönyv.", - "z3B83t": "Forgatókönyv keresése", - "fuDLDJ": "Csatorna létrehozása", - "UMoxP9": "Csatorna név sablon (nem kötelező)", - "RO+BaS": "Futás linkjének másolása", - "NA7Cw1": "Forgatóköny linkjének másolása", - "3MSGcL": "A csatorna neve érvénytelen.", - "0oL1zz": "Másolva!", - "cp7KUI": "Forgatókönyv", - "C6Oghd": "Futás összefoglalójának szerkesztése", - "cPIKU2": "Követed", - "d4g2r8": "Törölve: {timestamp}", - "O8o2lE": "Csatorna hozzáadása kategóriához", - "Mu2aDs": "Mindenkinek a csapatban ({team}) van hozzáférése.", - "iXNbPf": "Átnevezés", - "2/2yg+": "Hozzáadás", - "vaYTD+": "Van {outstanding, plural, =1 {egy} other {#}} megoldatlan feladat. Biztos, hogy be akarja fejezni a futást?", - "q0cpUe": "Ellenőrzőlista létrehozása", - "nSFBC2": "+ Új feladat", - "m/Q4ye": "Ellenőrzőlista átnevezése", - "k1djnL": "Ellenőrzőlista törlése", - "X2K92H": "Ellenőrzőlista neve", - "WbsomC": "Visszatekintő közzététele", - "TxCTXQ": "Biztosan be szeretné fejezni a futást?", - "QywYDe": "És a futás megjelölése befejezettként", - "MrJPOh": "Állapot frissítés engedélyezése", - "Ja1sVR": "Állapot frissítés letiétásra került ehhez a forgatókönyv futáshoz.", - "I5NMJ8": "Továbbiak", - "D9IV7i": "Visszatekintő letiltásra került ehhez a forgatókönyv futáshoz.", - "D/wCS9": "Biztosan szeretné közzétenni a visszatekintőt?", - "5Ofkag": "Visszatekintő engedélyezése", - "4vuNrq": "{duration} a futás megkezdése után", - "2563nT": "Futás befejezésének jóváhagyása", - "/gbqA6": "{duration} a futás megkezdése előtt", - "/ZsEUy": "Biztos, hogy törölni szeretné ezt az ellenőrzőlistát? Eltávolításra kerül ebből a futtatásból, de nem befolyásolja a forgatókönyvet.", - "JqKASQ": "@{displayName} hozzáadása a Csatornához", - "iDMOiz": "CSATORNA TAGJAI", - "5ciuDD": "NINCS A CSATORNÁBAN", - "pK6+CW": "@{displayName} nem taga a [{runName}]({overviewUrl}) csatornának. Szeretné hozzáadni a csatornához? Hozzáférése lesz az összes korábbi üzenethez.", - "Lo10yH": "Ismeretlen csatorna", - "osuP6z": "Fogja meg az ellenőrzőlista átrendezéséhez", - "SXJ98n": "A visszatekintő jelentést a közzétételt követően nem tudja szerkeszteni. Szeretné közzétenni a visszatekintő jelentést?", - "g0mp+I": "Ha privát forgatókönyvvé konvertálja, a tagság és a futtatási előzmények megmaradnak. Ez a változás végleges, és nem vonható vissza. Biztos benne, hogy a {playbookTitle} forgatókönyvet priváttá szeretné alakítani?", - "tVPYMu": "Forgatókönyv admin", - "ruJGqS": "Forgatókönyv hozzáférés", - "o+ZEL3": "Közzétett {timestamp}", - "5BUxvl": "Mindenki ebben a csapatban megtekintheti ezt a forgatókönyvet.", - "0Vvpht": "Legyen forgatókönyv tag", - "EvBQLq": "Legyen forgatókönyv admin", - "0tznw6": "Átalakítás privát forgatókönyvvé", - "3Ls2m+": "Forgatókönyv tag", - "wylJpv": "Mindenki a {team} csapatban megtekintheti ezt a forgatókönyvet.", - "lQT7iD": "Forgatókönyv létrehozása", - "gGcNUr": "Önnek nincs jogosultsága", - "QpUBDr": "{members, plural, =0 {Senki sem} =1 {Egy személy} other {# személy}} férhet hozzá ehhez a forgatókönyvhöz.", - "R/2lqw": "Sablon kiválasztása", - "MJ89uW": "Átalakítás privát forgatókönyvvé", - "HLn43R": "Hozzáférés kezelése", - "EWz2w5": "Forgatókönyv indítása", - "8oCVbz": "Biztosan publikálni szeretné", - "qsr3Zk": "Futás összefoglalójának frissítése", - "0q+hj2": "Hozzon létre egy sablont egy tömör leíráshoz, amely minden egyes futást elmagyaráz az érdekeltek számára.", - "FXCLuZ": "{total, number} összesen", - "3PoGhY": "Biztosan közzé szeretné tenni?", - "/urtZ8": "Ön forgatókönyvei", - "4alprY": "Forgatókönyv sablonok", - "4fHiNl": "Duplikálás", - "SVwJTM": "Exportálás", - "9XUYQt": "Importálás", - "4aupaG": "A {title} forgatókönyv vissza lett állítva.", - "4cwL43": "Archiváltakkal együtt", - "MTzF3S": "Biztos, hogy vissza szeretné állítani a {title} forgatókönyvet?", - "bTgMQ2": "Ez a forgatókönyv archivált.", - "lBqu4h": "Forgatókönyv visszaállítása", - "0Xt1ea": "Továbbra is hozzáférhet ennek a mérőszámnak a múltbeli adataihoz.", - "gsMPAS": "Dollár", - "y7o4Rn": "Biztos benne, hogy törölni szeretné?", - "uT4ebt": "pl. Erőforrások száma, Érintett ügyfelek száma", - "tbjmvS": "Egy azonos nevű mérőszám már létezik. Kérjük, adjon egyedi nevet minden egyes mérőszámhoz.", - "rzbYbE": "Cél", - "rMhrJH": "Kérem adjon meg címet a mérőszámának.", - "q/Qo8l": "A privát forgatókönyvek csak a Mattermost Enterprise rendszerben érhetőek el", - "mbo96h": "Egyéni mérőszámok konfigurálása a visszatekintő jelentés kitöltéséhez", - "mVpO8u": "Látta már ezt korábban?", - "9SIW2x": "Célérték minden egyes futáshoz", - "XpDetT": "Hagyja ki ezeket a tippeket.", - "TxmjKI": "Írja le, miről szól ez a mérőszám", - "f+bqgK": "Mérőszám neve", - "a0hBZ0": "Mérőszám törlése", - "VZRWFk": "pl. költségek, beszerzések", - "Sx3lHL": "Szám", - "OyZnsJ": "futásonként", - "NYTGIb": "Vettem", - "NJ9uPu": "Kulcs mérőszámok", - "LI7YlB": "Adjon hozzá részleteket arról, hogy miről szól ez a mérőszám, és hogyan kell kitölteni. Ez a leírás minden egyes futás visszatekintő oldalán elérhető lesz, ahol az ilyen mérőszámok értékeit kell megadni.", - "LDYFkN": "Időtartam (dd:hh:mm)", - "JrZ2th": "Mérőszám hozzáadása", - "FGzxgY": "pl.: Idő a tudomásulvételre, Idő a megoldásra", - "F4pfM/": "Kérjük, adjon meg egy számot, vagy hagyja üresen a célt.", - "6D6ffM": "Adja meg az időtartamot a következő formátumban: dd:hh:mm (pl. 12:00:00), vagy hagyja üresen a célt.", - "4BN53Q": "Megmutatjuk, hogy az egyes futások értéke milyen közel vagy távol van a céltól, és grafikonon is ábrázoljuk.", - "1ikfp3": "Ha törli ezt a mérőszámot, az értékek a jövőbeni futtatások során nem lesznek összegyűjtve.", - "wPVxBN": "Kattintson a szerkesztésre, hogy elkezdje testreszabni és saját modelljeihez és folyamataihoz igazítani. Ezen az oldalon részletesen felfedezheti a sablont.", - "vQqT/8": "Válassza a szerkesztés lehetőséget, hogy elkezdje testreszabni és saját modelljeihez és folyamataihoz igazítani. Ezen az oldalon részletesen felfedezheti a sablont.", - "Pue+oV": "Futtassa a forgatókönyvet, hogy megnézze működés közben", - "6GTzTR": "Bármikor megnézheti, hogy mi van ebben a forgatókönyvben", - "0EEIkR": "Gratulálunk! Létrehozta első forgatókönyvét egy sablon segítségével!", - "/fU9y/": "Ezen az oldalon részletesen megnézheti a forgatókönyv különböző részeit.", - "RzEVnf": "A forgatókönyvek segítségével a fontos folyamatok megismételhetőbbé és elszámoltathatóbbá válnak. Egy forgatókönyv többször is lefuttatható, és minden egyes futtatáshoz saját nyilvántartás és visszatekintés tartozik.", - "Tt04f1": "Nézze meg, ki érintett, és mit kell tenni anélkül, hogy kilépne a beszélgetésből.", - "ZkhArX": "Gyerünk!", - "cEWBE3": "Értékelje folyamatait visszatekintéssel, hogy minden egyes futtatással finomíthassa és javíthassa azokat.", - "dZmYk6": "Sikeresen duplikálta a forgatókönyveket", - "dxyZg3": "Hadd fedezzem fel magamnak", - "fhMaTZ": "Nézzen meg egy gyors bemutatót", - "lgZf0l": "Ismerkedjen meg a Forgatókönyvekkel", - "q/VD+s": "Állítson be időzítőket és állítson össze egy sablont az állapotfrissítésekhez, hogy az érdekeltek mindig naprakészek legyenek a fejleményekkel kapcsolatban.", - "vJ2SaW": "Automatizálhatja a forgatókönyv egyes aspektusait, például az üdvözlő üzenet elküldését, a kulcsfontosságú tagok meghívását és a frissítési csatorna létrehozását.", - "vL4++D": "Kövesse a haladást és a kiosztást", - "wbdGb5": "Osszon ki, pipáljon ki vagy hagyjon ki feladatokat, hogy a csapat tisztában legyen azzal, hogyan kell együtt haladni a cél felé.", - "R5Zh+l": "Ez lehetővé teszi, hogy először megtapasztaljon egy minta forgatókönyvet, mielőtt időt fektetne a sajátja létrehozásába.", - "Q3R9Uj": "A teljes folyamat lépéseinek dokumentálása itt. Minden feladatot rendeljen hozzá felelős személyekhez, és lehetőség szerint adjon hozzá határidőket vagy kapcsolódó műveleteket.", - "QbGfqo": "Küldje el az érdekelt feleknek több helyre, és tartsa meg a visszatekintéshez szükséges nyomvonalat egyetlen poszttal.", - "Q5hysF": "Valósítson meg többet a forgatókönyvek segítségével", - "HXvk56": "Állapotfrissítések közzététele", - "I5DYM+": "Tanuljon ÉS gondolkodjon", - "HGdWwZ": "Feladatok létrehozása és kiosztása", - "GG1yhI": "Számos felhasználási esethez és eseményhez vannak sablonok. Használhatja a forgatókönyvet úgy, ahogy van, vagy testre szabhatja — majd megoszthatja a csapatával.", - "GAuN6w": "Feltételezések felállítása", - "9m0I/B": "Az érdekelt felek naprakész tájékoztatása", - "8n24G2": "Futás részleteinek megjelenítése az oldalsávon", - "1isgPF": "Automatikusan létrehoztuk az első futásodat", - "1QosTr": "Használja", - "GjCS6U": "Válasszon egy sablont", - "udrLSP": "Használjon mérőszámokat a minták és az előrehaladás megértéséhez a futások során, és kövesse nyomon a teljesítményt.", - "lUfDe1": "Exportálja a forgatókönyv futtatási csatornáját, és mentse el későbbi elemzéshez.", - "hw83pa": "A kulcsfontosságú mérőszámok és értékek nyomon követése", - "69nlA3": "Kérjük, adja meg az időtartamot a következő formátumban: dd:hh:mm (pl. 12:00:00).", - "KXVV4+": "Üdvözöljük a forgatókönyv áttekintő oldalán!", - "NLeFGn": "-", - "l5/RKZ": "Ehhez a forgatókönyvhöz nincsenek befejezett futások.", - "lbs7UO": "futásonként az elmúlt 10 futás során", - "mvZUm3": "Itt részletesen felfedezheti a forgatókönyv összetevőit. Válassza a Szerkesztés lehetőséget, hogy a folyamataihoz és modelljeihez igazítsa a forgatókönyvet.", - "xVyHgP": "Próba futás indítása", - "ru+JCk": "Átlagos érték", - "fmbSyg": "Érték hozzáadása (formátum: dd:hh:mm)", - "efeNi1": "10-futásonkénti átlagos érték", - "awG90C": "Futásonkénti cél", - "ZNNjWw": "Kérjük, adjon meg egy számot.", - "Vf/QlZ": "Érték tartomány", - "NiAH1z": "Cél érték", - "NMxVd+": "Kérjük, adja meg a mért értéket.", - "M4gAc9": "Érték hozzáadása", - "9a9+ww": "Cím", - "MBNMo9": "Csatorna műveletek", - "B3Q5mz": "Aktiválás", - "DPj6DM": "Válassza ki az Indítás lehetőséget, hogy megnézze azt működés közben.", - "Y4MU/9": "Válassza ki a Próba futás indítása lehetőséget, hogy megnézze azt működés közben.", - "p1I/Fx": "Automatikusan létrehoztuk Önnek a futását", - "u7qh13": "Készen áll forgatókönyvének a futtatására?", - "c23IHq": "A csatorna akciók lehetővé teszik a tevékenységek automatizálását ezen a csatornán", - "ao44YC": "Mérőszámok beállítása", - "RUlvbf": "Próbálja ki az új forgatókönyvét!", - "MHzP9I": "Adjon meg egy üzenetet a csatornához csatlakozó felhasználók üdvözlésére.", - "5AJmOz": "Amikor egy felhasználó csatlakozik a csatornához", - "0RlzlZ": "Küldjön egy ideiglenes üdvözlő üzenetet a felhasználónak", - "Ob5cSv": "Az elvégzett módosítások nem kerülnek mentésre, ha elhagyja ezt az oldalt. Biztos, hogy el akarja vetni a változtatásokat és távozni szeretne?", - "e3z3P8": "Eldobás és kilépés", - "u4L4yd": "Önnek van elmentetlen módosítása", - "Ek1Fx2": "Amikor egy ilyen kulcsszavakkal ellátott üzenet kerül kiküldésre", - "+/x2FM": "Válasszon ki egy forgatókönyvet", - "dCtjdj": "Készen áll a forgatókönyve futtatásához?", - "9j5KzL": "Adja meg a kategória nevét", - "MbapTE": "{num} feladat lejárt", - "Z3ybv/": "A csatorna hozzáadása egy oldalsáv kategóriához a felhasználó számára", - "zWgbGg": "Ma", - "mLrh+0": "Nincs határidő", - "iMjjOH": "Következő hét", - "Ppx673": "Jelentések", - "MtrTNy": "Holnap", - "AF7+5o": "Határidő hozzáadása", - "UlJJ1i": "Perjel parancs hozzáadása", - "W0aij2": "Hozzárendelés...", - "mw9jVA": "Cím megadása", - "lyXljU": "Feladat duplikálása", - "371AC3": "Futás összefoglalójának frissítése", - "TTIQ6E": "Jelöljön ki határidőket a feladatokhoz, hogy a kijelöltek priorizálni és elvégezni tudják a feladatokat.", - "aEhjYg": "Áttekintés", - "oBeKB4": "Határidő {date}", - "oAJsne": "Nyilvános forgatókönyv", - "mm5vL8": "Csak meghívott tagok", - "lkv547": "Határidő (Elérhető a Professional előfizetésben)", - "lglICE": "Leírás megadása(nem kötelező)", - "lJ48wN": "Privát forgatókönyv", - "g9pEhE": "Határidő", - "Xgxruo": "Ellenőrző lista kihagyása", - "RQl8IW": "Altatás eddig…", - "NFyWnZ": "Még hatékonyabb munkavégzés", - "I7+d55": "Adja meg a dátumot/időt (\"4 órán belül\", \"Május 1.\" ...)", - "9trZXa": "A csapatból bárki megtekintheti", - "7P5T3W": "Ellenőrző lista visszaállítása", - "2Q5PhZ": "Kérdezzen forgatókönyv futtatás előtt", - "OqCzNb": "Feladat hozzáadása", - "JcefuP": "Leírás hozzáadása (nem kötelező)", - "mCrdeS": "Összes forgatókönyv futások", - "v5/Cox": "Ellenőrző lista duplikálása", - "CwwzAU": "Ellenőrző lista nevének megadása", - "IxtSML": "Ellenőrző lista hozzáadása", - "4GjZsL": "Összes forgatókönyv", - "XF8rrh": "Link másolása ''{name}'' számára", - "cyR7Kh": "Vissza", - "MyIJbr": "Tartalom", - "5ZIN3u": "Állapot frissítések", - "k12r+v": "Futás összefoglaló sablon hozzáadása...", - "RrCui3": "Összefoglaló", - "+PMJAg": "Kezdje meg a követést {followers, plural, =1 {egy} other {#}} felhasználó számára", - "x1phlu": "Nincs időablak", - "kYCbJE": "Idő ablak megadása", - "xHNF7i": "Műveletek futtatása", - "/RnCQb": "Kimenő webhorog küldése", - "28FTjr": "Műveletek futtatása lehetővé teszi a tevékenységek automatizálását ezen a csatornán", - "j940pJ": "Ez a frissítés az áttekintő oldalra lesz elmentve.", - "mkLeuq": "Frissítés közzététele a kiválasztott csatornákban", - "uhDKO8": "Használjon markdown formázást sablon létrehozásához", - "sX5Mn5": "Kérem soronként egy webhorgot adjon meg", - "kV5GkX": "Amikor egy állapotfrissítés rögzítésre kerül", - "aM44Z/": "Válasszon ki vagy adjon meg egy egyéni időtartamot…", - "YQOmSf": "Adjon meg soronként egy webhorgot", - "XRyRzf": "Állapotfrissítések nem várhatóak.", - "HvAcYh": "{text}{rest, plural, =0 {} one { és más} other { és {rest} más}}", - "DaHpK1": "Csatorna keresése", - "F9LrJA": "Tételek szűrése", - "OuZhcQ": "Adja meg az időtartamot (\"8 óra\", \"3 nap\"...)", - "OKhRC6": "Megosztás", - "9kQNdp": "Ez a forgatókönyv privát.", - "TD8WrM": "Duplikálás le van tiltva ennél a csapatnál.", - "LcC/pi": "Küldjön üdvözlő üzenetet…", - "xEQYo5": "A visszatekintő jelentés egyedi mérőszámainak beállítása.", - "yllba1": "Ez az archivált forgatókönyv nem nevezhető át.", - "zl6378": "Visszatekintő mérőszámainak beállítása", - "vSMfYU": "Futás infó", - "oL7YsP": "Utoljára módosítva {timestamp}", - "aZGAOI": "Állapot frissítő sablon hozzáadása…", - "Z2Hfu4": "Futás összefoglaló hozzáadása", - "Brya9X": "Futás összefoglaló sablon hozzáadása…", - "3hBelc": "Nem várt visszatekintő.", - "OQplDX": "Állapotfrissítések közzétételének várható időszaka: . Az új frissítések {channelCount, plural, =0 {egy csatornán sem} other {# csatornán}} és {webhookCount, plural, =0 {egy kimenő webhorgon sem} other {# kimenő webhorgon}} lesznek közzétéve.", - "iigkp8": "Ideje befejezni?", - "opn6uf": "Idővonal megtekintése", - "o6N9pU": "Műveletek futtatása", - "lbr3Lq": "Link másolása", - "ZJS10z": "Még nem lett frissítés közzétéve", - "hjteuA": "Az összes elérhető forgatókönyv itt fog megjelenni", - "bf5rs0": "Információk megtekintése", - "Q15rLN": "Frissítés kérése...", - "GDCpPr": "Legújabb állapot frissítés", - "+qDKgW": "Összes frissítés megtekintése", - "kkw4kS": "Ez a frissítés közzé lesz téve {hasChannels, select, true {{broadcastChannelCount, plural, =1 {egy csatornában} other {{broadcastChannelCount, number} csatornában}}} other {}}{hasFollowersAndChannels, select, true { és } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {egy közvetlen üzenetben} other {{followersChannelCount, number} közvetlen üzenetben}}} other {}}.", - "kEMvwX": "Nincs a szűrésnek megfelelő futás.", - "GXjP8g": "Minden futás, amelyhez hozzáférhet, itt fog megjelenni", - "m/KtHt": "Nincs jogosultsága a tulajdonos megváltoztatásához", - "nc8QpJ": "Legutóbbi tevékenységek", - "ocYb9S": "Kulcs mérőszámok", - "lr1CUA": "Forgatókönyvek áttekintése", - "Ul0aFX": "Forgatókönyv importálása", - "RnOiCg": "Nem volt lehetséges, hogy {isFollowing, select, true {ne kövesse} other {kövesse}} a futást", - "CFysvS": "Forgatókönyv legördülő létrehozása", - "/qDObA": "Futások áttekintése", - "LfhTNW": "Forgatókönyvek és futások áttekintése és létrehozása", - "GVpA4Q": "Új forgatókönyv létrehozása", - "4mCpAv": "Nem volt lehetséges a tulajdonos megváltoztatása", - "/+8SGX": "Megjelenítve {filteredNum} / {totalNum} eseményt", - "VpQKQE": "{displayName} nem résztvevője a futásnak. Szeretné őket is résztvevővé tenni? Hozzáférésük lesz a futó csatorna összes üzenet előzményéhez.", - "jboo9u": "Frissítés kérése", - "Xx0WZV": "Üzenet küldése", - "UePrSL": "{num} résztvevő", - "UMFnWV": "Visszatekintő megtekintése", - "RCT0Px": "{displayName} hozzáadása a Csatornához", - "P9PKvb": "Üzenet lett küldve a futó csatornára.", - "NGqzDU": "Frissítés kérésének jóváhagyása", - "JvEwg/": "Nem volt lehetséges egy frissítést kérni", - "Jli9m7": "A rendszer üzenetet küld a futó csatornának, amelyben kéri, hogy tegyen közzé egy frissítést.", - "9xs0pp": "Érték hozzáadása...", - "KeO51o": "Csatorna", - "lKeJ+i": "Nincsen összefoglaló", - "pFK6bJ": "Összes megtekintése", - "u6Fyic": "A kérését elküldtük a futás csatornájára.", - "pzTOmv": "Követők", - "P6NEL/": "Parancs...", - "1GOpgL": "Felelős...", - "1fXVVz": "Határidő...", - "J2NmIY": "Részvétel megerősítése", - "U8u4uF": "Vegyen részt", - "zW/5AB": "Professional funkció Ez egy fizetős funkció, amely 30 napos ingyenes próbaverzióval érhető el.", - "ch4Vs1": "Egyetlen kattintással kérhet frissítéseket a forgatókönyv futásokhoz, és kapjon közvetlen értesítést, ha frissítés érkezik. Indítson ingyenes, 30 napos próbaverziót a kipróbáláshoz.", - "pXWclp": "A résztvevői kérése el lett küldve a futás csatornájának.", - "vDvWJ6": "Próbálja ki a futás frissítés kérést egy ingyenes próbával", - "PdRg+3": "Összes megtekintése...", - "Nf9oAA": "Ön csatlakozni készül ehhez a futáshoz.", - "5PpBsd": "A kérése nem volt sikeres.", - "4Iqlfe": "Csatlakozott ehhez a futáshoz.", - "SMrXWc": "Kedvencek", - "PW+sL4": "N/A", - "KzHQCQ": "Nem található ezeknek a szűrőknek megfelelő befejeződött futás.", - "5HXkY/": "Típus: {typeTitle}", - "AH+V3r": "Legyen a futás részvevője.", - "b+DwLA": "Kérés a futásban való részvételre.", - "qp5G0Z": "A visszatekintő funkciók eléréséhez előfizetés váltása szükséges.", - "ojQue/": "{icon} Időtartam (dd:hh:mm formában)", - "wGp7l3": "{icon} Dollár", - "wRM2AO": "A frissítés kérése nem sikerült.", - "xfnuXm": "Részvétel", - "s+rSpl": "{icon} Szám", - "mNgqXf": "Ahhoz hogy feloldja ezt a szolgáltatást:", - "j2VYGA": "Összes forgatókönyv megjelenítése", - "ePhhuK": "A kérését elküldtük a futás csatornájára.", - "PoX2HN": "Kérés küldése", - "PWmZrW": "Összes futás megjelenítése", - "OfN7IN": "A futás csatornájára állapotfrissítési kérés lesz elküldve.", - "Gwmqz5": "Frissítés kérése", - "CV1ddt": "Részvétel a futásban", - "CUhlqp": "bemutató körút tipp termékkép", - "B9z0uZ": "A futáshoz való csatlakozási kérelme sikertelen volt.", - "+6DCr9": "Résztvevőként állapotfrissítéseket tehet közzé, feladatokat oszthat ki és végezhet el, valamint visszatekintéseket végezhet.", - "3zF589": "Visszaállítás a {filterName} szűrőre", - "gfUBRi": "Rendeljen hozzá másvalakit mielőtt kilép a futásból.", - "wBZz47": "Ön kilépett a futásból.", - "fnihsY": "Kilépés", - "a1vQ5Q": "Kilépés jóváhagyása", - "SK5APX": "Nem volt lehetséges a futásból kilépni.", - "N9CTUJ": "Kilépés a futásból", - "F/HKIy": "Biztosan ki szeretne lépni a futásból?", - "Mjq//Y": "Kedvenc visszavonása", - "5Hzwqs": "Kedvenc", - "XS4umx": "{name} elaltatott egy állapot frissítést", - "QegBKq": "Csatlakozás a forgatókönyvhöz", - "Xm0L7N": "Amikor egy állapotfrissítés, vagy egy visszatekintő közzé lesz téve", - "Suyx6A": "A forgatókönyv importálása nem sikerült. Kérjük, ellenőrizze, hogy a JSON helyes-e, és próbálja meg újra.", - "cnfVhV": "Kilépés a futásból{isFollowing, select, true { és követés kikapcsolása} other {}}", - "egUE/K": "Közzététel a kiválasztott csatornákban", - "AhY0vJ": "Kilépés és követés kikapcsolása", - "Q4sutg": "Kilépés jóváhagyása{isFollowing, select, true { és követés kikapcsolása} other {}}", - "P6PLpi": "Csatlakozás", - "FgydNe": "Megtekintés", - "iEtImk": "Amikor kilép egy futásból{isFollowing, select, true { és kikapcsolja a követést}}, az el lesz távolítva a bal oldali oldalsávból. Újra megtalálhatja, ha megtekinti az összes futást.", - "j2FnDV": "Egy csatorna lesz létrehozva ezzel a névvel", - "qGlwfc": "Futás indítása", - "iQhFxR": "Legutóbb használt", - "03oqA2": "Aktív futások", - "vqmRBs": "Futás újraindításán", - "Zg0obP": "Futás újraindítása", - "KjNfA8": "Érvénytelen idő intervallum", - "k5EChD": "Biztos benne, hogy szeretné újraindítani a futást?", - "0QD99o": "Csatornához csatlakozás kérése", - "XnICdK": "Nem volt lehetséges a futáshoz csatlakozni", - "unwVil": "A csatornához csatlakozási kérés nem volt sikeres.", - "ZRv7Dm": "Csatlakozás kérése", - "M9tXoZ": "Egy csatlakozási kérés el lett küldve a futás csatornájára.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# késésben}}", - "BiQjuS": "A futás átkerült a {channel} csatornába", - "BJNrYQ": "Résztvevőként frissítheti a futás összefoglalóját, ellenőrizheti a feladatokat, állapotfrissítéseket tehet közzé, és szerkesztheti a visszatekintést.", - "AoNLta": "Ehhez a csatornához nincsenek befejezett futások", - "AG7PKJ": "Futás átnevezése", - "9w0mDI": "Előre kijelölt tag eltávolításának megerősítése", - "9qqGGd": "Résztvevők meghívása", - "9X3jwi": "{icon} Költség", - "9AQ5FE": "Futás összefoglaló", - "95v+5O": "{actions, plural, =0 {Feladat műveletek} other {# művelet}}", - "7KMbBa": "Soha nem használták", - "6rygzu": "Eltávolítás a futásból", - "5b1zuB": "Hozzáadás a futási csatornához", - "3sXVwy": "Feladat műveletek...", - "3Yvt4d": "A forgatókönyvek olyan konfigurálható ellenőrző listák, amelyek egy megismételhető folyamatot határoznak meg a csapatok számára, hogy konkrét és kiszámítható eredményeket érjenek el", - "36NwLv": "A futás résztvevőinek listájának kezelése", - "2NDgJq": "Utolsó állapotfrissítés", - "2BCWLD": "Csatorna beállítása", - "1prgB2": "Személyek keresése", - "1OluNs": "Állapotfrissítések engedélyezésének megerősítése", - "0CeyUV": "Nincs találat a \"{searchTerm}\" kereséshez", - "0Azlrb": "Kezelés", - "/GCoTA": "Törlés", - "//o1Nu": "Frissítések letiltása" -} diff --git a/webapp/playbooks/i18n/ja.json b/webapp/playbooks/i18n/ja.json deleted file mode 100644 index 7cb59fe4797..00000000000 --- a/webapp/playbooks/i18n/ja.json +++ /dev/null @@ -1,836 +0,0 @@ -{ - "zINlao": "オーナー", - "z5RMPO": "このプレイブックにアクセスできるのは、あなただけです", - "wbwhbH": "タスク名", - "waVyVY": "現在活動中の参加者", - "wEQDC6": "編集する", - "v3+TmO": "{members, plural, =0 {0人} =1 {1人} other {#人}} がこのプレイブックにアクセスできます", - "uJ3bRR": "このテンプレートは、各実行の内容をステークホルダーに説明するために記述される簡潔な説明文のフォーマットを標準化するのに役立ちます。", - "recCg9": "更新", - "rX08cW": "今日以降の日付でなくてはなりません。", - "lbhO3D": "斜体", - "jvo0vs": "保存", - "jXT2++": "チャンネルへ行く", - "hzt6l8": "Markdownを使ってテンプレートを作成します。", - "hXIYHG": "チャネルのエクスポートをサポートするChannel Exportプラグインをインストールして有効にする", - "gy/Kkr": "(編集済)", - "eiPBw7": "レトロスペクティブのリマインド間隔", - "ebkl6I": "このチームの全員がこのプレイブックにアクセスできる", - "eHAvFf": "太字", - "dcV/DJ": "{timestamp}", - "d9epHh": "チャンネルログをエクスポートする", - "bPLen5": "過去30日間に終了した実行", - "bLK+Kr": "指定された間隔で、レトロスペクティブを記入するようチャンネルにリマインドを投稿します。", - "b40Pr7": "報告者", - "avPeEI": "アップグレードすると、このPlaybookの総実行回数、アクティブな実行回数、実行に参加した参加者の傾向を閲覧できるようになります。", - "YDuW/T": "{num_runs, plural, =0 {まだ実行されていません} one {実行数: #} other {全実行数: #}}", - "XmUdvV": "必要なすべての統計情報", - "VmpFFw": "利用可能な説明がありません。", - "TZYiF/": "取消線", - "TJo5E6": "プレビュー", - "T7Ry38": "メッセージ", - "T5rX+W": "どのくらいの間隔で進捗を投稿すべきですか?", - "SENRqu": "ヘルプ", - "RthEJt": "レトロスペクティブ", - "R+JQaJ": "チャンネルのメンバー", - "QnZAit": "任意の説明を追加する", - "QiKcO7": "レトロスペクティブのテンプレートを入力する", - "Q8Qw5B": "説明", - "Q7aZO4": "{numParticipants, plural, =0 {アクティブな参加者はいません} =1 {アクティブな参加者数 :#} other {アクティブな参加者数: #}}", - "ObmjTB": "スラッシュコマンド", - "JCGvY/": "このテンプレートは、各作業で行われる定期的な進捗報告のフォーマットを標準化するのに役立ちます。", - "IuFETn": "期間", - "ICqy9/": "チェックリスト", - "HhLp57": "引用", - "EC5MJD": "利用可能なアップデートはありません。", - "DCl7Vv": "インラインコード", - "BD66u6": "チャネルからの全メッセージを含むCSVをダウンロードする", - "AS5kar": "参加者({participants})", - "A3ptul": "テンプレート", - "9uOFF3": "概要", - "6Lwe7T": "チームの全員がこのプレイブックにアクセスできます", - "5FRgqE": "チャンネルログのダウンロード", - "5A46pW": "スラッシュコマンドを追加する", - "47FYwb": "キャンセル", - "3rCdDw": "ステータス更新", - "1I48bs": "レトロスペクティブのテンプレート", - "/jUtaM": "過去14日間の1日あたりのアクティブな実行数", - "/1FEJW": "過去14日間の1日あたりのアクティブな参加者数", - "+8G9qr": "レトロスペクティブのデフォルトテキストです。", - "+ZIXOR": "チャンネルアクセス", - "djXM+y": "選択されたユーザーのみがアクセスできます。", - "b5FaCc": "チャンネルをサイドバーのカテゴリーに追加する", - "X3DLGJ": "このワークスペースにいる全員がプレイブックを作成できます。", - "Ui6GK/": "新しいメンバーがチャンネルに加わったとき", - "TSSNg/": "過去12週間の1週間あたりの実行回数", - "SFuk1v": "権限", - "OsDomv": "すべてのイベント", - "MvEydR": "{name} がステータスの更新を投稿しました", - "KiXNvz": "実行", - "DnBhRg": "ユーザーを追加する", - "DXACD6": "レトロスペクティブレポートの発行とタイムラインへのアクセス", - "D3idYv": "設定", - "CjNrqO": "レトロスペクティブレポートのテンプレート", - "CL5OZP": "選択したユーザーのみ、このプレイブックを編集または実行することができます。", - "AT2QBo": "選択されたユーザーのみプレイブックスを作成できます。", - "AML4RW": "タスクの割り当て", - "9Obw6C": "フィルター", - "8hDbW6": "外向きのウェブフックの送信", - "5Ot7cd": "このプレイブックで作成するチャネルのタイプを決定します。", - "4Hrh5B": "{name} は{summary}からステータスを変更しました", - "ArpdYl": "タイムラインのイベントは、発生するたびにここに表示されます。イベントを削除するには、イベント上にカーソルを合わせてください。", - "+QgvjN": "オーナーの役割を割り当てる", - "LmhSmU": "入力内容の削除の確認", - "KUr+sG": "実行概要を更新する", - "OcpRSQ": "エントリを削除する", - "bGhCLX": "アップデートが投稿されたとき", - "aACJNp": "{name} によって実行されました", - "TyrY2b": "プレイブック作成", - "TvihSy": "再掲載", - "SDSqfA": "実行を開始した時", - "c8hxKk": "{date}の週", - "DSVJjB": "現在、{playbookTitle}のプレイブックを実行しています", - "yqpcOa": "使用する", - "kXFojL": "事前にプレイブックを作成しておけば、必要なときにすぐに利用することができます。", - "HSi3uv": "担当者なし", - "/HtNUp": "{mode, select, DurationValue {期間 (\"4 hours\", \"7 days\"...)} DateTimeValue {時刻 (\"in 4 hours\", \"May 1\", \"Tomorrow at 1 PM\"...)} other {時刻または期間}} を選択するか指定します", - "zy3cJT": "ユーザーがキーワードを含むメッセージを投稿したときに、このプレイブックを実行するよう促すプロンプトを表示する", - "zx0myy": "参加者", - "zWkvNO": "タイムライン", - "zELxbG": "保存されたメッセージ", - "z3A0LP": "前回は {relativeTime} に実行されました", - "yxguVq": "変更を破棄する", - "yhzuSC": "時刻: {time}", - "yhU1et": "タスク", - "xmcVZ0": "検索する", - "x8cvBr": "実行の概要を見る", - "x5Tz6M": "レポート", - "wsUmh9": "チーム", - "wcWpGs": "不正なウェブフックURL", - "wbsq7O": "実行状況", - "wZ83YL": "今はやめておく", - "wL7VAE": "アクション", - "w7tf2z": "発行済み", - "w0muFd": "外向きのウェブフックを送信する (1行に1つ)", - "vndQuC": "スラッシュコマンドが実行されました", - "vir0m9": "不正なカテゴリ名です。", - "viXE32": "非公開", - "vOFN0m": "状況についての投稿を削除しました:", - "vNiZXF": "現在、進行中の実行はありません。プレイブックを実行して、チームやツールのワークフローのオーケストレーションを始めましょう。", - "v8ZnNc": "チームを選択する", - "v1SpKO": "ロールの変更", - "v1DNMW": "レトロスペクティブが {name} によって発行されました", - "usa8vQ": "ウェルカムメッセージを送信する", - "uny3Zy": "Playbooks", - "uhu5aG": "公開", - "uBLF+D": "プレイブックとは?", - "u4MwUB": "Playbook実行履歴を保存する", - "tzMNF3": "状況", - "twieZh": "実行の概要を見る", - "t6SiGO": "現在進行中の実行", - "syEQFE": "発行する", - "sqNmlF": "レトロスペクティブをスキップする", - "soePYH": "{num_checklists, plural, =0 {チェックリストがありません} one {# 個のチェックリスト} other {# 個のチェックリスト}}", - "scYyVv": "レトロスペクティブレポートを記入してみませんか?", - "sVlNlY": "チームの構造はそれぞれ異なります。チーム内のどのユーザーがPlaybookを作成できるかを管理できます。", - "sQu1rA": "{numTotalRuns, plural, =0 {まだ実行されてません} =1 {# 個の実行が開始されました} other {# 個の実行が開始されました}}", - "sIX63S": "システム管理者に通知されました", - "sGJpuF": "説明を追加する…", - "s3jjqi": "{num_actions, plural, =0 {アクション無し} one {# 個のアクション} other {# 個のアクション}}", - "ryrP8K": "このPlaybookを表示、変更、実行できる人の権限を管理します。", - "rbrahO": "閉じる", - "rDvvQs": "{completed, number} / {total, number} 完了", - "qyJtWy": "表示を少なくする", - "qp3Fk4": "プレイブックとは、チェックリスト、アクション、テンプレート、レトロスペクティブなどを含む、チームやツールが従うべきワークフローのことです。", - "q6f8x9": "前回更新時からの変更点", - "prYDT6": "アナウンスチャンネル", - "pjt3qA": "新しいチェックリスト", - "pKLw8O": "本当にこのイベントを削除してもいいですか?削除されたイベントは、タイムラインから永久に削除されます。", - "oVHn4s": "最終更新日", - "oS0w4E": "デフォルトの更新タイマー", - "o2eHmz": "実行が {name} によって完了されました", - "nqVby7": "{numTasks, number}{numTasks, plural, =1 {タスク} other {タスク}} 中 {numTasksChecked, number} 項目チェック済み", - "nmpevl": "廃棄する", - "nkCCM2": "今後、リマインドは通知されません。", - "lxfpbh": "オーナーは、{reminderEnabled, select, true {次の期間ごとに状況の更新を促されます} other {状況の更新を促されません}}", - "lrbrjv": "はい、レトロスペクティブを開始します", - "lZwZi+": "日付: {date}", - "lJyq2a": "実行が見つかりませんでした", - "kvgvNW": "何が起こったかを知る", - "kGI46P": "タスク内容", - "k9q07e": "更新を他のチャンネルにブロードキャストする", - "jwimQJ": "了解", - "jq4eWU": "プレイブックへのアクセス", - "jnmORb": "このプレイブック内", - "jS/UOn": "テンプレートを更新する", - "jIgqRa": "オーナー / 参加者", - "jIIWN+": "フォーマット済み", - "j7jdWG": "商用版へ移行する。", - "izWS4J": "フォロー解除する", - "ijAUQf": "システム管理者にアップグレードを通知する。", - "ieGrWo": "フォローする", - "iNU1lj": "要求した実行は非公開になっているか、存在しません。", - "hfrrC7": "チームイニシャル", - "hVFgh4": "完了済みを含む", - "hO9EdA": "チャンネルに {numInvitedUsers, plural, =0 {0 人のメンバー} =1 {1 人のメンバー} other {# 人のメンバー}} を招待する", - "guunZt": "割り当て", - "gt6BhE": "実行詳細", - "g5pX+a": "概要", - "g4IF1x": "このPlaybookの実行はありません。", - "fpuWL1": "プレイブックを削除する", - "fmylXu": "ユーザーがメッセージを投稿したときにプレイブックを実行するプロンプトを表示する", - "fdQDz+": "プレイブック {title} が正常に削除されました。", - "fXGjhC": "オーナーが{summary}から変更されました", - "fV6578": "オーナーロールを割り当てる", - "fUEpLA": "これらのフィルターに一致するタイムラインイベントはありません。", - "egvJrY": "担当者が変更されました", - "edxtzC": "Playbookを作成する", - "eKv7yX": "投稿", - "e/AZL5": "30日間のトライアルが開始されました", - "dsTLW1": "タスクを編集する", - "dIwav9": "このタスクを削除してよろしいですか?このタスクは、この実行からは削除されますが、プレイブックには影響しません。", - "d8KvXJ": "あなたのトライアルライセンスは{expiryDate}で終了します。カスタマーポータルからいつでもライセンスを購入することができます。", - "c6LNcW": "タスクを削除", - "bE1Cro": "自分の実行のみ", - "b3TdyZ": "トライアル開始 をクリックすると、Mattermost Software Evaluation Agreementプライバシーポリシーに同意したことになり、製品メールを受信するようになります。", - "b/QBNs": "更新予定", - "aYIUar": "ありがとうございます!", - "aWpBzj": "さらに表示", - "ZwlIYH": "アクティブな {activeRuns, number} {activeRuns, plural, one {実行} other {実行}}", - "ZdWYcm": "スキップします", - "ZAJviT": "システム管理者に通知できませんでした。", - "Z7vWDQ": "エラーが発生しました", - "Z/hwEf": "このチャンネルでは、{reminderEnabled, select, true {次の期間ごとに} other {}}レトロスペクティブを実行するようリマインドされます", - "YORRGQ": "更新を投稿する", - "YKn+7s": "このチャンネルではプレイブックは実行されていません。", - "Y+U8La": "プレイブック {title}を削除してよろしいですか?", - "X/koAN": "不正なエントリ: ウェブフックの最大数は 64 です", - "WTQpnI": "プレイブックを使って今すぐ取り掛かる", - "WIxhrv": "実行名は2文字以上にしてください", - "WAHCT2": "システム管理者へ通知する", - "W1Qs5O": "実行", - "W/V6+Y": "折りたたむ", - "VmnoW8": "詳細はシステムログを確認してください。", - "VOzlSL": "Playbookを実行すると、チームやツールのワークフローがオーケストレーションされます。", - "V5TY0z": "参加者を追加しますか?", - "UbTsGY": "{start} と {end} の間に開始された実行", - "TdTXXf": "さらに詳しく", - "TDaF6J": "破棄", - "TBez4r": "表示するPlaybooksがありません。このワークスペースでPlaybooksを作成する権限がありません。", - "SmAUf9": "リマインダーは{timestamp}に送信されます", - "S0kWcH": "更新期限切れ", - "RoGxij": "{date} にアクティブな実行", - "Rgo4VW": "このワークスペースの全員がプレイブックを作成できます。システム管理者はこの設定を変更できます。", - "R4vA+C": "以下のユーザーのみがプレイブックを作成できます。これらのユーザーおよびシステム管理者は、この設定を変更することができます。", - "Qrl6bQ": "プレイブックでプロセスを効率化する", - "QaZNp9": "実行を完了する", - "QVQrgH": "このプレイブックへの自分のアクセスを削除すると、自分を再び追加することはできません。このアクションを実行してもよろしいですか?", - "QUwMsX": "レトロスペクティブの記入を促すリマインダ", - "Q7hMnp": "Playbookを実行する", - "Q67RuY": "すべての実行を見る", - "OK8u0r": "チェックリスト、アクション、テンプレート、レトロスペクティブなど、チームやツールが従うべきワークフローを規定するプレイブックを作成します。", - "OINwWS": "{isPublic, select, true {公開} other {非公開}} チャンネルを作成する", - "OHfpS1": "これらのキーワードのいずれかを含む", - "Nh91Us": "全 {total, number} 中 {from, number}–{to, number}", - "NE1OeI": "チーム({team})の全員がアクセスできます。", - "N2IrpM": "確認", - "N1U/QR": "タスクの状態変更", - "Mm1Gse": "メンバー検索", - "MhKICa": "あなたのプランでは、1チームにつき1つのプレイブックを作成できます。サブスクリプションをアップグレードすると、各チーム固有のワークフローを定義した複数のプレイブックを作成できます。", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {タスク} other {タスク}}", - "MDP9TS": "プレイブックから削除する", - "M/2yY/": "まだ誰もいません。", - "Lg3I1b": "@{targetUsername}さん、 状況について教えてください。", - "Leh2tk": "チームの全実行内容を確認するにはここをクリックしてください。", - "LVYPbG": "オーナーを割り当てる", - "LRFvqz": "{oneChannel, plural, one {チャンネル} other {チャンネル}} でアナウンスする", - "L6k6aT": "...または、テンプレートから始める", - "KJu1sq": "チェックリストを削除する", - "K4O03z": "新しいタスク", - "K3r6DQ": "削除", - "JeqL8w": "{name} によってレトロスペクティブがキャンセルされました", - "JXdbo8": "完了", - "JJNc3c": "前へ", - "JJMNME": "{withRunName, select, true {@{authorUsername} が更新を投稿しました [{runName}]({overviewURL})} other {@{authorUsername} が更新を投稿しました}}", - "J1G4S4": "まだプレイブックが作成されていません。", - "IwY/wg": "すべてのプロセスのためのプレイブック", - "Ietscn": "タスク完了", - "I90sbW": "たった今", - "I2zEie": "レトロスペクティブレポートを使って、成功を祝い、失敗から学びましょう。プロセスのレビュー、ステークホルダーとのエンゲージメント向上、監査などの目的でタイムラインのイベントをフィルタリングできます。", - "Hzwzgs": "{oneChannel, plural, one {チャンネル} other {チャンネル}}に更新をブロードキャストする", - "HAlOn1": "名前", - "GxJAK1": "要求したPlaybookは非公開になっているか、存在しません。", - "GwtR3W": "既存のタスクをドラッグ&ドロップするか、クリックして新しいタスクを作成します。", - "GRTyvN": "プレイブックリストの表示切替", - "G/yZLu": "削除", - "FEGywG": "更新のリマインドを行う未来の日時を指定してください。", - "DuRxjT": "プレイブックを作成する", - "DtCplA": "{numParticipants, plural, =1 {# 人の参加者} other {# 人の参加者}}", - "D55vrs": "ライセンスを生成できませんでした", - "D2CE02": "Webhookを入力する", - "Cy1AK/": "実行の詳細を見る", - "CyGaem": "実行名", - "CkYhdY": "チャンネルをサイドバーのカテゴリーに追加する", - "CSts8B": "チームアイコン", - "CBM4vh": "次回更新に向けたタイマー", - "C9NScU": "あなたのチームをコントロールする", - "C1khRR": "Playbooksへ戻る", - "BQtd5I": "Playbooksへようこそ!", - "BNB75h": "Playbookは、何度も実行するような手続きのためのチェックリスト、自動化、およびテンプレートを規定するものです。{br} チームがエラーを減らし、ステークホルダーとの信頼関係を築き、イテレーションを重ねるごとに効果を高めていくのに役立ちます。", - "B487HA": "進行中", - "Auj1ap": "トライアルを開始したり、サブスクリプションをアップグレードすることができます。", - "ApULhK": "メンバーを招待する", - "AF9wda": "この更新は 概要ページ{hasBroadcast, select, true {に保存され、{broadcastChannelCount, plural, =1 {1つのチャンネル} other {{broadcastChannelCount, number} チャンネル}}へブロードキャストされます} other {に保存されます}}。", - "A8dbCS": "Playbookが見つかりません", - "A21Mgv": "実行完了", - "9tBhzB": "今すぐアップグレードする", - "9qc7BX": "スヌーズ", - "9kCT7Q": "重要なイベントやメッセージを自動的に記録するタイムラインを使うことで、チームはすぐに振り返りを実施することができます。", - "9TTfXU": "システム管理者に通知されました。", - "9PXW6Q": "期間 / 開始時期", - "91Hr5f": "ドラッグして順序を入れ替える", - "9+Ddtu": "次へ", - "6uhSSw": "チャンネルを選択する", - "6n0XDG": "本当にチェックリストを削除してもよろしいですか?すべてのタスクが削除されます。", - "6jDabx": "フィードバックを送る", - "6CGo3o": "ステータス / 最終更新日", - "5wqhGy": "実行詳細の表示切替", - "5qBEKB": "Playbookの実行とは?", - "5j6GD/": "{numParticipants, plural, =0 {参加者無し} =1 {# 人の参加者} other {# 人の参加者}}", - "5CI3KH": "サポートに連絡する", - "4ltHYh": "Playbookへ移動", - "42qmJ5": "権限がないためアップデートを投稿できません。", - "3Psa+5": "キーワードの追加", - "3/wF0G": "スラッシュコマンド", - "2VrVHu": "実行名で検索", - "2Qq4YX": "変更内容を破棄してよろしいですか?", - "2QkJ4s": "全体像を把握し、効率的にレトロスペクティブを進めるために重要なメッセージを保存します。", - "2PNrBQ": "後の分析のためにプレイブックを実行したチャンネルをエクスポートし、保存します。", - "1MQ3XZ": "{numActiveRuns, plural, =0 {アクティブな実行はありません} =1 {# 個のアクティブな実行} other {# 個のアクティブな実行}}", - "15jbT0": "タイムラインへ詳細を追加する", - "0wJ7N+": "タスク", - "0oLj/t": "展開", - "/YZ/sw": "トライアル開始", - "/MaJux": "レトロスペクティブを開始する", - "+hddg7": "実行タイムラインに追加する", - "zz6ObK": "復元", - "z3B83t": "Playbookを検索する", - "ypIsVG": "タスクを復元", - "wX3k9U": "無題のプレイブック", - "wO6NOM": "本当にこのタスクを復元しますか? このタスクがこの実行に追加されます", - "vjzpnC": "フィルター条件に一致するPlaybooksはありません。", - "q0cpUe": "チェックリストを追加する", - "nSFBC2": "+ タスク追加", - "m/Q4ye": "チェックリスト名を変更する", - "l7zMH6": "オプションを選択するか任意の期間を選択する", - "l0hFoB": "プレイブックの説明を追加する...", - "kDcpd/": "{numKeywords, plural, other {# キーワード}}", - "k1djnL": "チェックリストを削除する", - "iXNbPf": "名前の変更", - "hrgo+E": "アーカイブ", - "h+e7G+": "メッセージに {numKeywords, select, 1 {キーワード} other {これらのいずれか}} が含まれている場合、このプレイブックを実行するよう促します", - "fuDLDJ": "チャンネルを作成する", - "eLeFE2": "名前と説明を編集する", - "dvhvum": "(オプション)このPlaybookがどのような場合に使われるべきか記述します", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {実行} other {実行}} が進行中", - "dSC1YD": "タスクをスキップ", - "d4g2r8": "削除済み: {timestamp}", - "cp7KUI": "Playbook", - "cPIKU2": "フォロー中", - "ZWtlyd": "{name} によって実行が復元されました", - "YMrTRm": "実行概要", - "XXbWAU": "このPlaybookが実行される時に自動で更新を受け取りたい場合、これを選択してください。", - "X2K92H": "チェックリスト名", - "Vhnd2J": "説明の表示を切り替える", - "UMoxP9": "チャンネル名テンプレート (オプション)", - "RO+BaS": "実行へのリンクをコピーする", - "Oo5sdB": "Playbook名", - "O8o2lE": "カテゴリにチャンネルを追加する", - "NA7Cw1": "プレイブックへのリンクをコピーする", - "Mu2aDs": "チーム ({team}) の全員がアクセスできます。", - "MrJPOh": "ステータス更新を有効にする", - "Ja1sVR": "このプレイブックの実行では、ステータスの更新が無効化されています。", - "IfxUgC": "実行概要を追加する…", - "IOnm/Z": "利用可能な実行概要はありません。", - "I5NMJ8": "さらに", - "EQpfkS": "完了", - "E0LnBo": "オプションから選択するか、任意の期間 (\"2 weeks”, \"3days 12 hours\", \"45 minutes\", ...) を指定することができます", - "D9IV7i": "レトロスペクティブはこのプレイブックの実行では無効化されています。", - "C6Oghd": "実行概要を編集する", - "7VTSeD": "本当にこのタスクをスキップしますか? この実行では無視されますが、プレイブックには影響しません。", - "5Ofkag": "レトロスペクティブを有効にする", - "4vuNrq": "実行開始後の {duration}", - "3MSGcL": "チャンネル名が有効ではありません。", - "36GNZj": "Playbook {title} は正常にアーカイブされました。", - "2/2yg+": "追加", - "0oL1zz": "コピーしました!", - "0HT+Ib": "アーカイブ", - "/gbqA6": "実行開始前の {duration}", - "/ZsEUy": "本当にこのチェックリストを削除しますか? この実行からは削除されますが、プレイブックには影響しません。", - "/4tOwT": "スキップ", - "+Tmpup": "このPlaybookが実行された時、自動で更新を受け取ります。", - "vaYTD+": "{outstanding, plural, =1 {# の未解決タスクが} other {# の未解決タスクが}}あります。本当に実行を完了してもよろしいですか?", - "WbsomC": "レトロスペクティブの公開", - "TxCTXQ": "本当に実行を完了してもよろしいですか?", - "QywYDe": "実行を完了としてマークする", - "D/wCS9": "本当にレトロスペクティブを公開してもよろしいですか?", - "2563nT": "実行を完了する", - "EvBQLq": "Playbook管理者にする", - "5ciuDD": "チャンネル未参加", - "0Vvpht": "Playbookのメンバーにする", - "wylJpv": "{team}の全員がこのPlaybookを閲覧できます。", - "tVPYMu": "Playbook管理者", - "sDKojV": "Playbookをアーカイブする", - "ruJGqS": "Playbookへのアクセス", - "pK6+CW": "@{displayName}は、[{runName}]({overviewUrl})チャンネルのメンバーではありません。彼らをこのチャンネルに追加しますか?彼らはすべてのメッセージ履歴にアクセスできるようになります。", - "osuP6z": "ドラッグしてチェックリストを並び替える", - "o+ZEL3": "{timestamp} に公開済み", - "lQT7iD": "Playbookを作成する", - "iDMOiz": "チャンネルメンバー", - "gGcNUr": "権限がありません", - "g0mp+I": "非公開Playbookに変換しても、メンバーシップと実行履歴は保存されます。この変更は永続的で、元に戻すことはできません。本当に {playbookTitle} を非公開Playbookに変換しますか?", - "SXJ98n": "公開した後にレトロスペクティブレポートを編集することはできません。本当にレトロスペクティブレポートを公開しますか?", - "R/2lqw": "テンプレート選択", - "QpUBDr": "{members, plural, =0 {0人} =1 {1人} other {# 人}}がこのPlaybookにアクセスできます。", - "MJ89uW": "非公開Playbookへ変換する", - "Lo10yH": "不明なチャンネル", - "JqKASQ": "@{displayName}をチャンネルに追加する", - "HLn43R": "アクセス管理", - "EWz2w5": "Playbookを実行する", - "8oCVbz": "本当に公開してもよろしいですか", - "5BUxvl": "このチームの全員がこのPlaybookを閲覧できます。", - "3Ls2m+": "Playbookのメンバー", - "0tznw6": "非公開Playbookへ変換する", - "qsr3Zk": "実行概要を更新する", - "0q+hj2": "各実行をステークホルダーへ説明するための簡潔な説明文のテンプレートを定義します。", - "FXCLuZ": "合計 {total, number}", - "3PoGhY": "本当に公開してもよろしいですか?", - "4fHiNl": "複製", - "4alprY": "Playbookテンプレート", - "/urtZ8": "あなたのプレイブック", - "y7o4Rn": "本当に削除しますか?", - "xvBDOH": "本当にPlaybook {title} をアーカイブしますか?", - "uT4ebt": "例: リソース数、影響を受けた顧客数", - "tbjmvS": "同名のメトリクスが既に存在します。各メトリクスに固有の名前を追加してください。", - "rzbYbE": "目標値", - "rMhrJH": "メトリクスのタイトルを追加してください。", - "q/Qo8l": "非公開PlaybookはMattermsot Enterpriseでのみ利用可能です", - "mbo96h": "レトロスペクティブレポートと共に記入するカスタムメトリクスを設定してください", - "mVpO8u": "以前に見たことがありますか?", - "lBqu4h": "Playbookを復元する", - "gsMPAS": "ドル", - "f+bqgK": "メトリクスの名前", - "bTgMQ2": "このPlaybookはアーカイブされています。", - "a0hBZ0": "メトリクスを削除する", - "XpDetT": "これらのコツを表示しない。", - "VZRWFk": "例: コスト、購買", - "TxmjKI": "このメトリクスが何であるかの説明を記入してください", - "Sx3lHL": "整数", - "SVwJTM": "エクスポート", - "OyZnsJ": "実行ごと", - "NYTGIb": "了解", - "NJ9uPu": "キーメトリクス", - "MTzF3S": "本当にPlaybook {title} を復元しますか?", - "LI7YlB": "このメトリクスが何であり、どのように記入すべきかの詳細を追加してください。この説明は、各実行のレトロスペクティブページでこれらのメトリクスの値を入力するときに利用できます。", - "LDYFkN": "期間 (dd:hh:mm形式)", - "JrZ2th": "メトリクスの追加", - "FGzxgY": "例: 確認までの時間、解決までの時間", - "F4pfM/": "数字を入力するか、空欄のままにしてください。", - "9XUYQt": "インポート", - "9SIW2x": "各実行における目標値", - "6D6ffM": "dd:hh:mm (例:12:00:00) の形式で期間を入力するか、空白のままにしてください。", - "4cwL43": "アーカイブも含む", - "4aupaG": "Playbook {title} が正常に復元されました。", - "4BN53Q": "各実行の値が目標にどれだけ近いか、あるいは遠いかを示し、さらにチャートにプロットします。", - "1ikfp3": "このメトリクスを削除すると、今後の実行ではこのメトリクスの値は収集されません。", - "0Xt1ea": "このメトリクスの過去のデータには引き続きアクセスできます。", - "dxyZg3": "自分で試してみる", - "Q5hysF": "Playbooksでできること", - "6GTzTR": "このプレイブックの内容をいつでも確認できます", - "wPVxBN": "", - "Pue+oV": "", - "q/VD+s": "関係者が常に最新の情報を入手できるように、タイマーを設定し、ステータス更新のテンプレートを作成します。", - "GjCS6U": "テンプレートを選択する", - "hw83pa": "キーメトリクスを追跡し、価値を測定する", - "udrLSP": "メトリクスを使用して、実行中のパターンや進捗を把握し、パフォーマンスを追跡します。", - "/fU9y/": "このページでは、プレイブックのさまざまなセクションの詳細を確認することができます。", - "cEWBE3": "レトロスペクティブによりプロセスを評価し、実行のたびに改良と改善を行いましょう。", - "RzEVnf": "Playbooksは、重要な手順をより再現性のあるものにし、説明責任を果たすことができます。Playbookは複数回実行することができ、それぞれの実行に記録と振り返りを残すことができます。", - "GG1yhI": "様々なユースケースやイベントに対応したテンプレートが用意されています。Playbookは、そのまま使うことも、カスタマイズしてチームで共有することもできます。", - "GAuN6w": "前提条件を設定する", - "HGdWwZ": "タスクの作成と割り当て", - "Q3R9Uj": "プロセス全体のステップをここに記録します。各タスクを担当者に割り当て、オプションでタイムラインや関連アクションを追加します。", - "9m0I/B": "ステークホルダーに常に最新情報を提供", - "dZmYk6": "Playbookを正常に複製しました", - "lUfDe1": "Playbookの実行チャンネルをエクスポートし、後で分析するために保存します。", - "wbdGb5": "タスクを割り当てたり、チェックしたり、スキップして、チームが共にゴールに向かって進む方法を明確にします。", - "vL4++D": "進捗とオーナーシップの追跡", - "vJ2SaW": "ウェルカムメッセージの送信、主要メンバーの招待、チャンネル作成など、プレイブックの一部を自動化します。", - "I5DYM+": "学び、そして振り返る", - "fhMaTZ": "クイックツアーに参加する", - "Tt04f1": "会話から離れることなく、誰が関与し、何をする必要があるのかを確認できます。", - "R5Zh+l": "時間をかけてあなた自身のPlaybookを作成する前に、まずサンプルのPlaybookを体験してみることができます。", - "QbGfqo": "複数の場所にいる関係者に向けて発信し、たった一度の投稿で振り返りのための文書を残すことができます。", - "HXvk56": "ステータスの更新を投稿する", - "8n24G2": "サイドパネルで実行の詳細を表示", - "1isgPF": "", - "lgZf0l": "Playbooksを始める", - "ZkhArX": "Lets go!", - "1QosTr": "使用中", - "vQqT/8": "", - "0EEIkR": "", - "Vf/QlZ": "値の範囲", - "efeNi1": "10実行平均値", - "mvZUm3": "ここでは、プレイブックのコンポーネントを詳しく調べることができます。編集を選択すると、あなたのプロセスやモデルに合わせてプレイブックをカスタマイズすることができます。", - "l5/RKZ": "このPlaybookには、 完了した実行はありません。", - "69nlA3": "dd:hh:mm (例: 12:00:00) の形式で期間を入力してください。", - "ZNNjWw": "数値を入力してください。", - "fmbSyg": "値を追加する(dd:hh:mm 形式)", - "9a9+ww": "タイトル", - "NLeFGn": "to", - "lbs7UO": "過去10回の実行あたり", - "KXVV4+": "プレイブックプレビューページへようこそ!", - "M4gAc9": "値を追加する", - "NMxVd+": "メトリクス値を入力してください。", - "ru+JCk": "平均値", - "awG90C": "実行あたりの目標値", - "xVyHgP": "テスト実行を開始する", - "NiAH1z": "目標値", - "u7qh13": "あなたのプレイブックを実行する準備はできていますか?", - "p1I/Fx": "あなたの実行を自動作成しました", - "c23IHq": "チャンネルアクションは、このチャンネルにおける活動を自動化することができます", - "ao44YC": "メトリクスを設定する", - "Y4MU/9": "活動内容を確認するために テスト実行を開始する を選択する。", - "RUlvbf": "新しいプレイブックをテストしてみましょう!", - "MHzP9I": "チャンネルに新たに参加するユーザーに向けたメッセージを定義します。", - "MBNMo9": "チャンネルアクション", - "DPj6DM": "活動内容を確認するために実行 を選択する。", - "B3Q5mz": "トリガー", - "5AJmOz": "ユーザーがチャンネルに参加したとき", - "0RlzlZ": "ユーザーへ一時的なウェルカムメッセージを送信する", - "u4L4yd": "保存されていない変更があります", - "hCMWC+": "{followers, plural, =0 {0 ユーザー} =1 {1 ユーザー} other {# ユーザー}}に対してフォローを開始", - "e3z3P8": "破棄して離れる", - "Ob5cSv": "このページを離れると、変更した内容は保存されません。本当に変更を破棄してもよろしいですか?", - "dCtjdj": "プレイブックを実行する準備はできていますか?", - "Z3ybv/": "ユーザーのサイドバーカテゴリにチャンネルを追加する", - "2Q5PhZ": "Playbookの実行を促す", - "Ek1Fx2": "これらのキーワードを含むメッセージが投稿された場合", - "9j5KzL": "カテゴリー名を入力", - "+/x2FM": "Playbookを選択する", - "aEhjYg": "概要", - "zWgbGg": "今日", - "mLrh+0": "期限なし", - "iMjjOH": "次週", - "Ppx673": "レポート", - "MtrTNy": "明日", - "MbapTE": "{num} {num, plural, =1 {タスク} other {タスク}} が期限超過", - "I7+d55": "日時指定する (\"in 4 hours\", \"May 1\"...)", - "AF7+5o": "期限を追加", - "+PMJAg": "{followers, plural, =1 {1 ユーザー} =1 {1 ユーザー} other {# ユーザー}}のフォローを開始する", - "oBeKB4": "期限は{date}", - "mw9jVA": "タイトルを追加する", - "lkv547": "期限 (Professionalプランで利用可能)", - "lglICE": "説明文を追加する(オプション)", - "W0aij2": "アサイン...", - "UlJJ1i": "スラッシュコマンドを追加する", - "TTIQ6E": "タスクに期限を設定することで、担当者が優先順位をつけて仕事を進められるようになります。", - "NFyWnZ": "より効率的に仕事を進める", - "371AC3": "実行概要を更新する", - "oAJsne": "公開Playbook", - "mm5vL8": "招待されたメンバーのみ", - "lJ48wN": "非公開Playbook", - "Xgxruo": "チェックリストをスキップ", - "OqCzNb": "タスクを追加", - "JcefuP": "説明文を追加する(オプション)", - "9trZXa": "チーム内の誰でも閲覧可能", - "7P5T3W": "チェックリストを復元", - "g9pEhE": "期限", - "lyXljU": "タスクを複製", - "mCrdeS": "Playbook総実行数", - "5ZIN3u": "ステータス更新", - "4GjZsL": "Playbook総数", - "v5/Cox": "チェックリストを複製", - "k12r+v": "実行概要テンプレートを追加...", - "cyR7Kh": "戻る", - "XF8rrh": "''{name}'' にリンクをコピー", - "RrCui3": "概要", - "RQl8IW": "スヌーズ間隔…", - "MyIJbr": "コンテンツ", - "IxtSML": "チェックリストを追加", - "CwwzAU": "チェックリストの名前を入力してください", - "x1phlu": "制限時間なし", - "kYCbJE": "制限時間を追加", - "xHNF7i": "アクションの実行", - "uhDKO8": "Markdownでテンプレートを作成する", - "sX5Mn5": "1行に1つのWebhookを入力してください", - "mkLeuq": "選択したチャンネルに更新内容を配信する", - "kkw4kS": "この更新は {hasChannels, select, true {{broadcastChannelCount, plural, =1 {1 チャンネル} other {{broadcastChannelCount, number} チャンネル}}} other {}}{hasFollowersAndChannels, select, true { と } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {1つのダイレクトメッセージ} other {{followersChannelCount, number} 個のダイレクトメッセージ}}} other {}}に配信されます。", - "kV5GkX": "ステータス更新が投稿された時", - "j940pJ": "この更新は概要ページに保存されます。", - "giM/X9": "ステータス更新は毎に予定されています。新たな更新は{channelCount, plural, =0 {0 チャンネル} one {# チャンネル} other {# チャンネル}}{webhookCount, plural, =0 {0 個の外向きのウェブフック} one {# 個の外向きのウェブフック} other {# 個の外向きのウェブフック}} に投稿されます。", - "YQOmSf": "1行に1つのウェブフックを入力", - "XRyRzf": "ステータス更新は予定されていません。", - "OuZhcQ": "期間を指定 (\"8 hours\", \"3 days\"...)", - "HvAcYh": "{text}{rest, plural, =0 {} one { と もう一つ} other { と {rest} 個}}", - "F9LrJA": "フィルター", - "DaHpK1": "チャンネルを検索", - "28FTjr": "アクションの実行により、このチャネルのアクティビティを自動化することができます", - "/RnCQb": "外向きのウェブフックの送信", - "zl6378": "レトロスペクティブのメトリクスを設定", - "yllba1": "このアーカイブされたPlaybookの名前は変更できません。", - "aZGAOI": "ステータス更新テンプレートの追加…", - "TD8WrM": "このチームでは、複製は無効です。", - "OQplDX": "ステータス更新は毎に予定されています。新たな更新は{channelCount, plural, =0 {0 チャンネル} one {# チャンネル} other {# チャンネル}}{webhookCount, plural, =0 {0 個の外向きのウェブフック} one {# 個の外向きのウェブフック} other {# 個の外向きのウェブフック}} に投稿されます。", - "OKhRC6": "共有", - "LcC/pi": "ウェルカムメッセージを送信…", - "Brya9X": "実行概要テンプレートを追加…", - "9kQNdp": "このPlaybookは非公開です。", - "3hBelc": "レトロスペクティブは想定されていません。", - "xEQYo5": "レトロスペクティブレポートで記入するカスタムメトリクスを設定してください。", - "vSMfYU": "実行情報", - "oL7YsP": "最終編集 {timestamp}", - "Z2Hfu4": "実行概要を追加", - "iigkp8": "対応は完了しましたか?", - "opn6uf": "タイムラインの表示", - "o6N9pU": "アクション実行", - "lbr3Lq": "リンクをコピー", - "kEMvwX": "フィルターに合致する実行がありません。", - "hjteuA": "アクセス可能な全てのPlaybooksがここに表示されます", - "bf5rs0": "情報を表示", - "ZJS10z": "更新はまだ投稿されていません", - "Q15rLN": "更新を要求する...", - "GXjP8g": "アクセス可能な全ての実行がここに表示されます", - "GDCpPr": "最近のステータス更新", - "+qDKgW": "すべてのアップデートを見る", - "ocYb9S": "キーメトリクス", - "nc8QpJ": "最近の活動", - "m/KtHt": "オーナーを変更する権限がありません", - "RnOiCg": "実行{isFollowing, select, true {のフォロー解除が} other {をフォロー}}できませんでした", - "4mCpAv": "オーナーを変更できませんでした", - "lr1CUA": "Playbooksの閲覧", - "Ul0aFX": "Playbookのインポート", - "LfhTNW": "Playbooksと実行の閲覧と作成", - "GVpA4Q": "新しいPlaybookを作成", - "CFysvS": "Playbookドロップダウンの作成", - "/qDObA": "実行中を表示", - "/+8SGX": "{filteredNum} / {totalNum} イベントを表示中", - "zW/5AB": "Professional版の機能 これは30日間の無料トライアルで利用可能になる有償版の機能です", - "vDvWJ6": "無料トライアルで更新要求を試す", - "u6Fyic": "あなたの要求が実行チャンネルに送信されました。", - "pzTOmv": "フォロワー", - "pXWclp": "あなたの参加要求が実行チャンネルに送信されました。", - "pFK6bJ": "すべて見る", - "lKeJ+i": "概要はありません", - "jboo9u": "更新を要求", - "ch4Vs1": "ワンクリックで実行中のPlaybookに更新を要求し、更新が投稿されたときに直接通知されるようになります。この機能を試すには30日間の無料トライアルを開始してください。", - "Xx0WZV": "メッセージを送信", - "VpQKQE": "{displayName}は実行の参加者ではありません。彼らを参加者にしますか? 彼らはこのチャンネルのすべてのメッセージ履歴にアクセスできるようになります。", - "UePrSL": "{num} {num, plural, one {参加済} other {参加済}}", - "UMFnWV": "レトロスペクティブを見る", - "U8u4uF": "参加する", - "RCT0Px": "チャンネルに{displayName}を追加する", - "PdRg+3": "すべて見る...", - "P9PKvb": "メッセージが実行チャンネルに送信されました。", - "P6NEL/": "コマンド...", - "Nf9oAA": "あなたはこの実行に参加することになります。", - "NGqzDU": "更新を要求する", - "KeO51o": "チャンネル", - "JvEwg/": "更新を要求できませんでした", - "Jli9m7": "メッセージが実行チャンネルに送信され、更新を投稿するよう要求します。", - "J2NmIY": "参加する", - "9xs0pp": "値を追加...", - "5PpBsd": "リクエストは成功しませんでした。", - "4Iqlfe": "この実行に参加済みです。", - "1fXVVz": "期限...", - "1GOpgL": "担当者...", - "xfnuXm": "参加", - "wRM2AO": "更新リクエストが失敗しました。", - "wGp7l3": "{icon} ドル", - "s+rSpl": "{icon} 数値", - "qp5G0Z": "レトロスペクティブ機能にアクセスするにはアップグレードが必要です。", - "ojQue/": "{icon} 期間 (in dd:hh:mm)", - "mNgqXf": "この機能をアンロックするには:", - "j2VYGA": "すべてのPlaybooksを見る", - "ePhhuK": "あなたのリクエストが実行チャンネルに送信されました。", - "b+DwLA": "実行への参加をリクエストする。", - "SMrXWc": "お気に入り", - "PoX2HN": "リクエスト送信", - "PWmZrW": "すべての実行を見る", - "PW+sL4": "N/A", - "OfN7IN": "ステータス更新リクエストが実行チャンネルに送信されます。", - "KzHQCQ": "フィルターに合致する完了済みの実行がありません。", - "Gwmqz5": "更新をリクエスト", - "CV1ddt": "実行に参加", - "CUhlqp": "チュートリアルツアー製品画像", - "B9z0uZ": "実行への参加リクエストが失敗しました。", - "AH+V3r": "実行の参加者になります。", - "5HXkY/": "タイプ: {typeTitle}", - "3zF589": "{filterName} を全てリセット", - "+6DCr9": "参加者として、ステータス更新の投稿、タスクの割り当てと完了、レトロスペクティブの実行を行えるようになります。", - "cnfVhV": "実行を脱退{isFollowing, select, true {およびフォロー解除} other {}}する", - "SK5APX": "実行から脱退できませんでした。", - "Q4sutg": "脱退{isFollowing, select, true {とフォロー解除} other {}} する", - "wBZz47": "実行から脱退しました。", - "iEtImk": "実行を脱退{isFollowing, select, true {およびフォロー解除} other {}}すると、左サイドバーから削除されます。再度その実行を見つけるには、全ての実行から見つけてください。", - "fnihsY": "脱退", - "XS4umx": "{name}がステータス更新をスヌーズしました", - "Suyx6A": "Playbookをインポートできませんでした。JSONが正しいことを確認し、再度実行してください。", - "QegBKq": "Playbookに参加する", - "P6PLpi": "参加", - "Mjq//Y": "お気に入り解除", - "FgydNe": "閲覧", - "5Hzwqs": "お気に入り", - "AhY0vJ": "脱退とフォロー解除", - "gfUBRi": "実行を脱退する前に新しいオーナーをアサインしてください。", - "qGlwfc": "実行開始", - "j2FnDV": "この名前でチャンネルが作成されます", - "vqmRBs": "実行を再開する", - "k5EChD": "本当に実行を再開しますか?", - "Zg0obP": "実行を再開する", - "XnICdK": "実行に参加でき魔戦でした", - "03oqA2": "アクティブな実行", - "iQhFxR": "前回使用日", - "KjNfA8": "不正な期間", - "unwVil": "チャンネル参加リクエストに失敗しました。", - "ZRv7Dm": "参加リクエスト", - "M9tXoZ": "参加リクエストが実行チャンネルへ送信されます。", - "0QD99o": "チャンネルへの参加をリクエスト", - "w4Nhhb": "参加者の追加", - "q48ca7": "Playbooksに関するご意見をお聞かせください。", - "jrOlPO": "実行のステータス更新通知を受ける", - "fVMECF": "参加者", - "cUCiWw": "参加者になる", - "bCmvTY": "フィードバックを送る", - "FLG4Iu": "実行のオーナーにする", - "6rygzu": "実行から除外する", - "1OVPiC": "実行の参加者になります。参加者になると、ステータス更新の投稿、タスクの割り当てと完了、レトロスペクティブの実行を行うことができるようになります。", - "0Azlrb": "管理", - "/GCoTA": "クリア", - "wCDmf3": "更新を有効化", - "utHl3F": "{runName} に人を追加する", - "qDxsQH": "この実行に関わるために参加者になる", - "nsd54s": "ステータス更新の無効化", - "lqzBNa": "実行チャンネルから削除する", - "l/W5n7": "参加者はこの実行にリンクされたチャンネルに追加されます", - "jAo8dd": "{name} によって実行のステータス更新が無効化されました", - "ieL3dC": "チャンネルアクションを設定する", - "ha1TB3": "参加者が実行に参加したとき", - "cpGAhx": "本当にこの実行に対するステータス更新を無効にしますか?", - "b8Gps8": "{name} によって実行のステータス更新が有効化されました", - "Z18I+c": "チャンネルアクションによりチャンネルに対するアクティビティを自動化できます", - "Y1EoT/": "参加者が実行から脱退するとき", - "WFA0Cg": "本当にこの実行に対するステータス更新を有効にしますか?", - "WC+NOj": "この実行とリンクしているチャンネルにユーザーを追加する", - "H7IzRB": "ステータス更新を無効にする", - "9qqGGd": "参加者を招待する", - "5b1zuB": "実行チャンネルに追加する", - "1prgB2": "人を検索", - "1OluNs": "ステータス更新の有効化", - "//o1Nu": "更新を無効化", - "zSOvI0": "フィルター", - "u/yGzS": "{name} が @{user} を実行に追加しました", - "t6lwwM": "{requester} が {users} を実行から削除しました", - "qxYWTy": "所有する実行から全てのタスクを表示する", - "meD+1Q": "実行の参加者", - "jfpnye": "@{user} が実行を脱退しました", - "iH5e4J": "このチャンネルにリンクされたチャンネルに追加されます。", - "grv9Fm": "タスクリストの表示を切り替えます。", - "feNxoJ": "{requester} が {users} を実行に追加しました", - "fBG/Ge": "コスト", - "ecS/qx": "{name} が {num} 人の参加者を実行に追加しました", - "YBvwXR": "アサインされたタスクはありません", - "WFd88+": "チェック済みのタスクを表示する", - "VjJYEV": "例: 売上への影響、購買", - "VM75su": "{name} が {num} 人の参加者を実行から削除しました", - "UAS7Bn": "この実行とリンクしているチャンネルへのアクセスを要求する", - "TnUG7m": "保留中のアサイン済みタスクはありません。", - "SwlL5j": "@{user} が実行に参加しました", - "SRqpbI": "{assignedNum, plural, =0 {アサインされたタスクはありません} other {# アサイン中}}", - "RXjd3Q": "{name} が @{user} を実行から削除しました", - "NGKqOC": "この実行とリンクしているチャンネルに参加する", - "L6vn9U": "実行の参加者", - "I0NIMp": "あなたのタスク", - "Gg/nch": "未参加", - "DUU48k": "アサインされたタスクはありません。フィルターを使って検索範囲を拡大することができます。", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# 期限切れ}}", - "BJNrYQ": "参加者として、実行サマリーの更新、タスクのチェック、ステータス更新の投稿、レトロスペクティブの編集ができるようになります。", - "9X3jwi": "{icon} コスト", - "36NwLv": "実行の参加者リストを管理する", - "lqceIp": "もしくは Playbookをインポートする", - "dK2JKl": "既存のチャンネルとリンクする", - "ORJ0Hb": "{outstanding, plural, =1 {# 件の未解決タスク} other {# 件の未解決タスク}}があります。本当に参加者全員に対して実行を完了しますか?", - "IdTL+v": "実行のチャンネルを作成する", - "AG7PKJ": "実行の名前を変更する", - "2BCWLD": "チャンネルを設定する", - "0boT49": "本当に参加者全員に対して実行を終了しますか?", - "a2r7Vb": "非公開チャンネル", - "VA1Q/S": "公開チャンネル", - "zxj2Gh": "最終更新 {time}", - "zscc/+": "{outstanding, plural, =1 {# つの未解決タスクがあります} other {未解決タスクが # 個あります}}. 本当にすべての参加者に対して{runName}を終了しますか?", - "yP3Ud4": "このチャンネルにリンクされた進行中の実行はありません", - "tqAmbk": "進行中の実行", - "prs4kX": "特定のキーワードを含むメッセージが投稿された時", - "m8hzTK": "最終使用 {time}", - "kQAf2d": "選択", - "gS1i4/": "タスクを完了としてマークする", - "gGtlrk": "あなたのPlaybooks", - "fvNMLo": "タスクアクション", - "cGCoJe": "投稿者", - "bEoDyV": "@{authorUsername} が [{runName}]({overviewURL}) に更新を投稿しました", - "ZSa3cf": "@{targetUsername}, [{runName}]({playbookURL}) に対するステータス更新を提供してください。", - "Z1sgPO": "終了した実行を見る", - "Wy3sw+": "{count, plural, =1{1 つの実行が進行中} =0 {進行中の実行はありません} other {# 実行が進行中}}", - "W1EKh5": "新しいPlaybookを作成する", - "SRbTcY": "その他のPlaybooks", - "RgQwWr": "実行のソート順", - "RC6rA2": "最近作成されたもの", - "Q/t0//": "終了した実行", - "NNksk4": "アルファベット順", - "LKu0ex": "本当にすべての参加者に対して {runName} を終了しますか?", - "L1tFef": "スペルを確認するか、別のワードで検索してみてください", - "KQunC7": "このチャンネルで使用されている", - "HfjhwE": "Playbooksを検索", - "GZoWl1": "このタスクのアクティビティを自動化する", - "EVSn9A": "実行開始", - "Bgt0C8": "{runName} に対する更新は、{hasChannels, select, true {{broadcastChannelCount, plural, =1 {1つのチャンネル} other {{broadcastChannelCount, number} チャンネル}}} other {}}{hasFollowersAndChannels, select, true { と } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {1 つのダイレクトメッセージ} other {{followersChannelCount, number} ダイレクトメッセージ}}} other {}} に配信されます。", - "AoNLta": "このチャンネルにリンクされた完了済みの実行がありません", - "9AQ5FE": "実行概要", - "95v+5O": "{actions, plural, =0 {タスクアクション} one {# アクション} other {# アクション}}", - "7KMbBa": "未使用", - "3sXVwy": "タスクアクション...", - "3Yvt4d": "Playbooksは、チームが成果をあげるために必要なプロセスを、何度も繰り返し実行できるようチェックリストとして定義する機能です", - "2NDgJq": "最終ステータス更新", - "0CeyUV": "\"{searchTerm}\" に該当する結果はありません", - "9w0mDI": "事前に割り当てられたメンバーを削除することを確認する", - "HGSVzc": "複数のファイルを一度にインポートすることはできません。", - "DQn9Uj": "{name} は、1つ以上のタスクが事前に割り当てられています。このユーザーを自動的に招待しないと、事前割り当てがクリアされます。{br}{br}本当にこのユーザーを実行のメンバーとして招待するのをやめますか?", - "BiQjuS": "実行は {channel} に移動しました", - "QJTSaI": "実行を別のチャンネルにリンク", - "MieztS": "エクスポートされたPlaybookファイルをドロップしてインポートします。", - "uYrkxy": "ファイルは有効なJSON形式のPlaybookテンプレートでなければなりません。", - "uCS6py": "このPlaybookを閲覧する権限がありません", - "mILd++": "実行名が {maxLength} 文字を超えてはいけません", - "m4vqJl": "ファイル", - "l3QwVw": "チャンネルを選択", - "ksG35Q": "このワークスペースでPlaybookを作成する権限がありません。", - "k7Nzfi": "招待を無効にする", - "fwW0T1": "事前割り当てメンバーを削除します", - "Zbk+OU": "ファイルサイつが5MBの制限を超えています。", - "YKLHXL": "進行中の実行を表示", - "TP/O/b": "ユーザーを削除する", - "QvEO6m": "この実行を編集する権限がありません", - "LaseGE": "このチェックリストを編集する権限がありません", - "IE2BzH": "1以上のタスクに事前に割り当てられているユーザーがいます。招待を無効にすると、すべての 事前割り当てがクリアされます。{br}{br}本当に招待を無効にしますか?", - "Edy3wX": "チェックリストを {channel} に移動しました", - "8//+Yb": "チェックリストを別のチャンネルにリンクする", - "706Soh": "タスク完了", - "OqWwvQ": "{user} がチェックリストの \"{name}\" のチェックを外しました", - "vjb+hS": "{user} がチェックリストの \"{name}\" を復元しました", - "XHJUSG": "実行を自動でフォロー", - "DqTQOp": "一度だけ", - "8FzC0B": "{user} がチェックリストの \"{name}\"にチェックを入れました", - "DKiv0o": "{user} がチェックリストの \"{name}\" をスキップしました", - "9M92On": "チャンネルを選択", - "3qPQMX": "{name} がステータスの更新を要求しました" -} diff --git a/webapp/playbooks/i18n/kk.json b/webapp/playbooks/i18n/kk.json deleted file mode 100644 index ffd3bc65b07..00000000000 --- a/webapp/playbooks/i18n/kk.json +++ /dev/null @@ -1,453 +0,0 @@ -{ - "eiPBw7": "", - "yhU1et": "", - "bPLen5": "", - "iNU1lj": "", - "l0hFoB": "", - "cp7KUI": "", - "FXCLuZ": "", - "nmpevl": "", - "bGhCLX": "", - "qsr3Zk": "", - "kDcpd/": "", - "lrbrjv": "", - "lQT7iD": "", - "nSFBC2": "", - "m/Q4ye": "", - "fUEpLA": "", - "egvJrY": "", - "cPIKU2": "", - "c8hxKk": "", - "bLK+Kr": "", - "nkCCM2": "", - "lxfpbh": "", - "lbhO3D": "", - "lZwZi+": "", - "bE1Cro": "", - "b5FaCc": "", - "b40Pr7": "", - "b3TdyZ": "", - "b/QBNs": "", - "aWpBzj": "", - "aACJNp": "", - "Z/hwEf": "", - "OsDomv": "", - "Oo5sdB": "", - "OcpRSQ": "", - "ObmjTB": "", - "OK8u0r": "", - "OINwWS": "", - "OHfpS1": "", - "O8o2lE": "", - "Nh91Us": "", - "NA7Cw1": "", - "N2IrpM": "", - "N1U/QR": "", - "MvEydR": "", - "MrJPOh": "", - "LmhSmU": "", - "Lg3I1b": "", - "K4O03z": "", - "K3r6DQ": "", - "JqKASQ": "", - "JeqL8w": "", - "Ja1sVR": "", - "JXdbo8": "", - "JJNc3c": "", - "JJMNME": "", - "JCGvY/": "", - "FEGywG": "", - "EQpfkS": "", - "EC5MJD": "", - "E0LnBo": "", - "DuRxjT": "", - "DtCplA": "", - "DnBhRg": "", - "0q+hj2": "", - "l7zMH6": "", - "kGI46P": "", - "k9q07e": "", - "iDMOiz": "", - "fV6578": "", - "avPeEI": "", - "aYIUar": "", - "XmUdvV": "", - "X2K92H": "", - "X/koAN": "", - "o+ZEL3": "", - "nqVby7": "", - "hXIYHG": "", - "hVFgh4": "", - "hO9EdA": "", - "h+e7G+": "", - "gy/Kkr": "", - "guunZt": "", - "gt6BhE": "", - "gGcNUr": "", - "g5pX+a": "", - "g4IF1x": "", - "g0mp+I": "", - "fuDLDJ": "", - "fpuWL1": "", - "fmylXu": "", - "fXGjhC": "", - "YORRGQ": "", - "YMrTRm": "", - "YKn+7s": "", - "YDuW/T": "", - "Y+U8La": "", - "XXbWAU": "", - "SXJ98n": "", - "C1khRR": "", - "BQtd5I": "", - "BNB75h": "", - "BD66u6": "", - "B487HA": "", - "Auj1ap": "", - "ArpdYl": "", - "8oCVbz": "", - "Cy1AK/": "", - "ApULhK": "", - "AS5kar": "", - "AML4RW": "", - "AF9wda": "", - "A8dbCS": "", - "A21Mgv": "", - "9uOFF3": "", - "9tBhzB": "", - "9qc7BX": "", - "9kCT7Q": "", - "9TTfXU": "", - "9PXW6Q": "", - "9Obw6C": "", - "91Hr5f": "", - "9+Ddtu": "", - "8hDbW6": "", - "7VTSeD": "", - "6uhSSw": "", - "6n0XDG": "", - "6jDabx": "", - "6CGo3o": "", - "5wqhGy": "", - "5qBEKB": "", - "5j6GD/": "", - "5ciuDD": "", - "5Ofkag": "", - "5FRgqE": "", - "5CI3KH": "", - "5BUxvl": "", - "3Ls2m+": "", - "2Qq4YX": "", - "2QkJ4s": "", - "2PNrBQ": "", - "2563nT": "", - "0tznw6": "", - "0Vvpht": "", - "xmcVZ0": "", - "x8cvBr": "", - "vNiZXF": "", - "v1SpKO": "", - "v1DNMW": "", - "usa8vQ": "", - "uny3Zy": "", - "uhu5aG": "", - "uBLF+D": "", - "u4MwUB": "", - "tzMNF3": "", - "sQu1rA": "", - "sIX63S": "", - "rDvvQs": "", - "qyJtWy": "", - "qp3Fk4": "", - "q6f8x9": "", - "q0cpUe": "", - "pjt3qA": "", - "pKLw8O": "", - "pK6+CW": "", - "oVHn4s": "", - "oS0w4E": "", - "kvgvNW": "", - "kXFojL": "", - "hzt6l8": "", - "hrgo+E": "", - "hfrrC7": "", - "edxtzC": "", - "eLeFE2": "", - "eHAvFf": "", - "e/AZL5": "", - "dvhvum": "", - "dsTLW1": "", - "djALPR": "", - "dSC1YD": "", - "d9epHh": "", - "d8KvXJ": "", - "d4g2r8": "", - "ZdWYcm": "", - "ZWtlyd": "", - "ZAJviT": "", - "Z7vWDQ": "", - "WTQpnI": "", - "WIxhrv": "", - "WAHCT2": "", - "W1Qs5O": "", - "W/V6+Y": "", - "VmnoW8": "", - "Vhnd2J": "", - "VOzlSL": "", - "V5TY0z": "", - "Ui6GK/": "", - "UbTsGY": "", - "UMoxP9": "", - "TdTXXf": "", - "TZYiF/": "", - "TSSNg/": "", - "TJo5E6": "", - "TBez4r": "", - "T7Ry38": "", - "T5rX+W": "", - "SmAUf9": "", - "SENRqu": "", - "SDSqfA": "", - "S0kWcH": "", - "RthEJt": "", - "RoGxij": "", - "RO+BaS": "", - "R+JQaJ": "", - "QywYDe": "", - "Mm1Gse": "", - "MFpAtm": "", - "Leh2tk": "", - "LRFvqz": "", - "L6k6aT": "", - "KiXNvz": "", - "KUr+sG": "", - "KJu1sq": "", - "IuFETn": "", - "IfxUgC": "", - "Ietscn": "", - "IOnm/Z": "", - "Hzwzgs": "", - "HhLp57": "", - "HSi3uv": "", - "HAlOn1": "", - "DXACD6": "", - "DSVJjB": "", - "DCl7Vv": "", - "D9IV7i": "", - "D55vrs": "", - "CkYhdY": "", - "CjNrqO": "", - "CSts8B": "", - "CBM4vh": "", - "C9NScU": "", - "C6Oghd": "", - "zELxbG": "", - "z3B83t": "", - "z3A0LP": "", - "yxguVq": "", - "wylJpv": "", - "wsUmh9": "", - "wcWpGs": "", - "wbwhbH": "", - "wbsq7O": "", - "waVyVY": "", - "wZ83YL": "", - "wX3k9U": "", - "wO6NOM": "", - "wL7VAE": "", - "wEQDC6": "", - "w0muFd": "", - "vndQuC": "", - "vjzpnC": "", - "vaYTD+": "", - "twieZh": "", - "tVPYMu": "", - "t6SiGO": "", - "syEQFE": "", - "sqNmlF": "", - "soePYH": "", - "scYyVv": "", - "sVlNlY": "", - "sDKojV": "", - "ruJGqS": "", - "recCg9": "", - "rbrahO": "", - "rX08cW": "", - "osuP6z": "", - "o2eHmz": "", - "lJyq2a": "", - "k1djnL": "", - "jwimQJ": "", - "jvo0vs": "", - "jnmORb": "", - "jXT2++": "", - "jS/UOn": "", - "jIgqRa": "", - "jIIWN+": "", - "j7jdWG": "", - "ijAUQf": "", - "ieGrWo": "", - "iXNbPf": "", - "TxCTXQ": "", - "R/2lqw": "", - "Qrl6bQ": "", - "QpUBDr": "", - "QnZAit": "", - "QiKcO7": "", - "QaZNp9": "", - "QUwMsX": "", - "Q8Qw5B": "", - "Q7hMnp": "", - "Q7aZO4": "", - "Q67RuY": "", - "MJ89uW": "", - "M/2yY/": "", - "Lo10yH": "", - "ICqy9/": "", - "I90sbW": "", - "I5NMJ8": "", - "I2zEie": "", - "HLn43R": "", - "GxJAK1": "", - "GwtR3W": "", - "GRTyvN": "", - "G/yZLu": "", - "EvBQLq": "", - "EWz2w5": "", - "D2CE02": "", - "CyGaem": "", - "yqpcOa": "", - "ypIsVG": "", - "x5Tz6M": "", - "viXE32": "", - "s3jjqi": "", - "ryrP8K": "", - "zz6ObK": "", - "zx0myy": "", - "zWkvNO": "", - "zINlao": "", - "5A46pW": "", - "4vuNrq": "", - "4ltHYh": "", - "4Hrh5B": "", - "47FYwb": "", - "42qmJ5": "", - "3rCdDw": "", - "3Psa+5": "", - "3MSGcL": "", - "36GNZj": "", - "3/wF0G": "", - "2VrVHu": "", - "2/2yg+": "", - "1MQ3XZ": "", - "1I48bs": "", - "15jbT0": "", - "0wJ7N+": "", - "0oLj/t": "", - "0oL1zz": "", - "0HT+Ib": "", - "/jUtaM": "", - "/gbqA6": "", - "/ZsEUy": "", - "/YZ/sw": "", - "/MaJux": "", - "/HtNUp": "", - "/4tOwT": "", - "/1FEJW": "", - "+hddg7": "", - "+Tmpup": "", - "+QgvjN": "", - "+8G9qr": "", - "F4pfM/": "", - "lgZf0l": "", - "Q5hysF": "", - "q/Qo8l": "", - "a0hBZ0": "", - "SVwJTM": "", - "y7o4Rn": "", - "8n24G2": "", - "hw83pa": "", - "vQqT/8": "", - "/fU9y/": "", - "LI7YlB": "", - "XpDetT": "", - "4cwL43": "", - "QbGfqo": "", - "fhMaTZ": "", - "9m0I/B": "", - "R5Zh+l": "", - "NJ9uPu": "", - "mbo96h": "", - "ZkhArX": "", - "NYTGIb": "", - "vJ2SaW": "", - "OyZnsJ": "", - "TxmjKI": "", - "0Xt1ea": "", - "Q3R9Uj": "", - "vL4++D": "", - "RzEVnf": "", - "Sx3lHL": "", - "wbdGb5": "", - "LDYFkN": "", - "JrZ2th": "", - "1isgPF": "", - "MTzF3S": "", - "rzbYbE": "", - "udrLSP": "", - "6GTzTR": "", - "lUfDe1": "", - "cEWBE3": "", - "mVpO8u": "", - "wPVxBN": "", - "Pue+oV": "", - "rMhrJH": "", - "q/VD+s": "", - "1ikfp3": "", - "Tt04f1": "", - "dxyZg3": "", - "I5DYM+": "", - "HGdWwZ": "", - "GAuN6w": "", - "HXvk56": "", - "GjCS6U": "", - "GG1yhI": "", - "1QosTr": "", - "dZmYk6": "", - "0EEIkR": "", - "uT4ebt": "", - "tbjmvS": "", - "gsMPAS": "", - "f+bqgK": "", - "VZRWFk": "", - "FGzxgY": "", - "9SIW2x": "", - "6D6ffM": "", - "4BN53Q": "", - "xvBDOH": "", - "lBqu4h": "", - "bTgMQ2": "", - "4aupaG": "", - "9XUYQt": "", - "4alprY": "", - "/urtZ8": "", - "4fHiNl": "", - "3PoGhY": "", - "NiAH1z": "", - "69nlA3": "", - "mvZUm3": "", - "l5/RKZ": "", - "awG90C": "", - "M4gAc9": "", - "9a9+ww": "", - "lbs7UO": "", - "fmbSyg": "", - "xVyHgP": "", - "Vf/QlZ": "", - "efeNi1": "", - "KXVV4+": "", - "ru+JCk": "", - "ZNNjWw": "", - "NMxVd+": "", - "NLeFGn": "" -} diff --git a/webapp/playbooks/i18n/ko.json b/webapp/playbooks/i18n/ko.json deleted file mode 100644 index 6aa96c1367e..00000000000 --- a/webapp/playbooks/i18n/ko.json +++ /dev/null @@ -1,768 +0,0 @@ -{ - "bPLen5": "지난 30일 동안 완료된 실행 수", - "g5pX+a": "에 대한", - "zINlao": "소유자", - "z5RMPO": "사용자만 이 플레이북에 액세스할 수 있습니다", - "wbwhbH": "작업명", - "wbsq7O": "사용법", - "uJ3bRR": "이 양식은 이해 관계자에게 각 실행을 설명하는 간결한 설명의 형식을 표준화하는 데 도움이 됩니다.", - "t6SiGO": "현재 진행 중인 것 실행", - "rX08cW": "날짜는 미래여야 합니다.", - "lZwZi+": "날짜: {date}", - "jIIWN+": "미리 포맷된", - "hzt6l8": "마크다운을 사용하여 양식을 작성합니다.", - "hXIYHG": "채널 내보내기를 지원하기 위해 채널 내보내기 플러그인 설치 및 활성화", - "gy/Kkr": "(수정됨)", - "djXM+y": "선택된 사용자만 액세스할 수 있습니다.", - "d9epHh": "채널 로그 내보내기", - "TJo5E6": "프리뷰", - "T7Ry38": "메시지", - "recCg9": "업데이트", - "lbhO3D": "이탤릭", - "jq4eWU": "플레이북 접근", - "jXT2++": "채널로 이동", - "oS0w4E": "기본 업데이트 시간", - "SFuk1v": "권한", - "uhu5aG": "공개", - "wEQDC6": "수정", - "viXE32": "비공개", - "SENRqu": "도움", - "jvo0vs": "저장", - "ebkl6I": "팀의 모든 사람이 이 플레이북에 액세스할 수 있습니다", - "eHAvFf": "굵게", - "b40Pr7": "리포터", - "dcV/DJ": "{timestamp}", - "IuFETn": "지속기간", - "ICqy9/": "체크리스트", - "HhLp57": "인용", - "EC5MJD": "사용할 수 있는 업데이트가 없습니다.", - "DnBhRg": "사람 추가", - "DCl7Vv": "인라인 코드", - "CL5OZP": "선택된 사용자만 이 플레이북을 편집하거나 실행할 수 있습니다.", - "BD66u6": "채널에서 모든 메시지가 포함된 CSV 다운로드", - "AS5kar": "참가자 ({participants})", - "A3ptul": "양식", - "9uOFF3": "개요", - "6Lwe7T": "{team}의 모든 사용자가 이 플레이북에 액세스할 수 있습니다", - "5FRgqE": "채널 로그 다운로드", - "5A46pW": "슬래시 명령어 추가", - "47FYwb": "취소", - "3rCdDw": "상태 업데이트", - "/jUtaM": "지난 14일 동안 일일 활성 실행", - "/1FEJW": "지난 14일 동안의 일일 활성 참가자 수", - "+ZIXOR": "채널 접근", - "TyrY2b": "플레이북 생성", - "X3DLGJ": "이 작업 공간에 있는 모든 사람이 플레이북을 만들 수 있습니다.", - "AT2QBo": "선택 된 사용자만 플레이북을 생성 할 수 있습니다.", - "D3idYv": "설정", - "aWpBzj": "더보기", - "aYIUar": "감사합니다!", - "c6LNcW": "작업 삭제", - "BQtd5I": "플레이북에 오신 것을 환영합니다!", - "B487HA": "진행 중", - "A8dbCS": "플레이북을 찾을 수 없음", - "6uhSSw": "채널 선택", - "9Obw6C": "필터", - "9tBhzB": "지금 판올림", - "9+Ddtu": "다음", - "5CI3KH": "문의처", - "3Psa+5": "키워드 추가", - "3/wF0G": "슬래시 명령어", - "0wJ7N+": "작업", - "4ltHYh": "플레이북으로 이동", - "0oLj/t": "펼침", - "/4tOwT": "건너뛰기", - "+QgvjN": "소유자 역할 할당", - "6jDabx": "피드백 제공", - "cPIKU2": "팔로잉", - "kGI46P": "", - "sIX63S": "시스템 관리자에게 알림이 전송되었습니다", - "jnmORb": "", - "1MQ3XZ": "{numActiveRuns, plural, =0 {활성 실행 없음} =1 {#개의 활성 실행} other {#개의 활성 실행}}", - "eiPBw7": "회고 리마인더 주기", - "Ja1sVR": "", - "hO9EdA": "", - "hVFgh4": "완료된 것을 포함", - "vaYTD+": "", - "sDKojV": "플레이북 보관", - "pK6+CW": "", - "vjzpnC": "해당 필터와 일치하는 플레이북이 없습니다.", - "jIgqRa": "소유자 / 참여자", - "RO+BaS": "실행의 링크 복사", - "5wqhGy": "", - "QUwMsX": "회고 작성 알림", - "X2K92H": "체크리스트 이름", - "8hDbW6": "", - "O8o2lE": "", - "Ietscn": "", - "I2zEie": "회고 보고서를 통해 성공을 축하하고 실수로부터 교훈을 얻으세요. 프로세스 검토, 이해 관계자 참여 및 감사 목적으로 타임라인 이벤트를 필터링하세요.", - "bGhCLX": "", - "nkCCM2": "다시 알림을 받지 않습니다.", - "zWkvNO": "타임라인", - "yxguVq": "", - "OK8u0r": "", - "NA7Cw1": "", - "Oo5sdB": "플레이북 이름", - "D55vrs": "라이선스를 생성할 수 없습니다", - "GRTyvN": "", - "V5TY0z": "", - "L6k6aT": "... 혹은 템플릿으로 시작", - "scYyVv": "회고 보고서를 작성하시겠습니까?", - "JeqL8w": "{name}님이 회고를 취소했습니다", - "JCGvY/": "", - "yqpcOa": "사용", - "j7jdWG": "상용 버전으로 전환합니다.", - "q0cpUe": "", - "RoGxij": "{date}에 실행 활성화", - "jwimQJ": "확인", - "iNU1lj": "요청 중인 Run이 비공개이거나 존재하지 않습니다.", - "ypIsVG": "작업 복원", - "Lo10yH": "알 수 없는 채널", - "X/koAN": "유효하지 않은 항목: 허용되는 웹훅의 최대 개수는 64개입니다", - "C1khRR": "플레이북으로 돌아가기", - "DuRxjT": "", - "MFpAtm": "{numTasks, number} {numTasks, plural, one{개 작업} other{개 작업}}", - "QaZNp9": "실행 완료", - "lxfpbh": "", - "9kCT7Q": "", - "42qmJ5": "업데이트를 게시하기 위한 권한이 없습니다.", - "ZAJviT": "시스템 관리자에게 알릴 수 없었습니다.", - "edxtzC": "플레이북 생성", - "ZdWYcm": "아뇨, 회고를 건너뜁니다", - "+8G9qr": "회고록에 포함될 기본 텍스트입니다.", - "36GNZj": "{title} 플레이북이 보관되었습니다.", - "/HtNUp": "선택 또는 지정 {mode, select, DurationValue {기간 (\"4시간\", \"7 일\"...)} DateTimeValue {시점 (\"4시간 이내\", \"5월 1일\", \"내일 오후 1시\"...)} other {시간 또는 기간}}", - "XmUdvV": "필요한 모든 통계", - "JqKASQ": "", - "z3A0LP": "", - "uny3Zy": "플레이북", - "FXCLuZ": "전체 {total, number}", - "JJMNME": "", - "soePYH": "{num_checklists, plural, =0 {체크리스트 없음} one {# 체크리스트} other {# 체크리스트}}", - "e/AZL5": "30일 평가판 사용이 시작되었습니다", - "fUEpLA": "", - "Qrl6bQ": "", - "JXdbo8": "완료", - "v1SpKO": "역할 변경", - "k9q07e": "", - "sVlNlY": "모든 팀의 구조는 다릅니다. 팀에서 어떤 사용자가 플레이북을 만들 수 있는지 관리할 수 있습니다.", - "sQu1rA": "{numTotalRuns, plural, =0 {시작된 Run 없음} =1 {#개의 Run이 시작됨} other {#개의 Run이 시작됨}}", - "zx0myy": "참가자들", - "Q7hMnp": "플레이북 실행", - "zz6ObK": "복원", - "yhU1et": "작업", - "waVyVY": "현재 활동 중인 참여자", - "wO6NOM": "", - "vndQuC": "슬래시 명령어가 실행되었습니다", - "qp3Fk4": "", - "nmpevl": "", - "lJyq2a": "Run을 찾을 수 없습니다", - "l7zMH6": "", - "kvgvNW": "", - "kXFojL": "", - "ijAUQf": "시스템 관리자에게 업그레이드를 요청합니다.", - "fuDLDJ": "", - "fpuWL1": "", - "fV6578": "소유자 역할 지정", - "egvJrY": "할당된 담당자 변경됨", - "dSC1YD": "작업 건너뛰기", - "d4g2r8": "삭제됨: {timestamp}", - "b/QBNs": "업데이트 마감", - "W/V6+Y": "접기", - "VmnoW8": "자세한 내용은 시스템 로그를 확인하세요.", - "TdTXXf": "자세히 알아보기", - "TBez4r": "볼 플레이북이 없습니다. 이 작업 공간에서 플레이북을 만들 수 있는 권한이 없습니다.", - "Q67RuY": "", - "ObmjTB": "슬래시 명령어", - "OHfpS1": "", - "Leh2tk": "", - "K4O03z": "", - "ruJGqS": "플레이북 액세스", - "qsr3Zk": "", - "lQT7iD": "플레이북 생성", - "0q+hj2": "", - "zELxbG": "저장된 메시지", - "z3B83t": "플레이북 검색", - "nSFBC2": "", - "m/Q4ye": "체크리스트 이름변경", - "o+ZEL3": "{timestamp}에 발행됨", - "SXJ98n": "회고 보고서를 발행한 후에는 수정할 수 없습니다. 회고 보고서를 발행하시겠습니까?", - "8oCVbz": "", - "wylJpv": "{team}에 속한 전체 멤버가 이 플레이북을 볼 수 있습니다.", - "tVPYMu": "플레이북 관리자", - "gGcNUr": "권한이 없습니다", - "g0mp+I": "비공개 플레이북으로 전환하면 멤버십 및 Run 기록이 보존됩니다. 이 변경 사항은 영구적이며 다시 되돌릴 수 없습니다. {playbookTitle}을(를) 비공개로 전환하시겠습니까?", - "TSSNg/": "지난 12주 동안 주당 시작된 총 실행 횟수", - "SDSqfA": "실행이 시작될 때", - "R/2lqw": "템플릿 선택", - "QpUBDr": "{members, plural, =0 {No one} =1 {One person} other {# people}}명이 이 플레이북을 액세스 할 수 있습니다.", - "OINwWS": "", - "MJ89uW": "비공개 플레이북으로 전환", - "HLn43R": "접근 관리", - "EvBQLq": "플레이북 관리자 만들기", - "EWz2w5": "플레이북 실행", - "ApULhK": "", - "5BUxvl": "이 팀에 있는 모두가 이 플레이북을 볼 수 있습니다.", - "3Ls2m+": "플레이북 멤버", - "0tznw6": "비공개 플레이북으로 변환", - "0Vvpht": "플레이북 멤버 만들기", - "twieZh": "Run 개요로 이동", - "s3jjqi": "{num_actions, plural, =0 {액션 없음} one {# 액션} other {# 액션}}", - "osuP6z": "드래그하여 체크리스트 순서 변경", - "ryrP8K": "이 플레이북을 보고, 수정하고, 실행할 수 있는 사용자 권한을 관리합니다.", - "rbrahO": "닫기", - "jS/UOn": "", - "hrgo+E": "보관", - "Ui6GK/": "", - "5ciuDD": "", - "TxCTXQ": "", - "2563nT": "실행 완료 확인", - "D9IV7i": "", - "5Ofkag": "", - "MrJPOh": "상태 업데이트 사용", - "k1djnL": "체크리스트 삭제", - "iXNbPf": "이름변경", - "YORRGQ": "업데이트 게시", - "I5NMJ8": "더 보기", - "2/2yg+": "추가", - "N2IrpM": "확인", - "MvEydR": "{name}이(가) 상태 업데이트를 게시했습니다", - "XXbWAU": "이 플레이북이 실행될 때 자동으로 업데이트를 받으려면 이 옵션을 선택합니다.", - "LmhSmU": "항목 삭제 확인", - "C6Oghd": "실행 요약 수정하기", - "cp7KUI": "플레이북", - "UMoxP9": "채널 이름 템플릿 (선택 사항)", - "3MSGcL": "채널 이름이 유효하지 않습니다.", - "tzMNF3": "", - "2VrVHu": "실행 이름으로 검색", - "0oL1zz": "복사 되었습니다!", - "v1DNMW": "{name}이(가) 회고를 발행했습니다", - "qyJtWy": "덜 보기", - "q6f8x9": "마지막 업데이트 이후 변경사항", - "pjt3qA": "", - "pKLw8O": "이 이벤트를 삭제하시겠습니까? 삭제된 이벤트는 영구적으로 타임라인에서 제거됩니다.", - "dsTLW1": "", - "d8KvXJ": "평가판 라이선스가 {expiryDate}일에 만료됩니다. 사용중인 기능 중단을 방지하려면 Customer Portal을 통해 언제든지 라이선스를 구매할 수 있습니다.", - "b3TdyZ": "체험판 시작을 클릭하면 Mattermost 소프트웨어 평가 계약, 개인정보 취급정책 그리고 제품 이메일 수신에 동의하게 됩니다.", - "Y+U8La": "", - "K3r6DQ": "", - "Vhnd2J": "설명 토글", - "S0kWcH": "업데이트 기한 초과", - "Nh91Us": "전체 {from, number}–{to, number} of {total, number}", - "JJNc3c": "이전", - "GwtR3W": "", - "7VTSeD": "", - "u4MwUB": "플레이북 Run 이력을 저장합니다", - "kDcpd/": "", - "h+e7G+": "", - "Mm1Gse": "", - "KiXNvz": "실행", - "EQpfkS": "완료", - "CyGaem": "실행 이름", - "Cy1AK/": "", - "Auj1ap": "평가판을 시작하거나 구독을 업그레이드하세요.", - "2QkJ4s": "중요한 메시지를 저장하면 회고를 간소화할 수 있는 전체 그림을 볼 수 있습니다.", - "+Tmpup": "이 플레이북이 실행될 때 자동으로 업데이트를 받습니다.", - "2Qq4YX": "", - "0HT+Ib": "보관됨", - "ZWtlyd": "{name}님이 실행을 복원했습니다", - "wcWpGs": "잘못된 웹훅 URL입니다", - "wZ83YL": "나중에", - "wX3k9U": "", - "w0muFd": "나가는 웹훅 전송(한줄에 하나씩)", - "l0hFoB": "", - "eLeFE2": "", - "dvhvum": "(선택) 이 플레이북을 어떻게 사용해야 하는지 설명하세요", - "djALPR": "", - "YMrTRm": "", - "IfxUgC": "실행 요약 추가…", - "guunZt": "할당", - "gt6BhE": "Run 세부사항", - "bE1Cro": "내 Run만", - "Lg3I1b": "", - "E0LnBo": "", - "xmcVZ0": "검색", - "hfrrC7": "", - "g4IF1x": "이 플레이북에는 Run이 없습니다.", - "fmylXu": "", - "Z7vWDQ": "오류가 발생했습니다", - "YKn+7s": "", - "YDuW/T": "", - "VOzlSL": "플레이북을 실행하면 팀과 도구의 작업 흐름을 조율할 수 있습니다.", - "SmAUf9": "{timestamp}에 알림이 전송됩니다", - "Q8Qw5B": "설명", - "HAlOn1": "이름", - "GxJAK1": "요청하신 플레이북이 비공개이거나 존재하지 않습니다.", - "D2CE02": "웹훅 입력", - "9qc7BX": "", - "x8cvBr": "Run 개요 보기", - "vNiZXF": "", - "uBLF+D": "", - "sqNmlF": "회고 건너뛰기", - "rDvvQs": "{completed, number} / {total, number} 완료", - "oVHn4s": "마지막 업데이트", - "nqVby7": "총 {numTasks, number} {numTasks, plural, =1 {task} other {tasks}} 개중 {numTasksChecked, number}개 작업이 체크됨", - "c8hxKk": "{date}일 주", - "WTQpnI": "", - "WIxhrv": "실행 이름은 두 글자 이상이어야 합니다", - "WAHCT2": "시스템 관리자 알림", - "W1Qs5O": "실행", - "wsUmh9": "", - "C9NScU": "팀을 제어하세요", - "4vuNrq": "실행 시작 후 {duration}", - "/ZsEUy": "", - "aACJNp": "{name}이(가) Run을 시작했습니다", - "1I48bs": "회고 템플릿", - "lrbrjv": "예, 회고를 시작합니다", - "fXGjhC": "{summary}에서 소유자 변경됨", - "QywYDe": "또한 이 실행이 완료된 것으로 처리됩니다", - "/gbqA6": "실행 시작 {duration} 전", - "iDMOiz": "", - "R+JQaJ": "", - "CBM4vh": "다음 업데이트 타이머", - "IOnm/Z": "", - "Q7aZO4": "{numParticipants, plural, =0 {활동하는 참여자 없음} =1 {활동하는 참여자 #명} other {활동하는 참여자 #명}}", - "b5FaCc": "", - "Z/hwEf": "", - "+hddg7": "Run 타임라인에 추가", - "T5rX+W": "", - "M/2yY/": "아직 아무도 없습니다.", - "KJu1sq": "", - "I90sbW": "방금", - "HSi3uv": "담당자 없음", - "G/yZLu": "제거", - "DtCplA": "{numParticipants, plural, =1 {# 명 참여} other {# 명 참여}}", - "DSVJjB": "", - "CkYhdY": "", - "CSts8B": "", - "BNB75h": "플레이북은 반복 가능한 절차에 대한 체크리스트, 자동화 및 템플릿을 규정합니다. {br} 이를 통해 팀은 오류를 줄이고, 이해 관계자의 신뢰를 얻으며, 반복할 때마다 더욱 효과적으로 작업할 수 있습니다.", - "A21Mgv": "실행 완료", - "9TTfXU": "시스템 관리자에게 알림이 전송되었습니다.", - "9PXW6Q": "기간 / 시작일", - "91Hr5f": "재정렬하려면 저를 드래그하세요", - "6n0XDG": "", - "6CGo3o": "상태 / 최근 업데이트", - "5qBEKB": "플레이북 실행이 뭐죠?", - "5j6GD/": "{numParticipants, plural, =0 {참여자 없음} =1 {참여자 #명} other {참여자 #명}}", - "2PNrBQ": "", - "15jbT0": "타임라인에 더 추가하기", - "/YZ/sw": "체험판 시작하기", - "/MaJux": "회고 시작", - "x5Tz6M": "보고", - "o2eHmz": "{name}이(가) Run을 끝냈습니다", - "ieGrWo": "팔로우", - "syEQFE": "발행", - "OsDomv": "모든 이벤트", - "OcpRSQ": "항목 삭제", - "N1U/QR": "작업 상태 변경사항", - "FEGywG": "업데이트 알림을 받을 향후 날짜/시간을 지정해 주세요.", - "DXACD6": "회고 보고서 발행 및 타임라인 액세스", - "ArpdYl": "", - "AML4RW": "작업 할당", - "4Hrh5B": "{name}의 상태가 {summary}에서 변경되었습니다", - "wL7VAE": "액션", - "usa8vQ": "", - "avPeEI": "이 플레이북의 전체 Run, 활성 Run 및 참여자에 대한 추세를 보려면 업그레이드하세요.", - "LRFvqz": "", - "KUr+sG": "", - "Hzwzgs": "", - "CjNrqO": "", - "bLK+Kr": "지정된 간격으로 채널에 회고록을 작성하도록 리마인드합니다.", - "UbTsGY": "{start}와 {end} 사이에 실행이 시작됩니다", - "TZYiF/": "취소선", - "RthEJt": "회고", - "QnZAit": "", - "QiKcO7": "회고 템플릿 입력", - "AF9wda": "", - "vQqT/8": "", - "GjCS6U": "템플릿 선택", - "wPVxBN": "", - "f+bqgK": "지표 이름", - "GAuN6w": "", - "a0hBZ0": "지표 삭제", - "rzbYbE": "목표", - "VZRWFk": "", - "dZmYk6": "플레이북 복제에 성공했습니다", - "mbo96h": "", - "OyZnsJ": "실행당", - "XpDetT": "이 팁을 사용하지 않도록 설정합니다.", - "lgZf0l": "플레이북 시작하기", - "RzEVnf": "플레이북을 사용하면 중요한 절차를 더욱 반복적이고 책임감 있게 진행할 수 있습니다. 플레이북은 여러 번 실행할 수 있으며, 각 실행에는 고유한 기록과 회고 기능이 있습니다.", - "vL4++D": "진행 상황 및 소유권 추적", - "fhMaTZ": "빠르게 둘러보기", - "GG1yhI": "다양한 사용 사례와 이벤트에 대한 템플릿이 있습니다. 플레이북을 그대로 사용하거나 입맛에 맞게 수정한 후에 팀과 공유할 수 있습니다.", - "6GTzTR": "", - "uT4ebt": "예: 자원 수, 영향받는 고객", - "udrLSP": "Run 전반의 패턴과 진행 상황을 이해하고 성과를 추적하려면 지표를 사용하세요.", - "q/Qo8l": "비공개 플레이북은 Mattermost Enterprise에서만 제공됩니다", - "rMhrJH": "지표 제목을 추가하세요.", - "Q5hysF": "플레이북으로 더 많은 작업 수행", - "Sx3lHL": "정수", - "FGzxgY": "예: 알릴 시간, 해결할 시간", - "Tt04f1": "대화에서 나가지 않고도 누가 관련되어 있고 무엇을 해야 하는지 확인할 수 있습니다.", - "y7o4Rn": "정말 삭제할까요?", - "cEWBE3": "", - "I5DYM+": "", - "Q3R9Uj": "", - "bTgMQ2": "이 플레이북은 보관되었습니다.", - "0Xt1ea": "이 지표의 과거 데이터에 계속 접근할 수 있습니다.", - "mVpO8u": "전에도 본 적이 있나요?", - "R5Zh+l": "이렇게 하면 시간을 들여 내 플레이북을 만들기 전에 샘플 플레이북을 먼저 경험할 수 있습니다.", - "Pue+oV": "", - "hw83pa": "주요 지표 추적 및 가치 측정", - "/urtZ8": "", - "1ikfp3": "이 지표를 지우면 이후의 모든 실행에 대해 이 값이 수집되지 않습니다.", - "4alprY": "플레이북 템플릿", - "/fU9y/": "", - "wbdGb5": "작업을 할당, 확인 또는 생략함으로써 함께 팀이 어떻게 목표를 향해 나아갈 수 있을지 명확히 알게 하세요.", - "lUfDe1": "플레이북 Run 채널을 내보내고 나중에 분석할 수 있도록 저장합니다.", - "vJ2SaW": "", - "q/VD+s": "", - "HGdWwZ": "", - "9m0I/B": "", - "dxyZg3": "직접 살펴보겠습니다", - "QbGfqo": "여러 곳의 이해 관계자들에게 알리고 단 하나의 게시물로 회고할 수 있도록 문서 추적을 유지하세요.", - "HXvk56": "상태 업데이트 게시", - "8n24G2": "사이드 패널에서 실행 상세 정보 보기", - "1isgPF": "", - "ZkhArX": "출발!", - "1QosTr": "사용 대상", - "0EEIkR": "", - "NYTGIb": "확인", - "tbjmvS": "같은 이름의 지표가 이미 있습니다. 각 지표마다 고유한 이름을 추가하세요.", - "gsMPAS": "", - "TxmjKI": "이 지표의 내용을 설명하세요", - "NJ9uPu": "주요 지표", - "LI7YlB": "이 지표의 내용 및 입력 방법에 대한 세부 정보를 추가합니다. 이 설명은 이러한 지표의 값을 입력할 각 실행에 대한 회고 페이지에서 확인할 수 있습니다.", - "LDYFkN": "기간 (dd:hh:mm 형식)", - "JrZ2th": "지표 추가", - "F4pfM/": "숫자를 입력하거나 빈 값으로 두세요.", - "9SIW2x": "각 실행의 목표값", - "6D6ffM": "기간을 dd:hh:mm (예: 12:00:00) 형식으로 입력하거나 빈 값으로 놔두세요.", - "4BN53Q": "각 실행의 값이 목표에 얼마나 가까워졌는지 또는 얼마나 멀어졌는지 보여드리고 차트에 표시해 드립니다.", - "xvBDOH": "{title} 플레이북을 보관할까요?", - "lBqu4h": "플레이북 복원", - "MTzF3S": "플레이북 {title}을(를) 복원할까요??", - "4cwL43": "보관 포함", - "4aupaG": "{title} 플레이북이 복원되었습니다.", - "SVwJTM": "내보내기", - "9XUYQt": "들여오기", - "4fHiNl": "복제", - "3PoGhY": "발행하시겠어요?", - "l5/RKZ": "이 플레이북은 모든 Run을 완료했습니다.", - "Vf/QlZ": "값 범위", - "KXVV4+": "", - "xVyHgP": "테스트 Run 시작", - "M4gAc9": "값 추가", - "mvZUm3": "", - "ru+JCk": "평균값", - "NMxVd+": "지표 값을 입력하세요.", - "fmbSyg": "값 추가(dd:hh:mm 형식)", - "69nlA3": "기간을 다음 형식으로 입력해주세요: dd:hh:mm (예: 12:00:00).", - "NiAH1z": "목표값", - "9a9+ww": "제목", - "ZNNjWw": "숫자를 입력해주세요.", - "efeNi1": "10-Run 평균값", - "NLeFGn": "(으)로", - "awG90C": "Run당 목표", - "lbs7UO": "지난 10개 Run을 기준으로 Run당", - "zxj2Gh": "최종 갱신됨 {time}", - "zWgbGg": "오늘", - "5ZIN3u": "상태 업데이트", - "7P5T3W": "체크리스트 복원하기", - "5Hzwqs": "즐겨찾기", - "/GCoTA": "지우기", - "//o1Nu": "업데이트 비활성화", - "+qDKgW": "모든 업데이트 보기", - "+/x2FM": "플레이북을 선택하세요", - "YQOmSf": "한 줄에 하나의 웹훅을 입력합니다", - "Z18I+c": "채널 액션을 통해 채널의 활동을 자동화할 수 있습니다", - "Z2Hfu4": "실행 요약 추가", - "ZRv7Dm": "참여 요청하기", - "Zg0obP": "실행 재시작", - "YKLHXL": "진행 중인 실행 보기", - "YBvwXR": "할당된 작업 없음", - "XnICdK": "실행에 참여할 수 없습니다", - "Xgxruo": "체크리스트 건너뛰기", - "XRyRzf": "예기치 못한 상태 업데이트입니다.", - "XF8rrh": "\"{name}\"의 링크 복사", - "WFd88+": "체크된 작업 표시", - "WC+NOj": "또한 이 실행에 연결된 채널에 사람들을 추가하세요", - "W1EKh5": "새 플레이북 만들기", - "VjJYEV": "예: 매출 영향, 구매", - "VA1Q/S": "공개 채널", - "UMFnWV": "회고 보기", - "TnUG7m": "보류 중인 담당 작업이 없습니다.", - "TTIQ6E": "작업에 마감일을 지정하여 담당자가 우선 순위를 정하고 작업을 완료할 수 있도록 하세요.", - "SwlL5j": "@{user}님이 실행에 참여했습니다", - "Suyx6A": "플레이북을 가져오지 못했습니다. JSON이 유효한지 확인한 후 다시 시도해보세요.", - "SRbTcY": "다른 플레이북", - "SMrXWc": "즐겨찾기", - "RrCui3": "요약", - "RC6rA2": "최근 생성", - "QegBKq": "플레이북 참여", - "QJTSaI": "다른 채널로 실행을 링크", - "Q15rLN": "업데이트 요청...", - "Ppx673": "보고서", - "PoX2HN": "요청 보내기", - "PdRg+3": "모두 보기...", - "PW+sL4": "해당 없음", - "P6PLpi": "참여", - "OuZhcQ": "기간 지정 (\"8 hours\", \"3 days\"...)", - "OqWwvQ": "{user}님이 체크리스트의 \"{name}\" 항목을 체크 해제했습니다", - "OfN7IN": "상태 업데이트 요청이 실행 채널로 전송됩니다.", - "MBNMo9": "채널 액션", - "M9tXoZ": "실행 채널로 참여 요청이 전송됩니다.", - "LaseGE": "이 체크리스트를 편집할 수 있는 권한이 없습니다", - "L6vn9U": "실행 참여자", - "L1tFef": "맞춤법을 확인하거나 다른 검색을 시도해 보세요", - "KzHQCQ": "해당 필터와 일치하는 완료된 실행이 없습니다.", - "KeO51o": "채널", - "KQunC7": "이 채널에서 사용됨", - "IxtSML": "체크리스트 추가", - "IdTL+v": "실행 채널 만들기", - "I0NIMp": "내 작업", - "HfjhwE": "플레이북 검색", - "Gwmqz5": "업데이트 요청", - "Gg/nch": "참여하지 않고 있음", - "GDCpPr": "최근 상태 업데이트", - "F9LrJA": "항목 필터링", - "Ek1Fx2": "다음 키워드가 포함된 메시지가 게시되는 경우", - "Edy3wX": "체크리스트가 {channel}로 이동됨", - "DaHpK1": "채널에서 검색", - "DKiv0o": "{user}님이 체크리스트 목록의 \"{name}\" 항목을 건너뛰었습니다", - "CwwzAU": "체크리스트 이름 추가", - "CUhlqp": "튜토리얼 투어 팁 제품 이미지", - "CFysvS": "플레이북 드롭다운 만들기", - "Brya9X": "실행 요약 템플릿 추가…", - "BiQjuS": "실행이 {channel} 채널로 이동됨", - "BJNrYQ": "참가자는 실행 요약을 업데이트하고, 작업을 체크하고, 상태 업데이트를 게시하고, 회고를 편집할 수 있습니다.", - "B3Q5mz": "트리거", - "AoNLta": "이 채널에 연결된 완료된 실행이 없습니다", - "9w0mDI": "사전 할당된 구성원 제거 확인", - "9trZXa": "팀의 누구나 볼 수 있음", - "9M92On": "채널 선택", - "9AQ5FE": "실행 요약", - "8FzC0B": "{user}님이 체크리스트에 있는 \"{name}\" 항목을 선택 해제했습니다", - "8//+Yb": "체크리스트를 다른 채널에 연결", - "7KMbBa": "사용된 적 없음", - "706Soh": "작업 완료", - "5b1zuB": "실행 채널에 추가", - "3zF589": "모든 {filterName} 초기화", - "3sXVwy": "작업 조치...", - "3Yvt4d": "플레이북은 팀이 구체적이고 예측 가능한 결과를 달성할 수 있도록 반복 가능한 프로세스를 정의하는 구성 가능한 체크리스트입니다", - "2Q5PhZ": "플레이북을 실행하기 위한 프롬프트", - "2NDgJq": "마지막 상태 업데이트", - "1OluNs": "상태 업데이트 활성화 확인", - "0RlzlZ": "사용자에게 임시 환영 메시지 보내기", - "0QD99o": "채널 가입 요청", - "0CeyUV": "\"{searchTerm}\"에 대한 결과가 없습니다", - "0Azlrb": "관리", - "03oqA2": "활동중인 Run", - "/qDObA": "실행 보기", - "/RnCQb": "나가는 웹훅 보내기", - "/+8SGX": "{totalNum}개의 이벤트 중의 {filteredNum}개를 표시 중", - "Z1sgPO": "완료된 실행 보기", - "Z3ybv/": "사용자를 위한 사이드바 카테고리에 채널 추가하기", - "ZJS10z": "아직 게시된 업데이트가 없습니다", - "Zbk+OU": "파일 크기가 5MB 제한을 초과합니다.", - "Y1EoT/": "참가자가 이 실행을 떠나는 경우", - "Xx0WZV": "메시지 보내기", - "WFA0Cg": "이 실행에 대해 상태 업데이트를 사용하도록 설정하시겠습니까?", - "Ul0aFX": "플레이북 들여오기", - "UAS7Bn": "이 실행에 연결된 채널에 대한 접근 요청", - "TP/O/b": "사용자 제거", - "TD8WrM": "이 팀에서는 복제가 비활성화되어 있습니다.", - "SK5APX": "실행을 떠날 수 없습니다.", - "RgQwWr": "실행 정렬", - "QvEO6m": "이 실행을 수정하기 위한 권한이 없습니다", - "Q/t0//": "완료된 실행", - "PWmZrW": "모든 실행 보기", - "P6NEL/": "명령...", - "OqCzNb": "작업 추가", - "LfhTNW": "플레이북 및 실행 찾아보기 또는 만들기", - "KjNfA8": "유효하지 않은 시간 간격", - "JcefuP": "설명 추가 (선택 사항)", - "I7+d55": "날짜/시간 지정 (“in 4 hours”, “May 1”...)", - "HGSVzc": "한번에 다수의 파일을 들여올 수 없습니다.", - "H7IzRB": "상태 업데이트 비활성화", - "GZoWl1": "이 작업에 대한 활동 자동화", - "GXjP8g": "접근할 수 있는 모든 실행이 여기에 표시됩니다", - "GVpA4Q": "새 플레이북 만들기", - "FLG4Iu": "실행 소유자 만들기", - "EVSn9A": "실행 시작", - "DqTQOp": "한 번", - "AhY0vJ": "떠난 후 언팔로우", - "AG7PKJ": "실행 이름 변경", - "AF7+5o": "목표일 추가", - "9xs0pp": "값 추가...", - "9qqGGd": "참가자 초대", - "9kQNdp": "이 플레이북은 비공개입니다.", - "9j5KzL": "카테고리 이름 입력", - "6rygzu": "실행에서 제거", - "5HXkY/": "유형: {typeTitle}", - "5AJmOz": "사용자가 채널에 참여할 때", - "4mCpAv": "소유자를 변경할 수 없습니다", - "4Iqlfe": "이 실행에 참여했습니다.", - "4GjZsL": "전체 플레이북", - "3qPQMX": "{name}님이 상태 업데이트를 요청했습니다", - "3hBelc": "회고가 예정되어 있지 않습니다.", - "36NwLv": "실행 참가자 목록 관리", - "2BCWLD": "채널 설정", - "28FTjr": "실행 작업을 통해 이 채널의 활동을 자동화할 수 있습니다", - "1prgB2": "멤버 검색", - "1fXVVz": "마감일...", - "1GOpgL": "담당자...", - "FgydNe": "보기", - "DUU48k": "명시적으로 할당된 작업이 없습니다. 필터를 사용하여 검색을 확장할 수 있습니다.", - "DQn9Uj": "사용자 {name} 은(는) 하나 이상의 작업에 사전 할당되어 있습니다. 이 사용자를 자동으로 초대하지 않으면 사전할당이 지워집니다. {br} 정말로 이 사용자에 대한 Run 멤버 초대를 중단할까요?", - "Bgt0C8": "{runName} 런에 대한 업데이트가 {hasChannels, select, true {{broadcastChannelCount, plural, =1 {one channel} other {{broadcastChannelCount, number} channels}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {one direct message} other {{followersChannelCount, number} direct messages}}} other {}}에 전파 됩니다.", - "9X3jwi": "{icon} 비용", - "NNksk4": "알파벳순", - "NGKqOC": "또한 이 Run에 연결된 채널에 저를 추가하세요", - "NFyWnZ": "더 효율적으로 일하세요", - "MyIJbr": "콘텐츠", - "MtrTNy": "내일", - "Mjq//Y": "즐겨찾기 취소", - "MieztS": "가져올 플레이북 내보내기 파일을 드롭하세요.", - "MbapTE": "마감일을 초과한 작업 {num} {num, plural, =1 {task} other {tasks}}개", - "MHzP9I": "채널 참여 사용자에 대한 환영 메시지를 정의하세요.", - "LKu0ex": "모든 참여자를 위해 {runName} Run을 끝내시겠습니까?", - "IE2BzH": "하나 이상의 작업에 사전 할당된 사용자가 있습니다. 초대를 비활성화하면 모든 사전 할당이 지워집니다.{br}{br}초대를 비활성화하시겠습니까?", - "OQplDX": "상태는 매 마다 업데이트 됩니다. 새 업데이트는 {channelCount, plural, =0 {no channels} one {# channel} other {# channels}}채널과 {webhookCount, plural, =0 {no outgoing webhooks} one {# outgoing webhook} other {# outgoing webhooks}} 웹훅에 게시됩니다.", - "ch4Vs1": "클릭 한 번으로 플레이북 Run업데이트를 요청하고 업데이트 시 바로 알림을 받습니다. 30일 무료 체험을 시작하고 사용해보세요.", - "cUCiWw": "참여하기", - "cGCoJe": "게시자", - "c23IHq": "채널 액션으로 이 채널의 활동을 자동화할 수 있습니다", - "bf5rs0": "정보 보기", - "bEoDyV": "@{authorUsername}이(가) [{runName}]({overviewURL})에 대한 업데이트를 게시했습니다", - "bCmvTY": "피드백 하기", - "b8Gps8": "{name}이(가) Run 상태 업데이트를 활성화 했습니다", - "aZGAOI": "상태 업데이트 템플릿 추가…", - "aEhjYg": "개요", - "a2r7Vb": "비공개 채널", - "ZSa3cf": "@{targetUsername}님, [{runName}]({playbookURL}) 상태를 업데이트 해주세요.", - "XS4umx": "{name}이(가) 상태 업데이트를 잠시 멈췄습니다", - "XHJUSG": "자동 팔로우 Run들", - "VM75su": "{name}이(가) {num}명의 참여자를 Run에서 제거했습니다", - "RnOiCg": "Run을 {isFollowing, select, true {unfollow} other {follow}} 할 수 없었습니다", - "RXjd3Q": "{name}이(가) @{user}을(를) Run에서 제거했습니다", - "RQl8IW": "다음 시간 동안 알림 중지…", - "Q4sutg": "나가기 확인{isFollowing, select, true { and unfollow} other {}}", - "Ob5cSv": "이 페이지를 나가면 변경 사항이 저장되지 않습니다. 변경 사항을 버리고 나갈까요?", - "lqceIp": "또는 플레이북 가져오기", - "lkv547": "마감일(Professional Plan에서 제공)", - "lbr3Lq": "링크 복사", - "lKeJ+i": "요약이 없습니다", - "lJ48wN": "비공개 플레이북", - "l3QwVw": "채널을 선택하세요", - "l/W5n7": "참가자는 또한 이 Run에 연결된 채널에 추가됩니다", - "ksG35Q": "이 워크스페이스에 플레이북을 생성할 권한이 없습니다.", - "kYCbJE": "기간 추가", - "kV5GkX": "상태 업데이트가 게시될 때", - "kQAf2d": "선택", - "kEMvwX": "해당 필터와 일치하는 Run이 없습니다.", - "k7Nzfi": "초대 비활성화", - "k5EChD": "Run을 다시 시작할까요?", - "jrOlPO": "Run 상태 업데이트 알림 받기", - "jfpnye": "@{user}이(가) Run에서 나갔습니다", - "jAo8dd": "{name}이(가) Run 상태 업데이트를 비활성화 했습니다", - "j940pJ": "이 업데이트에 대한 주요 변경사항은 overview page에서 볼 수 있습니다.", - "j2VYGA": "전체 플레이북 보기", - "izWS4J": "팔로우 취소", - "iigkp8": "이제 정리할 시간인가요?", - "iQhFxR": "마지막으로 사용", - "iMjjOH": "다음 주", - "iH5e4J": "또한 이 Run에 연결된 채널에도 추가됩니다.", - "iEtImk": "{isFollowing, select, true { and unfollow a run} other { a run}}을(를) 나가면 왼쪽 사이드바에서도 제거됩니다. 전체 Run 보기에서 다시 찾을 수 있습니다.", - "hjteuA": "액세스할 수 있는 모든 플레이북이 여기에 표시됩니다", - "ha1TB3": "Run에 참여자가 생겼을 때", - "grv9Fm": "작업 목록을 전환하려면 선택하세요.", - "gfUBRi": "Run에서 나가기 전에 새로운 소유자를 지정하세요.", - "gS1i4/": "작업을 완료로 표시", - "gGtlrk": "당신의 플레이북", - "g9pEhE": "마감시한", - "fwW0T1": "사전 할당된 멤버 제거 확인", - "fvNMLo": "작업 액션", - "fnihsY": "나가기", - "feNxoJ": "{requester}이(가) {users}을(를) Run에 추가했습니다", - "fVMECF": "참여자", - "fBG/Ge": "비용", - "ecS/qx": "{name}이(가) {num}명의 참여자를 Run에 추가했습니다", - "ePhhuK": "요청이 Run 채널로 전송되었습니다.", - "e3z3P8": "무시하고 나가기", - "dK2JKl": "기존 채널에 링크", - "cyR7Kh": "돌아가기", - "cpGAhx": "이 Run에 대한 상태 업데이트를 비활성화할까요?", - "cnfVhV": "{isFollowing, select, true { and unfollow } other {}} Run에서 나가기", - "zl6378": "회고용 지표 설정", - "zW/5AB": "Professional 기능 유료 기능입니다. 30일 무료 평가판을 사용할 수 있습니다", - "zSOvI0": "필터", - "yllba1": "이 보관된 플레이북 이름을 변경할 수 없습니다.", - "yP3Ud4": "이 채널에 연결되어 진행 중인 Run을 찾을 수 없습니다", - "xfnuXm": "참여", - "xHNF7i": "Run 액션", - "xEQYo5": "회고 보고서에 포함 시킬 사용자 지정 지표를 구성합니다.", - "x1phlu": "일정 계획 없음", - "wRM2AO": "업데이트 요청이 실패했습니다.", - "wCDmf3": "업데이트 활성화", - "wBZz47": "Run에서 나왔습니다.", - "w4Nhhb": "참여자 추가", - "vqmRBs": "Run 다시시작 확인", - "vjb+hS": "{user}이(가) 체크리스트 항목 \"{name}\"을(를) 복원했습니다", - "vSMfYU": "Run 정보", - "vDvWJ6": "무료 평가판으로 업데이트 요청", - "v5/Cox": "체크리스트 복제", - "utHl3F": "{runName}에 인원 추가", - "unwVil": "채널 가입 요청에 실패했습니다.", - "uYrkxy": "파일은 반드시 올바른 JSON 플레이북 템플릿 형식이어야 합니다.", - "uCS6py": "이 플레이북을 볼 수 있는 권한이 없습니다", - "u4L4yd": "저장하지 않은 변경사항이 있습니다", - "u/yGzS": "{name}이(가) @{user}을(를) Run에 추가했습니다", - "tqAmbk": "진행중인 Run", - "t6lwwM": "{requester}이(가) {users}을(를) Run에서 제거했습니다", - "sX5Mn5": "한 줄에 하나씩 웹훅을 입력하세요", - "sGJpuF": "설명 추가…", - "s+rSpl": "{icon} 정수", - "qxYWTy": "내가 소유한 Run의 모든 작업 표시", - "qGlwfc": "Run 시작", - "qDxsQH": "이 Run에 참여하기", - "q48ca7": "플레이북에 대한 피드백을 제출합니다.", - "pzTOmv": "팔로워", - "prs4kX": "특정 키워드를 포함한 메시지가 게시될 때", - "pFK6bJ": "전체 보기", - "p1I/Fx": "Run을 자동 생성했습니다", - "opn6uf": "타임라인 보기", - "ojQue/": "{icon} 기간 (dd:hh:mm 형식)", - "ocYb9S": "주요 지표", - "oL7YsP": "{timestamp}에 마지막으로 수정됨", - "oBeKB4": "{date}에 마감", - "oAJsne": "공개 플레이북", - "o6N9pU": "Run 액션", - "nsd54s": "상태 업데이트 비활성화 확인", - "nc8QpJ": "최근 활동", - "mw9jVA": "제목 추가", - "mm5vL8": "초대받은 멤버 전용", - "mkLeuq": "선택한 채널로 업데이트 알림", - "meD+1Q": "RUN 참여자들", - "mNgqXf": "이 기능을 잠금 해제하려면:", - "mLrh+0": "마감일 없음", - "mILd++": "Run 이름은 {maxLength} 글자를 초과하면 안됩니다", - "mCrdeS": "전체 플레이북 Run", - "m8hzTK": "마지막 사용 {time}", - "m4vqJl": "파일", - "m/KtHt": "소유자 변경 권한이 없습니다", - "lyXljU": "작업 복제", - "lr1CUA": "플레이북 찾아보기", - "lqzBNa": "Run 채널에서 제거", - "Wy3sw+": "{count, plural, =1{1개의 진행 중인 Run} =0 {진행 중인 Run 없음} other {#개의 진행 중인 Run}}", - "UePrSL": "{num} {num, plural, one {참여자} other {참여자}}", - "SRqpbI": "{assignedNum, plural, =0 {할당된 작업 없음} other {#개의 작업 할당됨}}", - "HvAcYh": "{text}{rest, plural, =0 {} one { 그리고 그외} other { 그리고 그외 {rest}}}", - "CgAtTJ": "{overdueNum, plural, =0 {} other {시한초과 #개}}", - "95v+5O": "{actions, plural, =0 {작업 액션} one {#개 액션} other {#개 액션}}", - "zscc/+": "{outstanding, plural, =1 {#개의 미완료 작업이} other {#개의 미완료 작업들이}} 있습니다. 전체 참여자를 위해 {runName}을(를) 끝내시겠습니까?", - "N7Ln74": "다시 Run", - "DoskyC": "모든 팀", - "9S+ZiL": "선택한 팀이 없습니다" -} diff --git a/webapp/playbooks/i18n/ml.json b/webapp/playbooks/i18n/ml.json deleted file mode 100644 index 6741704a6df..00000000000 --- a/webapp/playbooks/i18n/ml.json +++ /dev/null @@ -1,459 +0,0 @@ -{ - "9kCT7Q": "പ്രധാന ഇവന്റുകളുടെയും സന്ദേശങ്ങളുടെയും ട്രാക്ക് സ്വയമേവ സൂക്ഷിക്കുന്ന ടൈംലൈൻ ഉപയോഗിച്ച് റിട്രോസ്‌പെക്‌റ്റീവുകൾ എളുപ്പമാക്കുക, അതുവഴി ടീമുകൾക്ക് അത് അവരുടെ വിരൽത്തുമ്പിൽ ലഭിക്കും.", - "9TTfXU": "നിങ്ങളുടെ സിസ്റ്റം അഡ്‌മിന് അറിയിപ്പ് ലഭിച്ചു.", - "9PXW6Q": "ദൈർഘ്യം / ആരംഭിച്ചത്", - "9uOFF3": "അവലോകനം", - "8hDbW6": "ഒരു ഔട്ട്‌ഗോയിംഗ് വെബ്‌ഹുക്ക് അയയ്‌ക്കുക", - "6uhSSw": "ഒരു ചാനൽ തിരഞ്ഞെടുക്കുക", - "6n0XDG": "ചെക്ക്‌ലിസ്റ്റ് നീക്കം ചെയ്യണമെന്ന് തീർച്ചയാണോ? എല്ലാ ജോലികളും നീക്കം ചെയ്യപ്പെടും.", - "6jDabx": "നിർദ്ദേശങ്ങൾ നൽകുക", - "5Ot7cd": "ഈ പ്ലേബുക്ക് സൃഷ്ടിക്കുന്ന ചാനലിന്റെ തരം നിർണ്ണയിക്കുക.", - "2Qq4YX": "നിങ്ങളുടെ മാറ്റങ്ങൾ നിരസിക്കണമെന്ന് തീർച്ചയാണോ?", - "2QkJ4s": "റിട്രോസ്‌പെക്റ്റീവുകൾ കാര്യക്ഷമമാക്കുന്ന ഒരു പൂർണ്ണ ചിത്രത്തിനായി പ്രധാനപ്പെട്ട സന്ദേശങ്ങൾ സംരക്ഷിക്കുക.", - "5A46pW": "ഒരു സ്ലാഷ് കമാൻഡ് ചേർക്കുക", - "4ltHYh": "പ്ലേബുക്കിലേക്ക് പോകുക", - "42qmJ5": "ഒരു അപ്‌ഡേറ്റ് പോസ്‌റ്റ് ചെയ്യാൻ നിങ്ങൾക്ക് അനുമതിയില്ല.", - "3rCdDw": "സ്റ്റാറ്റസ് അപ്ഡേറ്റുകൾ", - "3Psa+5": "കീവേഡുകൾ ചേർക്കുക", - "3/wF0G": "സ്ലാഷ് കമാൻഡുകൾ", - "2VrVHu": "റൺ നാമം ഉപയോഗിച്ച് തിരയുക", - "/jUtaM": "കഴിഞ്ഞ 14 ദിവസങ്ങളിൽ പ്രതിദിനം സജീവമായ റണ്ണുകൾ", - "/YZ/sw": "ട്രയൽ ആരംഭിക്കുക", - "HAlOn1": "പേര്", - "G/yZLu": "നീക്കം ചെയ്യുക", - "DnBhRg": "ആളുകളെ ചേർക്കുക", - "DXACD6": "മുൻകാല റിപ്പോർട്ട് പ്രസിദ്ധീകരിക്കുകയും ടൈംലൈൻ ആക്സസ് ചെയ്യുകയും ചെയ്യുക", - "DSVJjB": "നിലവിൽ {playbookTitle} പ്ലേബുക്ക് പ്രവർത്തിക്കുന്നു", - "DCl7Vv": "ഇൻലൈൻ കോഡ്", - "D55vrs": "നിങ്ങളുടെ ലൈസൻസ് സൃഷ്ടിക്കാൻ കഴിഞ്ഞില്ല", - "D3idYv": "ക്രമീകരണങ്ങൾ", - "D2CE02": "വെബ്ഹുക്ക് നൽകുക", - "CyGaem": "റൺ പേര്", - "Cy1AK/": "റൺ വിശദാംശങ്ങൾ കാണുക", - "CkYhdY": "ഒരു സൈഡ്‌ബാർ വിഭാഗത്തിലേക്ക് ചാനൽ ചേർക്കുക", - "CjNrqO": "റിട്രോസ്പെക്റ്റീവ് റിപ്പോർട്ട് ടെംപ്ലേറ്റ്", - "CSts8B": "ടീം ഐക്കൺ", - "CL5OZP": "നിങ്ങൾ തിരഞ്ഞെടുക്കുന്ന ഉപയോക്താക്കൾക്ക് മാത്രമേ ഈ പ്ലേബുക്ക് എഡിറ്റ് ചെയ്യാനോ പ്രവർത്തിപ്പിക്കാനോ കഴിയൂ.", - "CBM4vh": "അടുത്ത അപ്ഡേറ്റിനുള്ള ടൈമർ", - "C9NScU": "നിങ്ങളുടെ ടീമിനെ നിയന്ത്രണത്തിലാക്കുക", - "C1khRR": "പ്ലേബുക്കുകളിലേക്ക് മടങ്ങുക", - "BQtd5I": "പ്ലേബുക്ക്-ലേക്ക് സ്വാഗതം!", - "BNB75h": "ഒരു പ്ലേബുക്ക് ചെക്ക്‌ലിസ്റ്റുകളും ഓട്ടോമേഷനുകളും ടെംപ്ലേറ്റുകളും ആവർത്തിക്കാവുന്ന നടപടിക്രമങ്ങൾക്കായി നിർദ്ദേശിക്കുന്നു. {br} ഇത് ടീമുകളെ പിശകുകൾ കുറയ്ക്കാനും, പങ്കാളികളുമായി വിശ്വാസം നേടാനും, ഓരോ ആവർത്തനത്തിലും കൂടുതൽ ഫലപ്രദമാകാനും സഹായിക്കുന്നു.", - "BD66u6": "ചാനലിൽ നിന്നുള്ള എല്ലാ സന്ദേശങ്ങളും അടങ്ങുന്ന ഒരു CSV ഡൗൺലോഡ് ചെയ്യുക", - "B487HA": "പുരോഗതിയിൽ", - "Auj1ap": "ഒരു ട്രയൽ ആരംഭിക്കുക അല്ലെങ്കിൽ നിങ്ങളുടെ സബ്സ്ക്രിപ്ഷൻ അപ്ഗ്രേഡ് ചെയ്യുക.", - "ArpdYl": "ടൈംലൈൻ ഇവന്റുകൾ സംഭവിക്കുമ്പോൾ അവ ഇവിടെ പ്രദർശിപ്പിക്കും. ഒരു ഇവന്റ് നീക്കം ചെയ്യാൻ അതിന് മുകളിൽ ഹോവർ ചെയ്യുക.", - "ApULhK": "അംഗങ്ങളെ ക്ഷണിക്കുക", - "AT2QBo": "തിരഞ്ഞെടുത്ത ഉപയോക്താക്കൾക്ക് മാത്രമേ പ്ലേബുക്കുകൾ സൃഷ്ടിക്കാൻ കഴിയൂ.", - "AS5kar": "പങ്കെടുക്കുന്നവർ ({participants})", - "91Hr5f": "പുനഃക്രമീകരിക്കാൻ എന്നെ വലിച്ചിടുക", - "9+Ddtu": "അടുത്തത്", - "5CI3KH": "പിന്തുണക്കായി ബന്ധപ്പെടുക", - "47FYwb": "നിര്‍ത്തലാക്കല്‍", - "15jbT0": "നിങ്ങളുടെ ടൈംലൈനിലേക്ക് കൂടുതൽ ചേർക്കുക", - "0wJ7N+": "കര്‍ത്തവ്യം", - "0oLj/t": "വികസിപ്പിക്കുക", - "4Hrh5B": "{name} {summary} എന്നതിൽ നിന്ന് സ്റ്റാറ്റസ് മാറ്റി", - "2PNrBQ": "നിങ്ങളുടെ പ്ലേബുക്ക് റണ്ണിന്റെ ചാനൽ എക്‌സ്‌പോർട്ടുചെയ്‌ത് പിന്നീടുള്ള വിശകലനത്തിനായി സംരക്ഷിക്കുക.", - "0HT+Ib": "ആർക്കൈവ് ചെയ്തു", - "+ZIXOR": "ചാനൽ ആക്സസ്", - "/MaJux": "റിട്രോസ്പെക്റ്റീവ് ആരംഭിക്കുക", - "/1FEJW": "കഴിഞ്ഞ 14 ദിവസങ്ങളിൽ പ്രതിദിനം സജീവ പങ്കാളികൾ", - "7VTSeD": "ഈ ടാസ്‌ക് ഒഴിവാക്കണമെന്ന് തീർച്ചയാണോ? ഇത് ഈ ഓട്ടത്തിൽ നിന്ന് മറികടക്കുമെങ്കിലും പ്ലേബുക്കിനെ ബാധിക്കില്ല.", - "5qBEKB": "പ്ലേബുക്ക് റൺ എന്താണ്?", - "5FRgqE": "ചാനൽ ലോഗ് ഡൗൺലോഡ് ചെയ്യുന്നു", - "6Lwe7T": "{team} എല്ലാവർക്കും ഈ പ്ലേബുക്ക് ആക്‌സസ് ചെയ്യാൻ കഴിയും", - "6CGo3o": "സ്റ്റാറ്റസ് / അവസാന അപ്ഡേറ്റ്", - "36GNZj": "പ്ലേബുക്ക് {title} വിജയകരമായി ആർക്കൈവ് ചെയ്തു.", - "/4tOwT": "ഒഴിവാക്കുക", - "5wqhGy": "", - "EWz2w5": "പ്ലേബുക്ക് പ്രവർത്തിപ്പിക്കുക", - "HLn43R": "", - "lbhO3D": "", - "0tznw6": "", - "5j6GD/": "", - "1I48bs": "", - "ICqy9/": "", - "D9IV7i": "", - "A21Mgv": "", - "M/2yY/": "", - "wEQDC6": "എഡിറ്റ് ചെയ്യുക", - "5BUxvl": "", - "g0mp+I": "", - "l0hFoB": "", - "v1DNMW": "", - "Q7aZO4": "", - "ruJGqS": "പ്ലേബുക്ക് ആക്സസ്", - "IuFETn": "", - "jIIWN+": "", - "s3jjqi": "", - "jXT2++": "", - "tVPYMu": "പ്ലേബുക്ക് അഡ്മിൻ", - "FXCLuZ": "{total, number} ആകെ", - "9Obw6C": "", - "jnmORb": "", - "uny3Zy": "പ്ലേബുക്കുകൾ", - "fUEpLA": "", - "jIgqRa": "", - "lrbrjv": "", - "lZwZi+": "", - "k9q07e": "", - "zINlao": "ഉടമ", - "ypIsVG": "ചുമതല പുനഃസ്ഥാപിക്കുക", - "wL7VAE": "പ്രവർത്തനങ്ങൾ", - "vaYTD+": "", - "fuDLDJ": "", - "eLeFE2": "", - "dSC1YD": "", - "d4g2r8": "", - "UMoxP9": "", - "Oo5sdB": "", - "NA7Cw1": "", - "JCGvY/": "", - "qsr3Zk": "", - "X2K92H": "", - "TZYiF/": "", - "SmAUf9": "", - "SENRqu": "", - "Qrl6bQ": "", - "A8dbCS": "", - "0q+hj2": "", - "wylJpv": "{team} എല്ലാവർക്കും ഈ പ്ലേബുക്ക് കാണാനാകും.", - "o+ZEL3": "", - "lQT7iD": "", - "k1djnL": "", - "gGcNUr": "", - "SXJ98n": "", - "8oCVbz": "", - "w0muFd": "ഔട്ട്‌ഗോയിംഗ് വെബ്‌ഹുക്ക് അയയ്‌ക്കുക (ഒരു വരിയിൽ ഒന്ന്)", - "sDKojV": "", - "R/2lqw": "", - "QpUBDr": "", - "EvBQLq": "പ്ലേബുക്ക് അഡ്മിൻ ആക്കുക", - "3Ls2m+": "", - "0Vvpht": "", - "wO6NOM": "ഈ ടാസ്‌ക് പുനഃസ്ഥാപിക്കണമെന്ന് തീർച്ചയാണോ? ഈ ടാസ്ക് ഈ റണ്ണിലേക്ക് ചേർക്കും", - "osuP6z": "", - "yhU1et": "ചുമതലകൾ", - "xmcVZ0": "", - "x8cvBr": "", - "x5Tz6M": "റിപ്പോർട്ട് ചെയ്യുക", - "wsUmh9": "ടീം", - "vjzpnC": "ആ ഫിൽട്ടറുകളുമായി പൊരുത്തപ്പെടുന്ന പ്ലേബുക്കുകളൊന്നുമില്ല.", - "usa8vQ": "ഒരു സ്വാഗത സന്ദേശം അയയ്ക്കുക", - "iNU1lj": "", - "hVFgh4": "", - "e/AZL5": "", - "dvhvum": "", - "b40Pr7": "", - "b3TdyZ": "", - "Lo10yH": "", - "u4MwUB": "നിങ്ങളുടെ പ്ലേബുക്ക് റൺ ചരിത്രം സേവ് ചെയുക", - "qyJtWy": "", - "pK6+CW": "", - "iDMOiz": "", - "hfrrC7": "", - "fpuWL1": "", - "eHAvFf": "", - "iXNbPf": "", - "XmUdvV": "", - "TxCTXQ": "", - "QywYDe": "", - "GwtR3W": "", - "5Ofkag": "", - "nSFBC2": "", - "Ja1sVR": "", - "QnZAit": "", - "QiKcO7": "", - "QaZNp9": "", - "Q8Qw5B": "", - "Ietscn": "", - "I90sbW": "", - "GRTyvN": "", - "2/2yg+": "", - "/ZsEUy": "", - "tzMNF3": "", - "sQu1rA": "", - "sIX63S": "നിങ്ങളുടെ സിസ്റ്റം അഡ്‌മിന് അറിയിപ്പ് ലഭിച്ചു", - "ryrP8K": "ഈ പ്ലേബുക്ക് ആർക്കൊക്കെ കാണാനും പരിഷ്‌ക്കരിക്കാനും പ്രവർത്തിപ്പിക്കാനും കഴിയും എന്നതിനുള്ള അനുമതി മാനേജ് ചെയ്യുക.", - "recCg9": "അപ്ഡേറ്റുകൾ", - "O8o2lE": "", - "yxguVq": "മാറ്റങ്ങൾ ഉപേക്ഷിക്കുക", - "yqpcOa": "ഉപയോഗിക്കുക", - "YMrTRm": "", - "W/V6+Y": "", - "VmnoW8": "", - "VOzlSL": "", - "4vuNrq": "", - "/gbqA6": "", - "cPIKU2": "", - "nmpevl": "", - "kvgvNW": "", - "kGI46P": "", - "kDcpd/": "", - "Q7hMnp": "", - "C6Oghd": "", - "cp7KUI": "", - "T5rX+W": "", - "RO+BaS": "", - "HSi3uv": "", - "3MSGcL": "", - "uhu5aG": "", - "uBLF+D": "എന്താണ് ഒരു പ്ലേബുക്ക്?", - "jwimQJ": "", - "jvo0vs": "", - "jS/UOn": "", - "j7jdWG": "", - "TdTXXf": "", - "MFpAtm": "", - "0oL1zz": "പകർത്തി!", - "zz6ObK": "പുനഃസ്ഥാപിക്കുക", - "z3B83t": "", - "YORRGQ": "", - "YKn+7s": "", - "Y+U8La": "", - "K3r6DQ": "", - "Z/hwEf": "", - "YDuW/T": "", - "Vhnd2J": "", - "V5TY0z": "", - "Ui6GK/": "", - "UbTsGY": "", - "HhLp57": "", - "ijAUQf": "", - "h+e7G+": "", - "hzt6l8": "", - "hrgo+E": "", - "XXbWAU": "", - "OcpRSQ": "", - "ObmjTB": "", - "OK8u0r": "", - "OHfpS1": "", - "Nh91Us": "", - "Mm1Gse": "", - "GxJAK1": "", - "EQpfkS": "പൂർത്തിയായി", - "+Tmpup": "", - "zELxbG": "", - "z3A0LP": "", - "lJyq2a": "", - "l7zMH6": "", - "fV6578": "", - "T7Ry38": "", - "R+JQaJ": "", - "Q67RuY": "", - "OsDomv": "", - "OINwWS": "", - "N2IrpM": "", - "N1U/QR": "", - "MvEydR": "", - "hXIYHG": "", - "g5pX+a": "", - "g4IF1x": "", - "ZWtlyd": "", - "wcWpGs": "അസാധുവായ webhook URL-കൾ", - "wbwhbH": "ചുമതലയുടെ പേര്", - "wbsq7O": "ഉപയോഗം", - "waVyVY": "പങ്കെടുക്കുന്നവർ നിലവിൽ സജീവമാണ്", - "wZ83YL": "ഇപ്പോഴില്ല", - "wX3k9U": "പേരില്ലാത്ത പ്ലേബുക്ക്", - "q6f8x9": "", - "nqVby7": "", - "eiPBw7": "", - "egvJrY": "", - "edxtzC": "", - "dsTLW1": "", - "djALPR": "", - "aACJNp": "", - "ZdWYcm": "", - "ZAJviT": "", - "Z7vWDQ": "", - "TSSNg/": "", - "TJo5E6": "", - "TBez4r": "", - "IfxUgC": "", - "IOnm/Z": "", - "zx0myy": "", - "zWkvNO": "", - "W1Qs5O": "", - "S0kWcH": "", - "RthEJt": "", - "RoGxij": "", - "QUwMsX": "", - "Lg3I1b": "", - "Leh2tk": "", - "LRFvqz": "", - "L6k6aT": "", - "KiXNvz": "", - "KUr+sG": "", - "KJu1sq": "", - "K4O03z": "", - "JeqL8w": "", - "JXdbo8": "", - "JJNc3c": "", - "JJMNME": "", - "FEGywG": "അപ്‌ഡേറ്റ് റിമൈൻഡറിനായി ഭാവി തീയതി/സമയം വ്യക്തമാക്കുക.", - "EC5MJD": "അപ്‌ഡേറ്റുകളൊന്നും ലഭ്യമല്ല.", - "E0LnBo": "", - "DuRxjT": "", - "DtCplA": "", - "AML4RW": "", - "AF9wda": "", - "v1SpKO": "", - "fmylXu": "", - "9qc7BX": "", - "lxfpbh": "", - "I2zEie": "", - "MrJPOh": "", - "Hzwzgs": "", - "MJ89uW": "", - "9tBhzB": "", - "LmhSmU": "", - "qp3Fk4": "", - "ieGrWo": "", - "q0cpUe": "", - "SDSqfA": "", - "fXGjhC": "", - "JqKASQ": "", - "5ciuDD": "", - "2563nT": "", - "m/Q4ye": "", - "I5NMJ8": "", - "nkCCM2": "", - "vndQuC": "സ്ലാഷ് കമാൻഡ് എക്സിക്യൂട്ട് ചെയ്തു", - "viXE32": "സ്വകാര്യം", - "vNiZXF": "", - "twieZh": "", - "t6SiGO": "നിലവിൽ റണ്ണുകൾ പുരോഗമിക്കുന്നു", - "syEQFE": "പ്രസിദ്ധീകരിക്കുക", - "sqNmlF": "", - "soePYH": "", - "scYyVv": "", - "sVlNlY": "ഓരോ ടീമിന്റെയും ഘടന വ്യത്യസ്തമാണ്. ടീമിലെ ഏത് ഉപയോക്താക്കൾക്ക് പ്ലേബുക്കുകൾ സൃഷ്‌ടിക്കാമെന്ന് നിങ്ങൾക്ക് മാനേജ് ചെയ്യാം.", - "rbrahO": "", - "rX08cW": "", - "rDvvQs": "", - "pjt3qA": "", - "pKLw8O": "", - "oVHn4s": "", - "oS0w4E": "", - "o2eHmz": "", - "kXFojL": "", - "hO9EdA": "", - "gy/Kkr": "", - "guunZt": "", - "gt6BhE": "", - "d9epHh": "", - "d8KvXJ": "", - "c8hxKk": "", - "bPLen5": "", - "bLK+Kr": "", - "bGhCLX": "", - "bE1Cro": "", - "b5FaCc": "", - "b/QBNs": "", - "avPeEI": "", - "aYIUar": "", - "aWpBzj": "", - "X/koAN": "", - "WTQpnI": "", - "WIxhrv": "", - "WAHCT2": "", - "1MQ3XZ": "", - "/HtNUp": "", - "+hddg7": "", - "+QgvjN": "", - "+8G9qr": "", - "uT4ebt": "", - "/urtZ8": "", - "9m0I/B": "", - "vQqT/8": "", - "Q3R9Uj": "", - "dxyZg3": "", - "q/VD+s": "", - "8n24G2": "", - "rzbYbE": "", - "lUfDe1": "", - "vJ2SaW": "", - "4BN53Q": "", - "0Xt1ea": "", - "fhMaTZ": "", - "q/Qo8l": "", - "f+bqgK": "", - "HGdWwZ": "", - "GG1yhI": "", - "RzEVnf": "", - "wbdGb5": "", - "GjCS6U": "", - "Pue+oV": "", - "/fU9y/": "", - "Q5hysF": "", - "udrLSP": "", - "HXvk56": "", - "xvBDOH": "", - "1isgPF": "", - "rMhrJH": "", - "y7o4Rn": "", - "tbjmvS": "", - "XpDetT": "", - "lBqu4h": "", - "1QosTr": "", - "a0hBZ0": "", - "mVpO8u": "", - "NYTGIb": "", - "lgZf0l": "", - "hw83pa": "", - "vL4++D": "", - "cEWBE3": "", - "ZkhArX": "", - "Tt04f1": "", - "I5DYM+": "", - "GAuN6w": "", - "R5Zh+l": "", - "QbGfqo": "", - "dZmYk6": "", - "wPVxBN": "", - "0EEIkR": "", - "1ikfp3": "", - "VZRWFk": "", - "TxmjKI": "", - "Sx3lHL": "", - "SVwJTM": "", - "OyZnsJ": "", - "NJ9uPu": "", - "LI7YlB": "", - "LDYFkN": "", - "JrZ2th": "", - "FGzxgY": "", - "9SIW2x": "", - "6D6ffM": "", - "bTgMQ2": "", - "MTzF3S": "", - "4cwL43": "", - "4aupaG": "", - "9XUYQt": "", - "4fHiNl": "", - "3PoGhY": "", - "6GTzTR": "", - "gsMPAS": "", - "F4pfM/": "", - "mbo96h": "", - "4alprY": "", - "efeNi1": "", - "KXVV4+": "", - "xVyHgP": "", - "Vf/QlZ": "", - "NiAH1z": "", - "M4gAc9": "", - "ru+JCk": "", - "ZNNjWw": "", - "fmbSyg": "", - "9a9+ww": "", - "l5/RKZ": "", - "NLeFGn": "", - "NMxVd+": "", - "69nlA3": "", - "mvZUm3": "", - "lbs7UO": "", - "awG90C": "" -} diff --git a/webapp/playbooks/i18n/nl.json b/webapp/playbooks/i18n/nl.json deleted file mode 100644 index cba4ec7e1f0..00000000000 --- a/webapp/playbooks/i18n/nl.json +++ /dev/null @@ -1,845 +0,0 @@ -{ - "/jUtaM": "ACTIEF UITGEVOERD per dag gedurende de laatste 14 dagen", - "/HtNUp": "Selecteer of specificeer een {mode, select, DurationValue {tijdspanne (\"4 uur\", \"7 dagen\"...)} DateTimeValue {tijd (\"over 4 uur\", \"1 mei\", \"Morgen om 13.00 uur\"...)} other {tijd of tijdspanne}}", - "/1FEJW": "ACTIEVE DEELNEMERS per dag gedurende de afgelopen 14 dagen", - "z5RMPO": "Alleen jij hebt toegang tot deze playbook", - "waVyVY": "Momenteel actieve deelnemers", - "wEQDC6": "Bewerken", - "v3+TmO": "{members, plural, =0 {Niemand} =1 {Een persoon} other {# personen}} hebben toegang tot deze playbook", - "t6SiGO": "Uitvoeringen die bezig zijn", - "soePYH": "{num_checklists, plural, =0 {geen checklists} one {# checklist} other {# checklists}}", - "sQu1rA": "{numTotalRuns, plural, =0 {geen gestarte uitvoeringen} =1 {# uitvoering gestart} other {# uitvoeringen gestart}}", - "s3jjqi": "{num_actions, plural, =0 {geen acties} one {# actie} other {# acties}}", - "lZwZi+": "Dag: {date}", - "ebkl6I": "Iedereen in dit team heeft toegang tot deze playbook", - "c8hxKk": "Week van {date}", - "bPLen5": "Uitvoeringen voltooid in de laatste 30 dagen", - "avPeEI": "Upgrade om trends te zien voor totaal aantal uitvoeringen, actieve uitvoeringen en deelnemers die betrokken zijn bij uitvoeringen van deze playbook.", - "YDuW/T": "{num_runs, plural, =0 {Nog niet uitgevoerd} one {# uitvoering} other {# uitvoeringen in het totaal}}", - "XmUdvV": "Alle statistieken die je nodig hebt", - "UbTsGY": "Uitvoeringen gestart tussen {start} en {end}", - "TSSNg/": "TOTAAL UITVOERINGEN gestart per week over de laatste 12 weken", - "RoGxij": "Actieve uitvoering op {date}", - "Q7aZO4": "{numParticipants, plural, =0 {geen actieve deelnemers} =1 {# actieve deelnemer} other {# actieve deelnemers}}", - "KiXNvz": "Uitvoeren", - "AF9wda": "Deze update zal worden opgeslagen op de overzichtspagina{hasBroadcast, select, true { en uitgezonden naar {broadcastChannelCount, plural, =1 {een kanaal} other{{broadcastChannelCount, number} kanalen}}} other{}}.", - "6Lwe7T": "Iedereen in {team} heeft toegang tot deze playbook", - "1MQ3XZ": "{numActiveRuns, plural, =0 {niet actief in uitvoering} =1 {# actieve uitvoering} other {# actieve uitvoeringen}}", - "zINlao": "Eigenaar", - "wbwhbH": "Taaknaam", - "wbsq7O": "Gebruik", - "viXE32": "Privé", - "uhu5aG": "Publiek", - "uJ3bRR": "Dit sjabloon helpt bij het standaardiseren van de opmaak voor een beknopte beschrijving die elke uitvoering uitlegt aan de belanghebbenden.", - "recCg9": "Updates", - "rX08cW": "De datum moet in de toekomst liggen.", - "oS0w4E": "Standaard update timer", - "lbhO3D": "cursief", - "jvo0vs": "Bewaar", - "jq4eWU": "Draaiboektoegang", - "jXT2++": "Ga naar kanaal", - "jIIWN+": "voorgeformatteerd", - "hzt6l8": "Gebruik Markdown om een sjabloon te maken.", - "hXIYHG": "Installeer en activeer de kanaalexportplugin om het exporteren van het kanaal te ondersteunen", - "gy/Kkr": "(bewerkt)", - "g5pX+a": "Over", - "eiPBw7": "Terugblik herinneringsinterval", - "eHAvFf": "vetjes", - "djXM+y": "Enkel geselecteerde gebruikers hebben toegang.", - "dcV/DJ": "{timestamp}", - "d9epHh": "Kanaallog exporteren", - "bLK+Kr": "Herinnert het kanaal er met een gespecificeerd interval aan om de terugblik in te vullen.", - "b40Pr7": "Verslaggever", - "VmpFFw": "Er is geen beschrijving beschikbaar.", - "TZYiF/": "doorhalen", - "TJo5E6": "Voorbeeld", - "T7Ry38": "Bericht", - "T5rX+W": "Hoe vaak moet een update worden geplaatst?", - "SFuk1v": "Machtigingen", - "SENRqu": "Help", - "RthEJt": "Terugblik", - "R+JQaJ": "Kanaalleden", - "QnZAit": "Optionele beschrijving toevoegen", - "QiKcO7": "Invoeren sjabloon voor terugblik", - "Q8Qw5B": "Omschrijving", - "ObmjTB": "Slash opdracht", - "NE1OeI": "Iedereen in team ({team}) heeft toegang.", - "JCGvY/": "Dit sjabloon helpt bij het standaardiseren van het formaat voor terugkerende updates die gedurende elke uitvoering plaatsvinden.", - "IuFETn": "Duur", - "ICqy9/": "Checklists", - "HhLp57": "citaat", - "EC5MJD": "Er zijn geen updates beschikbaar.", - "DnBhRg": "Mensen toevoegen", - "DCl7Vv": "In-line code", - "CL5OZP": "Alleen gebruikers die je selecteert kunnen dit draaiboek bewerken of uitvoeren.", - "BD66u6": "Download een CSV met alle berichten van het kanaal", - "AS5kar": "Deelnemers ({participants})", - "A3ptul": "Sjablonen", - "9uOFF3": "Overzicht", - "5Ot7cd": "Bepaal het type kanaal dat dit draaiboek creëert.", - "5FRgqE": "Downloaden van kanaallog", - "5A46pW": "Slash opdracht toevoegen", - "47FYwb": "Annuleren", - "3rCdDw": "Statusupdates", - "1I48bs": "Sjabloon voor terugblik", - "+ZIXOR": "Kanaaltoegang", - "+8G9qr": "Standaardtekst voor de terugblik.", - "X3DLGJ": "Iedereen in deze werkruimte kan playbooks maken.", - "TyrY2b": "Playbook maken", - "D3idYv": "Instellingen", - "AT2QBo": "Alleen geselecteerde gebruikers kunnen playbooks maken.", - "KUr+sG": "Werk overzicht van uitvoeringen bij", - "I2zEie": "Vier succes en leer van fouten met terugblikrapporten. Filter tijdlijngebeurtenissen voor procesbeoordeling, betrokkenheid van belanghebbenden en controledoeleinden.", - "JeqL8w": "Terugblik geannuleerd door {name}", - "Hzwzgs": "Broadcast updates in de {oneChannel, plural, one {kanaal} other {kanalen}}", - "FEGywG": "Gelieve een datum/tijd in de toekomst op te geven voor de updateherinnering.", - "DXACD6": "Publiceren van terugblikverslag en toegang tot de tijdlijn", - "CjNrqO": "Sjabloon voor terugblik", - "ArpdYl": "Tijdlijngebeurtenissen worden hier weergegeven wanneer ze zich voordoen. Beweeg met de muis over een gebeurtenis om deze te verwijderen.", - "AML4RW": "Toegewezen taken", - "9Obw6C": "Filter", - "8hDbW6": "Een uitgaande webhook verzenden", - "4Hrh5B": "{name} wijzigde de status van {summary}", - "3/wF0G": "Slash-opdrachten", - "+QgvjN": "Wijs de rol van eigenaar toe aan", - "v1SpKO": "Rolveranderingen", - "v1DNMW": "Terugblik gepubliceerd door {name}", - "usa8vQ": "Stuur een welkomstbericht", - "syEQFE": "Publiceren", - "pKLw8O": "Weet je zeker dat je deze gebeurtenis wilt verwijderen? Verwijderde gebeurtenissen worden permanent verwijderd uit de tijdlijn.", - "o2eHmz": "Uitvoering beëindigd door {name}", - "nvy0pS": "Wanneer een uitvoering voltooid is, exporteer je het kanaal", - "lxfpbh": "De eigenaar zal {reminderEnabled, select, true {worden gevraagd om elke statusupdate te geven} other {worden niet gevraagd om een statusupdate te geven}}", - "jnmORb": "In dit playbook", - "jS/UOn": "Sjabloon bijwerken", - "hO9EdA": "Nodig {numInvitedUsers, plural, =0 {geen leden} =1 {een lid} other {# leden}} uit in het kanaal", - "fXGjhC": "Eigenaar veranderd van {summary}", - "fUEpLA": "Er zijn geen gebeurtenissen op de tijdlijn die aan deze filters voldoen.", - "egvJrY": "Verantwoordelijke werd veranderd", - "bGhCLX": "Als een update geplaatst is", - "b5FaCc": "Voeg het kanaal toe aan de zijbalk categorie", - "aACJNp": "Uitvoering gestart door {name}", - "ZwlIYH": "{activeRuns, number} actieve {activeRuns, plural, one {uitvoering} other {uitvoeringen}}", - "Z/hwEf": "Het kanaal zal worden herinnerd om de terugblik {reminderEnabled, select, true {elke} other{}} uit te voeren", - "W9j0FJ": "{date}", - "Ui6GK/": "Wanneer een nieuw lid het kanaal binnenkomt", - "TvihSy": "Opnieuw publiceren", - "SDSqfA": "Wanneer een uitvoering begint", - "OsDomv": "Alle gebeurtenissen", - "OcpRSQ": "Invoer wissen", - "OINwWS": "Maak een {isPublic, select, true {publiek} other {privé}} kanaal", - "N1U/QR": "Status van taak veranderd", - "MvEydR": "{name} heeft een statusupdate geplaatst", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {taak} other {taken}}", - "LmhSmU": "Bevestig wissen invoer", - "LRFvqz": "Kondig aan in het {oneChannel, plural, one {kanaal} other {kanalen}}", - "vOFN0m": "Status bericht verwijderd:", - "A21Mgv": "Uitvoering beëindigd", - "9tBhzB": "Upgrade nu", - "9qc7BX": "Snooze", - "9kCT7Q": "Maak terugblikken eenvoudig met een tijdlijn die automatisch de belangrijkste gebeurtenissen en berichten bijhoudt, zodat teams deze binnen handbereik hebben.", - "9TTfXU": "Jouw systeembeheerder is verwittigd.", - "9PXW6Q": "Duur / Begonnen op", - "91Hr5f": "Sleep me om de volgorde te wijzgen", - "9+Ddtu": "Volgende", - "6uhSSw": "Kies een kanaal", - "6n0XDG": "Ben je zeker dat he de checklist wil verwijderen? Alle taken zullen worden verwijderd.", - "6jDabx": "Geef feedback", - "6CGo3o": "Status / Laatst bijgewerkt", - "5wqhGy": "Toggle uitvoeringsdetails", - "5qBEKB": "Wat zijn playbook uitvoeringen?", - "5j6GD/": "{numParticipants, plural, =0 {geen deelnemers} =1 {# deelnemer} other {# deelnemers}}", - "5CI3KH": "Neem contact met de helpdesk", - "4ltHYh": "Ga naar het playbook", - "42qmJ5": "Je hebt geen rechten om een update te plaatsen.", - "3Psa+5": "Trefwoorden toevoegen", - "2VrVHu": "Zoeken op naam van de uitvoering", - "2Qq4YX": "Weet je zeker dat u jouw wijzigingen wil ongedaan maken?", - "2QkJ4s": "Bewaar belangrijke berichten voor een compleet beeld dat terugblikken vereenvoudigt.", - "2PNrBQ": "Exporteer het kanaal van jouw playbookuitvoering en bewaar het voor latere analyse.", - "15jbT0": "Voeg meer toe aan jouw tijdlijn", - "0wJ7N+": "Taak", - "0oLj/t": "Uitvouwen", - "/YZ/sw": "Probeerversie starten", - "/MaJux": "Start terugblik", - "+hddg7": "Toevoegen aan uitvoeringstijdlijn", - "BQtd5I": "Welkom bij Playbooks!", - "BNB75h": "Een playbook schrijft de checklists, automatiseringen en sjablonen voor herhaalbare procedures voor. {br} Het helpt teams fouten te verminderen, vertrouwen te winnen van belanghebbenden, en effectiever te worden met elke uitvoering.", - "B487HA": "In uitvoering", - "Auj1ap": "Start een proefabonnement of upgrade jouw abonnement.", - "ApULhK": "Leden uitnodigen", - "A8dbCS": "Playbook niet gevonden", - "CBM4vh": "Timer voor volgende update", - "C9NScU": "Geef jouw team de touwtjes in handen", - "C1khRR": "Terug naar Playbooks", - "CkYhdY": "Voeg het kanaal toe aan de zijbalk categorie", - "CSts8B": "Teampictogram", - "GwtR3W": "Sleep een bestaande taak of klik om een nieuwe taak aan te maken.", - "GRTyvN": "Toggle Playbook Lijst", - "G/yZLu": "Verwijderen", - "EQpfkS": "Voltooid", - "Cy1AK/": "Bekijk details van de uitvoering", - "36GNZj": "De playbook {title} is succesvol gearchiveerd.", - "0HT+Ib": "Gearchiveerd", - "E0LnBo": "Je kaneen optie kiezen of een aangepaste duur opgeven (\"2 weken\", \"3 dagen 12 uur\", \"45 minuten\", ...)", - "DuRxjT": "Een Playbook maken", - "DtCplA": "{numParticipants, plural, =1 {# deelnemer} other {# deelnemers}}", - "DSVJjB": "Momenteel wordt de {playbookTitle} playbook uitgevoerd", - "D55vrs": "Jouw licentie kon niet gegenereerd worden", - "D2CE02": "Webhook invoeren", - "CyGaem": "Naam van de uitvoering", - "+Tmpup": "Je ontvangt automatisch updates wanneer deze playbook wordt uitgevoerd.", - "Leh2tk": "Klik hier om alle uitvoeringen in het team te zien.", - "LVYPbG": "Eigenaar toewijzen", - "L6k6aT": "...of begin met een sjabloon", - "KJu1sq": "Checklist verwijderen", - "K4O03z": "Nieuwe taak", - "K3r6DQ": "Verwijderen", - "JXdbo8": "Klaar", - "JJNc3c": "Vorige", - "JJMNME": "{withRunName, select, true {@{authorUsername} heeft een update geplaatst voor [{runName}]({overviewURL})} other {@{authorUsername} heeft een update geplaatst}}", - "J1G4S4": "Er zijn nog geen playbooks gedefinieerd.", - "IwY/wg": "Een playbook voor elk proces", - "IfxUgC": "Voeg een samenvatting van de uitvoering toe…", - "Ietscn": "Taken voltooid", - "IOnm/Z": "Er is geen samenvatting van de uitvoering beschikbaar.", - "I90sbW": "zonet", - "HSi3uv": "Niemand toegewezen", - "HAlOn1": "Naam", - "GxJAK1": "De playbook die je opvraagt is privé of bestaat niet.", - "V5TY0z": "Deelnemers toevoegen?", - "TdTXXf": "Meer info", - "TDaF6J": "Afwijzen", - "TBez4r": "Er zijn geen playbooks om te bekijken. Je hebt geen rechten om playbooks te maken in deze werkruimte.", - "SmAUf9": "Er zal een herinnering worden gestuurd {timestamp}", - "S0kWcH": "Achterstallige update", - "Rgo4VW": "Iedereen in deze werkruimte kan playbooks aanmaken. Systeembeheerders kunnen deze instelling wijzigen.", - "R4vA+C": "Alleen de onderstaande gebruikers kunnen playbooks aanmaken. Deze gebruikers, maar ook systeembeheerders, kunnen deze instelling wijzigen.", - "Qrl6bQ": "Stroomlijn jouw processen met playbooks", - "QaZNp9": "Uitvoering beëindigen", - "QVQrgH": "Nadat je jouw eigen toegang tot dit playbook heeft verwijderd, kan je jezelf niet meer toevoegen. Weet je zeker dat je deze actie wilt uitvoeren?", - "QUwMsX": "Herinnering om de terugblik in te vullen", - "Q7hMnp": "Playbook uitvoeren", - "Q67RuY": "Bekijk alle uitvoeringen", - "Oo5sdB": "Playbook naam", - "OK8u0r": "Maak een playbook om de workflow voor te schrijven die jouw teams en tools moeten volgen, met alles van checklists, acties, sjablonen en terugblikken.", - "OHfpS1": "Met een van deze trefwoorden", - "Nh91Us": "{from, number}–{to, number} van {total, number} totaal", - "N2IrpM": "Bevestigen", - "Mm1Gse": "Zoeken naar lid", - "MhKICa": "Jouw abonnement staat één playbook per team toe. Upgrade jouw abonnement en maak meerdere playbooks met unieke workflows voor elk team.", - "MDP9TS": "Verwijderen uit playbook", - "M/2yY/": "Nog niemand.", - "Lg3I1b": "@{targetUsername}, geef een status update aub.", - "aWpBzj": "Toon Meer", - "ZdWYcm": "Nee, sla terugblik over", - "ZWtlyd": "Uitvoering hersteld door {name}", - "ZAJviT": "We konden de systeembeheerder niet inlichten.", - "Z7vWDQ": "Er was een fout", - "YORRGQ": "Update plaatsen", - "YMrTRm": "Uitvoering Samenvatting", - "YKn+7s": "Dit kanaal heeft geen playbook in uitvoering.", - "XXbWAU": "Selecteer dit om automatisch updates te ontvangen wanneer deze playbook wordt uitgevoerd.", - "X/koAN": "Ongeldige invoer: het maximum aantal toegestane webhooks is 64", - "WTQpnI": "Onderneem nu actie met behulp van playbooks", - "WIxhrv": "De naam van de uitvoering moet uit ten minste twee tekens bestaan", - "WAHCT2": "Verwittig systeembeheerder", - "W1Qs5O": "In uitvoering", - "W/V6+Y": "Samenvouwen", - "VmnoW8": "Controleer de systeemlogboeken voor meer informatie.", - "VOzlSL": "Het uitvoeren van een playbook orkestreert workflows voor jouw team en tools.", - "7VTSeD": "Weet je zeker dat je deze taak wilt overslaan? Dit zal worden doorgestreept uit deze uitvoering, maar zal geen invloed hebben op het playbook.", - "/4tOwT": "Overslaan", - "hVFgh4": "Inclusief afgewerkt", - "h+e7G+": "Verzoek om dit playbook uit te voeren wanneer een bericht {numKeywords, select, 1 {het sleutelwoord} other {een of meer van deze}} bevat", - "guunZt": "Toewijzen", - "gt6BhE": "Details uitvoering", - "g4IF1x": "Er zijn geen uitvoeringen voor dit playbook.", - "fmylXu": "Verzoek om het playbook uit te voeren wanneer een gebruiker een bericht plaatst", - "fV6578": "De eigenaarsrol toekennen", - "edxtzC": "Playbook maken", - "eLeFE2": "Wijzig naam en beschrijving", - "eKv7yX": "Bericht", - "e/AZL5": "Jouw 30 dagen proefperiode is begonnen", - "dvhvum": "(Optioneel) Beschrijf hoe dit playbook moet worden gebruikt", - "dsTLW1": "Taak bewerken", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {uitvoering} other {uitvoeringen}} lopende", - "dSC1YD": "Taak overslaan", - "d8KvXJ": "Jouw proeflicentie vervalt op {expiryDate}. Je kan op elk moment een licentie aanschaffen via het Klantenportaal om onderbrekingen te voorkomen.", - "bE1Cro": "Alleen mijn uitvoeringen", - "b3TdyZ": "Door op Start proefversie te klikken, ga ik akkoord met de Mattermost Software Evaluatie Overeenkomst, Privacy Beleid, en het ontvangen van product emails.", - "b/QBNs": "Update verschuldigd", - "aYIUar": "Dankjewel!", - "Vhnd2J": "Toggle beschrijving", - "hrgo+E": "Archiveren", - "hfrrC7": "Initialen van het team", - "zz6ObK": "Herstellen", - "zx0myy": "Deelnemers", - "zWkvNO": "Tijdlijn", - "zELxbG": "Bewaarde berichten", - "z3B83t": "Zoek naar een playbook", - "z3A0LP": "Laatste uitvoering was {relativeTime}", - "yxguVq": "Wijzigingen verwijderen", - "yqpcOa": "Gebruiken", - "ypIsVG": "Taak herstellen", - "yhzuSC": "Tijd: {time}", - "yhU1et": "Taken", - "xmcVZ0": "Zoeken", - "x8cvBr": "Uitvoeringsoverzicht bekijken", - "x5Tz6M": "Verslag", - "wsUmh9": "Team", - "wcWpGs": "Ongeldige webhook URL's", - "wZ83YL": "Niet nu", - "wX3k9U": "Titelloos playbook", - "wO6NOM": "Weet je zeker dat u deze taak wilt herstellen? Deze taak zal worden toegevoegd aan deze uitvoering", - "wL7VAE": "Acties", - "w7tf2z": "Gepubliceerd", - "w0muFd": "Uitgaande webhook verzenden (Eén per regel)", - "vndQuC": "Slash commando uitgevoerd", - "vjzpnC": "Er zijn geen playbooks die aan die filters voldoen.", - "vir0m9": "Ongeldige categorienaam.", - "vNiZXF": "Er zijn op dit moment geen lopende uitvoeringen. Voer een playbook uit om te beginnen met het orkestreren van workflows voor jouw team en tools.", - "v8ZnNc": "Selecteer een team", - "uny3Zy": "Playbooks", - "uBLF+D": "Wat is een playbook?", - "u4MwUB": "Bewaar jouw playbook-uitvoeringsgeschiedenis", - "tzMNF3": "Status", - "twieZh": "Ga naar het uitvoeringsoverzicht", - "sqNmlF": "Sla terugblik over", - "scYyVv": "Wilt u het terugblikverslag invullen?", - "sVlNlY": "De structuur van elk team is anders. Je kan beheren welke gebruikers in het team playbooks kunnen maken.", - "sIX63S": "Jouw systeembeheerder is verwittigd", - "ryrP8K": "Beheer de rechten voor wie dit afspeelboek mag bekijken, wijzigen en uitvoeren.", - "rbrahO": "Sluiten", - "rDvvQs": "{completed, number} / {total, number} gedaan", - "qyJtWy": "Toon minder", - "qp3Fk4": "Een playbook is een workflow die jouw teams en tools moeten volgen, met alles van checklists, acties, sjablonen en terugblikken.", - "q6f8x9": "Wijziging sinds laatste update", - "prYDT6": "Aankondigingskanaal", - "pjt3qA": "Nieuwe checklist", - "oVHn4s": "Laatst bijgewerkt", - "nqVby7": "{numTasksChecked, number} van {numTasks, number} {numTasks, plural, =1 {taak} other {takens}} afgevinkt", - "nmpevl": "Weggooien", - "nkCCM2": "Je zal er niet meer aan herinnerd worden.", - "lrbrjv": "Ja, start terugblik", - "lJyq2a": "Uitvoering niet gevonden", - "l7zMH6": "Selecteer een optie of geef een aangepaste duur op", - "l0hFoB": "Playbook beschrijving toevoegen...", - "kvgvNW": "Weet wat er gebeurd is", - "kXFojL": "Je kan ook van tevoren een playbook maken, zodat het beschikbaar is wanneer je het nodig hebt.", - "kGI46P": "Taakomschrijving", - "kDcpd/": "{numKeywords, plural, other {# trefwoorden}}", - "k9q07e": "Update versturen naar andere kanalen", - "jwimQJ": "Ok", - "jIgqRa": "Eigenaar / Deelnemers", - "j7jdWG": "Converteren naar een commerciële editie.", - "izWS4J": "Niet langer volgen", - "ijAUQf": "Breng jouw systeembeheerder op de hoogte van de upgrade.", - "ieGrWo": "Volgen", - "iNU1lj": "De uitvoering die je opvraagt is privé of bestaat niet.", - "fpuWL1": "Verwijder playbook", - "Y+U8La": "Weet je zeker dat u het Playbook {title} wilt verwijderen?", - "UMoxP9": "Kanaalnaamsjabloon (optioneel)", - "RO+BaS": "Kopieer link naar uitvoering", - "NA7Cw1": "Kopieer link naar playbook", - "3MSGcL": "Kanaalnaam is ongeldig.", - "0oL1zz": "Gekopieerd!", - "cp7KUI": "Playbook", - "cPIKU2": "Volgend", - "C6Oghd": "Bewerk samenvatting van de uitvoering", - "fuDLDJ": "Maak een nieuw kanaal", - "d4g2r8": "Verwijderd: {timestamp}", - "4vuNrq": "{duration} sinds de uitvoering begon", - "/gbqA6": "{duration} voordat de uitvoering begon", - "O8o2lE": "Kanaal toevoegen aan categorie", - "Mu2aDs": "Iedereen in team ({team}) heeft toegang.", - "D/wCS9": "Weet je zeker dat je de terugblik wil publiceren?", - "5ciuDD": "NIET IN KANAAL", - "5Ofkag": "Maak terugblik mogelijk", - "2563nT": "Bevestig beëindigen uitvoering", - "2/2yg+": "Toevoegen", - "/ZsEUy": "Weet je zeker dat je deze checklist wil verwijderen? Hij zal uit deze run worden verwijderd, maar heeft geen invloed op het playbook.", - "vaYTD+": "Er zijn {outstanding, plural, =1 {is # openstaande taak} other {are # openstaande taken}}. Ben je zeker dat je deze uitvoering wil afsluiten?", - "q0cpUe": "Checklist toevoegen", - "pK6+CW": "@{displayName} is geen lid van het [{runName}]({overviewUrl}) kanaal. Wil je deze toevoegen aan dit kanaal? Deze zal toegang hebben tot de gehele berichtengeschiedenis.", - "nSFBC2": "+ Taak toevoegen", - "m/Q4ye": "Hernoem checklist", - "k1djnL": "Checklist verwijderen", - "iXNbPf": "Hernoemen", - "iDMOiz": "KANAALLEDEN", - "X2K92H": "Naam checklist", - "WbsomC": "Publiceer terugblik", - "TxCTXQ": "Weet je zeker dat je de uitvoering wil markeren als afgewerkt?", - "QywYDe": "Markeer de uitvoering ook als voltooid", - "MrJPOh": "Statusupdates inschakelen", - "JqKASQ": "Voeg @{displayName} toe aan kanaal", - "Ja1sVR": "Statusupdates waren uitgeschakeld voor deze playbook run.", - "I5NMJ8": "Meer", - "D9IV7i": "Een terugblik was uitgeschakeld voor deze playbook run.", - "Lo10yH": "Onbekend kanaal", - "osuP6z": "Slepen om checklist te herschikken", - "wylJpv": "Iedereen in {team} kan dit playbook bekijken.", - "tVPYMu": "Playbook beheerder", - "sDKojV": "Playbook archiveren", - "ruJGqS": "Playbook toegang", - "o+ZEL3": "Gepubliceerd {timestamp}", - "lQT7iD": "Playbook maken", - "gGcNUr": "Je hebt geen rechten", - "g0mp+I": "Wanneer je dit omzet naar een privaat playbook, blijven lidmaatschap en uitvoeringsgeschiedenis behouden. Deze verandering is permanent en kan niet ongedaan worden gemaakt. Weet je zeker dat je {playbookTitle} wilt omzetten naar een privaat playbook?", - "SXJ98n": "Je kan de terugblikrapportage niet meer bewerken nadat je dit hebt gepubliceerd. Wil je de terugblikrapportage publiceren?", - "R/2lqw": "Kies een sjabloon", - "QpUBDr": "{members, plural, =0 {Niemand} =1 {één persoon heeft} other {# mensen hebben}} toegang tot dit playbook.", - "MJ89uW": "Omzetten naar privaat playbook", - "HLn43R": "Toegang beheren", - "0Vvpht": "Maak Playbook lid", - "EvBQLq": "Maak Playbook admin", - "EWz2w5": "Playbook uitvoeren", - "8oCVbz": "Weet je zeker dat je wilt publiceren", - "5BUxvl": "Iedereen in dit team kan dit playbook bekijken.", - "3Ls2m+": "Playbook lid", - "0tznw6": "Omzetten naar privaat playbook", - "0q+hj2": "Definieer een sjabloon voor een beknopte beschrijving die elke uitvoering aan de deelnemers verklaart.", - "FXCLuZ": "{total, number} totaal", - "qsr3Zk": "De samenvatting van de uitvoering bijwerken", - "3PoGhY": "Weet je zeker dat je dit wil publiceren?", - "4fHiNl": "Kopiëren", - "4alprY": "Playbook-sjablonen", - "/urtZ8": "Jouw Playbooks", - "SVwJTM": "Exporteren", - "9XUYQt": "Importeren", - "rzbYbE": "Doel", - "rMhrJH": "Voeg een titel toe voor jouw metriek.", - "q/Qo8l": "Private playbooks zijn alleen beschikbaar in Mattermost Enterprise", - "mbo96h": "Configureer aangepaste meetwaarden om in te vullen met het terugblikrapport", - "mVpO8u": "Heb je dit al gezien?", - "lBqu4h": "Playbook terugzetten", - "gsMPAS": "Dollars", - "f+bqgK": "Naam van de meetwaarde", - "bTgMQ2": "Deze playbook is gearchiveerd.", - "a0hBZ0": "Meetwaarde verwijderen", - "XpDetT": "Schakel deze tips uit.", - "VZRWFk": "b.v., Kosten, Aankopen", - "TxmjKI": "Beschrijf waar deze meetwaarde over gaat", - "Sx3lHL": "Geheel getal", - "OyZnsJ": "per uitvoering", - "NYTGIb": "Begrepen", - "NJ9uPu": "Belangrijkste meetwaarden", - "MTzF3S": "Weet je zeker dat je de playbook {title} wilt herstellen?", - "LI7YlB": "Voeg details toe over waar deze meetwaarde over gaat en hoe deze ingevuld moet worden. Deze beschrijving zal beschikbaar zijn op de terugblikpagina voor elke uitvoering waar de waarden voor deze meetwaarde zullen worden ingevoerd.", - "LDYFkN": "Duur (in dd:hh:mm)", - "JrZ2th": "Meetwaarde toevoegen", - "FGzxgY": "b.v. Tijd om te beoordelen, Tijd om op te lossen", - "F4pfM/": "Voer een nummer in, of laat het doel leeg.", - "9SIW2x": "Streefwaarde voor elke uitvoering", - "6D6ffM": "Voer een tijdsduur in, in het formaat: dd:hh:mm (bijv. 31:23:59), of laat het doel leeg.", - "4cwL43": "Met gearchiveerde", - "4aupaG": "De playbook {title} is succesvol hersteld.", - "4BN53Q": "Wij laten je zien hoe dicht of hoe ver de waarde van elke reeks van het doel verwijderd is en zetten het ook op een grafiek.", - "1ikfp3": "Als je deze statistiek verwijdert, zullen de waarden voor deze statistiek niet worden verzameld voor toekomstige uitvoeringen.", - "0Xt1ea": "Je hebt nog steeds toegang tot historische gegevens voor de statistiek.", - "/fU9y/": "Je kan de verschillende onderdelen van de playbook in detail bekijken op deze pagina.", - "1isgPF": "We hebben automatisch je eerste uitvoering gemaakt", - "1QosTr": "Gebruikt door", - "0EEIkR": "Gefeliciteerd! Je hebt jouw eerste playbook gemaakt met behulp van een sjabloon!", - "y7o4Rn": "Weet je zeker dat je wilt verwijderen?", - "xvBDOH": "Weet je zeker dat je het playbook {title} wilt archiveren?", - "wbdGb5": "Wijs taken toe, vink ze af of sla ze over om ervoor te zorgen dat het voor het team duidelijk is hoe het samen naar de eindstreep moet toewerken.", - "wPVxBN": "Selecteer Bewerken om het sjabloon aan te passen aan jouw eigen modellen en processen. Je kan het sjabloon in detail bekijken op deze pagina.", - "vQqT/8": "Selecteer Bewerken om het sjabloon aan te passen aan jouw eigen modellen en processen. Je kan het sjabloon in detail bekijken op deze pagina.", - "vL4++D": "Vooruitgang en verantwoordelijkheid volgen", - "vJ2SaW": "Automatiseer aspecten van jouw playbook, zoals het versturen van een welkomstbericht, het uitnodigen van belangrijke leden en het aanmaken van een updatekanaal.", - "udrLSP": "Gebruik meetgegevens om patronen en vooruitgang in uitvoeringen te begrijpen en prestaties te volgen.", - "uT4ebt": "b.v. aantal middelen, betrokken klanten", - "tbjmvS": "Een meetwaarde met deze naam bestaat al. Voeg een unieke naam toe voor elke meetwaarde.", - "q/VD+s": "Stel timers in en stel een sjabloon samen voor statusupdates, zodat belanghebbenden altijd op de hoogte zijn van de ontwikkelingen.", - "lgZf0l": "Aan de slag met Playbooks", - "lUfDe1": "Exporteer het playbookuitvoeringskanaal en bewaar het voor latere analyse.", - "hw83pa": "Belangrijke statistieken bijhouden en waarde meten", - "fhMaTZ": "Neem een snelle rondleiding", - "dxyZg3": "Laat me zelf onderzoeken", - "dZmYk6": "Succesvol playbook gedupliceerd", - "cEWBE3": "Evalueer jouw processen met behulp van een retrospectief om ze bij elke uitvoering run te verfijnen en te verbeteren.", - "ZkhArX": "Laten we starten!", - "Tt04f1": "Zien wie betrokken is en wat er moet gebeuren zonder het gesprek te verlaten.", - "RzEVnf": "Playbooks maken belangrijke procedures beter herhaalbaar en controleerbaar. Een playbook kan meerdere keren worden uitgevoerd, en elke uitvoering heeft zijn eigen archief en retrospectief.", - "R5Zh+l": "Zo kan je eerst een voorbeeld van een playbook ervaren voordat je tijd investeert om je eigen playbook te maken.", - "QbGfqo": "Zend uit naar belanghebbenden op meerdere plaatsen en houd een papieren spoor voor retrospectief met slechts één bericht.", - "Q5hysF": "Doe meer met Playbooks", - "Q3R9Uj": "Documenteer hier de stappen voor het gehele proces. Wijs elke taak toe aan verantwoordelijke personen en voeg eventueel tijdlijnen of gekoppelde acties toe.", - "Pue+oV": "Voer de playbook uit om het in actie te zien", - "I5DYM+": "Leer EN reflecteer", - "HXvk56": "Statusupdates plaatsen", - "HGdWwZ": "Taken maken en toewijzen", - "GjCS6U": "Kies een sjabloon", - "GG1yhI": "Er zijn sjablonen voor een reeks gebruikssituaties en gebeurtenissen. Je kan een playbook gebruiken zoals het is, of het aanpassen en het vervolgens delen met uw team.", - "GAuN6w": "Veronderstellingen opstellen", - "9m0I/B": "Belanghebbenden op de hoogte houden", - "8n24G2": "Bekijk uitvoeringsdetails in een zijpaneel", - "6GTzTR": "Bekijk op elk moment wat er in deze playbook staat", - "KXVV4+": "Welkom op de playbook preview pagina!", - "9a9+ww": "Titel", - "69nlA3": "Voer een tijdsduur in, in het formaat: dd:hh:mm (bijv. 12:00:00).", - "NMxVd+": "Vul de meetwaarde in.", - "NLeFGn": "aan", - "MHzP9I": "Stel een bericht op om gebruikers te verwelkomen die het kanaal binnenkomen.", - "MBNMo9": "Kanaalacties", - "M4gAc9": "Waarde toevoegen", - "DPj6DM": "Kies Uitvoering om het in actie te zien.", - "B3Q5mz": "Trigger", - "5AJmOz": "Wanneer een gebruiker het kanaal binnenkomt", - "0RlzlZ": "Stuur een tijdelijk welkomstbericht naar de gebruiker", - "Y4MU/9": "Kies Start een testuitvoering om het in actie te zien.", - "Vf/QlZ": "Waardebereik", - "RUlvbf": "Test je nieuwe palybook uit!", - "NiAH1z": "Streefwaarde", - "xVyHgP": "Start een testuitvoering", - "u7qh13": "Ben je klaar om naar je playbook uit te voeren?", - "ru+JCk": "Gemiddelde waarde", - "p1I/Fx": "We hebben je uitvoering automatisch gemaakt", - "mvZUm3": "Hier kunt je jouw playbook-componenten in detail bekijken. Selecteer Bewerken om jouw afspeelboek aan te passen aan jouw processen en modellen.", - "lbs7UO": "per uitvoering over de laatste 10 uitvoeringen", - "l5/RKZ": "Er zijn geen afgewerkte uitvoeringen voor dit playbook .", - "hCMWC+": "Begin met volgen voor {followers, plural, =0 {geen} =1 {een gebruiker} other {# gebruikers}}", - "fmbSyg": "Voeg waarde toe (in dd:hh:mm)", - "efeNi1": "gemiddelde waarde na 10 uitvoeringen", - "c23IHq": "Met kanaalacties kan je activiteiten voor dit kanaal automatiseren", - "awG90C": "Doel per uitvoering", - "ao44YC": "Meetwaarden configureren", - "ZNNjWw": "Voer een nummer in.", - "u4L4yd": "Je hebt niet opgeslagen wijzigingen", - "e3z3P8": "Wijzigingen negeren en pagina verlaten", - "Ob5cSv": "Wijzigingen die je gemaakt hebt, worden niet opgeslagen als je deze pagina verlaat. Weet je zeker dat u de wijzigingen wilt verliezen en de pagina wilt verlaten?", - "dCtjdj": "Klaar om je playbook uit te voeren?", - "Z3ybv/": "Voeg het kanaal toe aan een zijbalk-categorie voor de gebruiker", - "Ek1Fx2": "Wanneer een bericht met deze sleutelwoorden wordt geplaatst", - "9j5KzL": "Categorienaam invoeren", - "2Q5PhZ": "Verzoek om een playbook uit te voeren", - "+PMJAg": "Begin met volgen voor {followers, plural, =1 {één gebruiker} other {# gebruikers}}", - "+/x2FM": "Selecteer een playbook", - "Ppx673": "Verslagen", - "zWgbGg": "Vandaag", - "mLrh+0": "Geen vervaldatum", - "iMjjOH": "Volgende week", - "aEhjYg": "Doel", - "MtrTNy": "Morgen", - "MbapTE": "{num} {num, plural, =1 {taak} other {taken}} overtijd", - "I7+d55": "Geef een datum/tijd op (\"over 4 uur\", \"1 mei\"...)", - "AF7+5o": "Vervaldatum toevoegen", - "W0aij2": "Toewijzen aan...", - "UlJJ1i": "Slash-opdracht toevoegen", - "mw9jVA": "Voeg een titel toe", - "lyXljU": "Dubliceer taak", - "lglICE": "Voeg een beschrijving toe (optioneel)", - "oBeKB4": "Vervalt op {date}", - "lkv547": "Vervaldatum (beschikbaar in het Professional plan)", - "g9pEhE": "Vervallen", - "TTIQ6E": "Wijs deadlines toe aan taken, zodat degenen die de taken toegewezen krijgen prioriteiten kunnen stellen en de dingen gedaan krijgen.", - "NFyWnZ": "Werk efficiënter", - "371AC3": "De samenvatting van de uitvoering bijwerken", - "oAJsne": "Publiek playbook", - "mm5vL8": "Alleen uitgenodigde leden", - "lJ48wN": "Privé playbook", - "Xgxruo": "Checklist overslaan", - "RQl8IW": "Snooze voor…", - "9trZXa": "Iedereen in het team kan kijken", - "7P5T3W": "Checklist opnieuw instellen", - "OqCzNb": "Voeg een taak toe", - "JcefuP": "Voeg een beschrijving toe (optioneel)", - "v5/Cox": "Dupliceer checklist", - "mCrdeS": "Totaal aantal Playbook uotvoeringen", - "cyR7Kh": "Terug", - "XF8rrh": "Copy link to \"{name}\"", - "MyIJbr": "Inhoud", - "IxtSML": "Een checklist toevoegen", - "CwwzAU": "Voeg naam toe van checklist", - "5ZIN3u": "Statusupdates", - "4GjZsL": "Totaal aantal Playbooks", - "k12r+v": "Voeg een samenvattingsjabloon voor de uitvoering toe...", - "RrCui3": "Samenvatting", - "x1phlu": "Geen tijdsbestek", - "kYCbJE": "Tijdsbestek toevoegen", - "c6LNcW": "Taak verwijderen", - "giM/X9": "Elke wordt een statusupdate verwacht. Nieuwe updates worden geplaatst in {channelCount, plural, =0 {geen kanalen} one {# kanaal} other {# kanalen}} and {webhookCount, plural, =0 {geen uitgaande webhooks} one {# uitgaande webhook} other {# uitgaande webhooks}} .", - "aM44Z/": "Kies of specificeer een aangepaste duur…", - "YQOmSf": "Voer één webhook per regel in", - "XRyRzf": "Statusupdates worden niet verwacht.", - "HvAcYh": "{text}{rest, plural, =0 {} one { en een andere} other { en {rest} andere}}", - "DaHpK1": "Een kanaal zoeken", - "28FTjr": "Met Run-acties kan je activiteiten voor dit kanaal automatiseren", - "/RnCQb": "Uitgaande webhook verzenden", - "xHNF7i": "Acties voor uitvoeringen", - "uhDKO8": "Gebruik Markdown om een sjabloon te maken", - "sX5Mn5": "Gelieve één webhook per lijn in te voeren", - "mkLeuq": "Verstuur update naar geselecteerde kanalen", - "kkw4kS": "Deze update wordt uitgezonden naar {hasChannels, select, true {{broadcastChannelCount, plural, =1 {een kanaal} other {{broadcastChannelCount, number} kanalen}}} other {}}{hasFollowersAndChannels, select, true {en } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {één direct bericht} other {{followersChannelCount, number} directe berichten}}} other {}}.", - "kV5GkX": "Wanneer een statusupdate is geplaatst", - "j940pJ": "Deze update zal worden opgeslagen in de overzichtspagina.", - "F9LrJA": "Items filteren", - "zl6378": "Configureer meetwarden in Terugblik", - "sGJpuF": "Voeg een beschrijving toe…", - "aZGAOI": "Voeg een statusupdatesjabloon toe…", - "OuZhcQ": "Specificeer duur (\"8 uur\", \"3 dagen\"...)", - "OKhRC6": "Delen", - "LcC/pi": "Stuur een welkomstbericht…", - "Brya9X": "Voeg een samenvattingsjabloon voor de uitvoering toe…", - "9kQNdp": "Deze playbook is privé.", - "3hBelc": "Een terugblik wordt niet verwacht.", - "yllba1": "Dit gearchiveerde playbook kan niet hernoemd worden.", - "xEQYo5": "Configureer aangepaste meetwaarden om in te vullen met het terugblikrapport.", - "oL7YsP": "Laatst bijgewerkt {timestamp}", - "Z2Hfu4": "Voeg een samenvatting van de uitvoering toe", - "TD8WrM": "Dupliceren is uitgeschakeld voor dit team.", - "OQplDX": "Elke wordt een statusupdate verwacht. Nieuwe updates worden geplaatst in {channelCount, plural, =0 {geen kanalen} one {# kanaal} other {# kanalen}} and {webhookCount, plural, =0 {geen uitgaande webhooks} one {# uitgaande webhook} other {# uitgaande webhooks}} .", - "vSMfYU": "Informatie over de uitvoering", - "opn6uf": "Bekijk Tijdlijn", - "o6N9pU": "Acties voor uitvoeringen", - "lbr3Lq": "Link kopiëren", - "kEMvwX": "Er zijn geen uitvoeringen die aan deze filters voldoen.", - "iigkp8": "Tijd om af te ronden?", - "hjteuA": "Alle Playbooks waartoe je toegang hebt worden hier getoond", - "bf5rs0": "Bekijk Info", - "ZJS10z": "Er zijn nog geen updates geplaatst", - "Q15rLN": "Vraag een update...", - "GXjP8g": "Alle uitvoeringen waartoe je toegang hebt worden hier getoond", - "GDCpPr": "Recente status update", - "+qDKgW": "Bekijk alle updates", - "ocYb9S": "Belangrijkste meetwaarden", - "nc8QpJ": "Recente activiteit", - "m/KtHt": "Je hebt geen rechten om de eigenaar te wijzigen", - "RnOiCg": "Het was niet mogelijk om de uitvoering {isFollowing, select, true {niet langer te volgen} other {te volgen}}", - "4mCpAv": "Het was niet mogelijk om de eigenaar te wijzigen", - "GVpA4Q": "Nieuw Playbook maken", - "CFysvS": "Maak Playbook keuzemenu", - "/qDObA": "Bladeren door uitvoeringen", - "KeO51o": "Kanaal", - "JvEwg/": "Het was niet mogelijk om een update te vragen", - "Jli9m7": "Er zal een bericht naar het kanaal van de uitvoering worden gestuurd met het verzoek een update te posten.", - "J2NmIY": "Bevestig betrokkenheid", - "9xs0pp": "Waarde toevoegen...", - "3O8M5M": "Verzoek is verzonden naar het kanaal van de uitvoering.", - "1fXVVz": "Vervaldatum...", - "1GOpgL": "Aangeduide...", - "/+8SGX": "Weergave van {filteredNum} van {totalNum} gebeurtenissen", - "Nf9oAA": "Je staat op het punt om lid te worden van deze uitvoering.", - "NGqzDU": "Updateverzoek bevestingen", - "LfhTNW": "Playbooks en uitvoeringen doorbladeren of aanmaken", - "5PpBsd": "Jouw verzoek was niet succesvol.", - "4Iqlfe": "Je sloot aan bij deze uitvoering.", - "wGp7l3": "{icon} Dollars", - "s+rSpl": "{icon} Geheel getal", - "qp5G0Z": "Upgrade is vereist voor toegang tot terugblikfuncties.", - "ojQue/": "{icon} Duur (in dd:hh:mm)", - "mNgqXf": "Om deze functie te ontgrendelen:", - "j2VYGA": "Bekijk alle Playbooks", - "PWmZrW": "Bekijk alle uitvoeringen", - "PW+sL4": "nvt", - "KzHQCQ": "Er zijn geen afgewerkte uitvoeringen die aan die filters voldoen.", - "CUhlqp": "handleiding tour tip product afbeelding", - "5HXkY/": "Type: {typeTitle}", - "3zF589": "Reset naar alle {filterName}", - "zW/5AB": "Professional functie Dit is een betaalde functie, beschikbaar met een gratis 30-dagen proefversie", - "vDvWJ6": "Probeer updates verzoeken met een gratis proefversie", - "u6Fyic": "Jouw verzoek is verzonden naar het kanaal van de uitvoering.", - "pzTOmv": "Volgers", - "pXWclp": "Jouw verzoek om deel te nemen zal naar het uitvoeringskanaal worden gestuurd.", - "pFK6bJ": "Alle bekijken", - "lr1CUA": "Playbooks doorbladeren", - "lKeJ+i": "Er is geen samenvatting", - "jboo9u": "Vraag een update", - "ch4Vs1": "Vraag met één klik updates aan voor playbook-uitvoeringen en krijg direct bericht als er een update is gepost. Begin een gratis proefperiode van 30 dagen om het uit te proberen.", - "Xx0WZV": "Bericht versturen", - "VpQKQE": "{displayName} is geen deelnemer van de uitvoering. Wilt je hen een deelnemer maken? Zij zullen toegang hebben tot de gehele berichtengeschiedenis in het uitvoeringskanaal.", - "Ul0aFX": "Playbook importeren", - "UePrSL": "{num} {num, plural, one {Deelnemer} other {Deelnemers}}", - "UMFnWV": "Terugblik bekijken", - "U8u4uF": "Wordt betrokken", - "SMrXWc": "Favorieten", - "RCT0Px": "Voeg @{displayName} toe aan het kanaal", - "PdRg+3": "Alle bekijken...", - "P9PKvb": "Een bericht werd naar het uitvoeringskanaal gestuurd.", - "P6NEL/": "Opdracht...", - "xfnuXm": "Deelnemen", - "wRM2AO": "De update aanvraag is mislukt.", - "ePhhuK": "Je verzoek is naar het uitvoeringskanaal gestuurd.", - "b+DwLA": "Verzoek om deel te nemen aan deze uitvoering.", - "PoX2HN": "Verzoek verzenden", - "OfN7IN": "Een statusupdate-verzoek zal naar het uitvoeringskanaal gestuurd worden.", - "Gwmqz5": "Om een update verzoeken", - "CV1ddt": "Aan de uitvoering deelnemen", - "B9z0uZ": "Jouw verzoek om deel te nemen aan de uitvoering was niet succesvol.", - "AH+V3r": "Neem deel aan de uitvoering.", - "+6DCr9": "Als deelnemer kan je statusupdates plaatsen, taken toewijzen en voltooien, en terugblikken uitvoeren.", - "wBZz47": "Je hebt de uitvoering verlaten.", - "gfUBRi": "Wijs een nieuwe eigenaar aan voordat je de uitvoering verlaat.", - "fnihsY": "Verlaten", - "a1vQ5Q": "Verlaten bevestigen", - "SK5APX": "Het was niet mogelijk om de uitvoering te verlaten.", - "N9CTUJ": "Uitvoering verlaten", - "F/HKIy": "Weet je zeker dat je de uitvoering wilt verlaten?", - "mttASm": "Verlaten en het niet langer volgen", - "lpWBJE": "Verlaten en het niet langer volgen bevestingen", - "hnYSP3": "Wanneer je een uitvoering verlaat en niet langer volgt, wordt deze verwijderd uit de linkerzijbalk. Je kan deze terugvinden door alle uitvoeringen te bekijken.", - "XS4umx": "{name} snoozde een status-update", - "5Hzwqs": "Markeren als favoriet", - "Mjq//Y": "Markeren als favoriet ongedaan maken", - "AhY0vJ": "Verlaten en niet langer volgen", - "Xm0L7N": "Wanneer een status update gepost wordt, of een terugblik gepubliceerd wordt", - "Suyx6A": "De playbook-import is mislukt. Controleer of JSON geldig is en probeer het opnieuw.", - "QegBKq": "Deelnemen aan Playbook", - "Q4sutg": "Bevestig verlaten{isFollowing, select, true { en niet langer volgen} other{}}", - "P6PLpi": "Deelnemen", - "FgydNe": "Bekijken", - "iEtImk": "Wanneer je {isFollowing, select, true { en een uitvoering niet langer volgt} other { een uitvoering}} verlaat, wordt deze verwijderd uit de linkerzijbalk. Je kan deze terugvinden door alle uitvoeringen te bekijken.", - "cnfVhV": "De uitvoering verlaten {isFollowing, select, true { en niet langer volgen } other {}}", - "vqmRBs": "Bevestig herstarten uitvoering", - "qGlwfc": "Uitvoering starten", - "k5EChD": "Weet je zeker dat je deze uitvoering wil herstarten?", - "j2FnDV": "Er zal een kanaal aangemaakt worden met deze naam", - "iQhFxR": "Laatst gebruikt", - "Zg0obP": "Uitvoering opnieuw starten", - "KjNfA8": "Ongeldige tijdsduur", - "03oqA2": "Actieve uitvoeringen", - "XnICdK": "Het was niet mogelijk om je toe te voegen aan de uitvoering", - "unwVil": "Het verzoek om lid te worden van het kanaal was niet succesvol.", - "ZRv7Dm": "Verzoek om lid te worden", - "M9tXoZ": "Een toetredingsverzoek wordt naar het kanaal gestuurd.", - "0QD99o": "Verzoek om lid te worden van het kanaal", - "q48ca7": "Geef feedback over Playbooks.", - "fVMECF": "Deelnemer", - "bCmvTY": "Feedback geven", - "FLG4Iu": "Eigenaar maken van uitvoering", - "6rygzu": "Uit uitvoering verwijderen", - "0Azlrb": "Beheren", - "/GCoTA": "Wissen", - "wCDmf3": "Updates inschakelen", - "w4Nhhb": "Deelnemer toevoegen", - "utHl3F": "Mensen toevoegen aan {runName}", - "qDxsQH": "Word deelnemer om met deze uitvoering in interactie te gaan", - "nsd54s": "Bevestig het uitschakelen van statusupdates", - "l/W5n7": "Deelnemers zullen ook worden toegevoegd aan het kanaal dat aan deze uitvoering gekoppeld is", - "jrOlPO": "Ontvang meldingen over updates van de uitvoeringsstatus", - "jAo8dd": "Statusupdates uitvoeren uitgeschakeld door {name}", - "cpGAhx": "Weet je zeker dat je statusupdates wilt uitschakelen voor deze uitvoering?", - "cUCiWw": "Word een deelnemer", - "b8Gps8": "Statusupdates van uitvoering ingeschakeld door {name}", - "WFA0Cg": "Weet je zeker dat je statusupdates wilt inschakelen voor deze uitvoering?", - "WC+NOj": "Voeg ook mensen toe aan het kanaal gekoppeld aan deze uitvoering", - "H7IzRB": "Statusupdates uitschakelen", - "9qqGGd": "Deelnemers uitnodigen", - "1prgB2": "Mensen zoeken", - "1OluNs": "Bevestig het inschakelen van statusupdates", - "1OVPiC": "Word deelnemer aan de uitvoering. Als deelnemer kan je statusupdates plaatsen, taken toewijzen en voltooien en terugblikken uitvoeren.", - "//o1Nu": "Schakel updates uit", - "Z18I+c": "Met kanaalacties kan je activiteiten voor dit kanaal automatiseren", - "Y1EoT/": "Als een deelnemer de uitvoering verlaat", - "VM75su": "{name} verwijderde {num} deelnemers van de uitvoering", - "SwlL5j": "@{user} neemt mee deel aan de uitvoering", - "RXjd3Q": "{name} verwijderde @{user} van de uitvoering", - "5b1zuB": "Voeg ze toe aan het uitvoerkanaal", - "u/yGzS": "{name} voegde @{user} toe aan de run", - "t6lwwM": "{requester} verwijderde {users} uit de uitvoering", - "lqzBNa": "Verwijder ze uit het uitvoeringskanaal", - "jfpnye": "@{user} verliet de uitvoering", - "ieL3dC": "Kanaalacties instellen", - "ha1TB3": "Als een deelnemer toetreeedt tot de uitvoering", - "feNxoJ": "{requester} voegde {users} toe aan de uitvoering", - "ecS/qx": "{name} heeft {num} deelnemers aan de uitvoering toegevoegd", - "zSOvI0": "Filters", - "qxYWTy": "Toon alle taken van uitvoeringen waar ik eigenaar van ben", - "meD+1Q": "DEELNEMERS AAN DE UITVOERING", - "grv9Fm": "Selecteer om te wisselen tussen een lijst van taken.", - "YBvwXR": "Geen toegewezen taken", - "WFd88+": "Geef gecontroleerde taken weer", - "TnUG7m": "Je hebt geen toegewezen openstaande taken.", - "SRqpbI": "{assignedNum, plural, =0 {geen toegewezen taken} other {# toegewezen taken}}", - "L6vn9U": "Deelnemers aan de uitvoering", - "I0NIMp": "Jouw taken", - "Gg/nch": "GEEN DEELNEMER VAN", - "DUU48k": "Er is geen taak expliciet aan jou toegewezen. Je kan jouw zoekopdracht uitbreiden met behulp van de filters.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# overtijd}}", - "36NwLv": "Deelnemerslijst beheren", - "iH5e4J": "Je wordt ook toegevoegd aan het kanaal dat aan deze uitvoering gekoppeld is.", - "fBG/Ge": "Kost", - "dK2JKl": "Link aan een bestaand kanaal", - "VjJYEV": "bijv. verkoopimpact, aankopen", - "UAS7Bn": "Vraag toegang tot het kanaal gekoppeld aan deze uitvoering", - "NGKqOC": "Voeg me ook toe aan het kanaal gekoppeld aan deze uitvoering", - "IdTL+v": "Maak een uitvoeringskanaal", - "BJNrYQ": "Als deelnemer kan je het overzicht bijwerken, taken afvinken, statusupdates plaatsen en de terugblik bewerken.", - "9X3jwi": "{icon} Kost", - "2BCWLD": "Kanaal configureren", - "RC6rA2": "Recent gemaakt", - "Q/t0//": "Voltooide uitvoeringen", - "NNksk4": "Alfabetisch", - "LKu0ex": "Weet je zeker dat je de uitvoering {runName} voor alle deelnemers wil afwerken?", - "L1tFef": "Controleer de spelling of probeer een andere zoekopdracht", - "KQunC7": "Gebruikt in dit kanaal", - "HfjhwE": "Playbooks zoeken", - "zxj2Gh": "Laatst bijgewerkt {time}", - "GZoWl1": "Activiteiten voor deze taak automatiseren", - "EVSn9A": "Uitvoering starten", - "Bgt0C8": "Deze update over de uitvoering {runName} wordt verstuurd naar {hasChannels, select, true {{broadcastChannelCount, plural, =1 {een kanaal} other {{broadcastChannelCount, number} kanalen}}} other {}}{hasFollowersAndChannels, select, true {en } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {één privé-bericht} other {{followersChannelCount, number} privé-berichten}}} other {}}.", - "AoNLta": "Er zijn geen voltooide uitvoeringen gekoppeld aan dit kanaal", - "AG7PKJ": "Uitvoering hernoemen", - "9AQ5FE": "Samenvatting uitvoeren", - "95v+5O": "{actions, plural, =0 {Acties voor taken} one {# actie} other {# acties}}", - "7KMbBa": "Nooit gebruikt", - "3sXVwy": "Acties voor taken...", - "3Yvt4d": "Playbooks zijn configureerbare checklists die een herhaalbaar proces definiëren voor teams om specifieke en voorspelbare resultaten te bereiken", - "2NDgJq": "Meest recente statusupdate", - "0CeyUV": "Geen resultaten voor \"{searchTerm}\"", - "zscc/+": "Er {outstanding, plural, =1 {is # openstaande taak} other {zijn # openstaande taken}}. Weet je zeker dat je de uitvoering {runName} voor alle deelnemers wilt beëindigen?", - "yP3Ud4": "Er zijn geen lopende uitvoeringen gekoppeld aan dit kanaal", - "tqAmbk": "Lopende uitvoeringen", - "prs4kX": "Wanneer een bericht met specifieke trefwoorden wordt geplaatst", - "m8hzTK": "Laatst gebruikt {time}", - "lqceIp": "of Een playbook importeren", - "kQAf2d": "Selecteer", - "gS1i4/": "Markeer de taak als gedaan", - "gGtlrk": "Jouw Playbooks", - "fvNMLo": "Acties voor taken", - "cGCoJe": "Geplaatst door", - "bEoDyV": "@{authorUsername} plaatste een update voor [{runName}]({overviewURL})", - "a2r7Vb": "Privé-kanaal", - "ZSa3cf": "@{targetUsername}, graag een statusupdate voor [{runName}]({playbookURL}).", - "Z1sgPO": "Voltooide uitvoeringen bekijken", - "Wy3sw+": "{count, plural, =1{1 uitvoering actief} =0 {geen uitvoeringen actief} other {# uitvoeringen actief}}", - "W1EKh5": "Nieuw playbook maken", - "VA1Q/S": "Publiek kanaal", - "SRbTcY": "Andere playbooks", - "RgQwWr": "Uitvoeringen sorteren op", - "fwW0T1": "Bevestig verwijderen van vooraf toegewezen leden", - "TP/O/b": "Gebruiker verwijderen", - "QvEO6m": "Je hebt geen rechten om deze uitvoering te bewerken", - "QJTSaI": "Link uitvoering aan een ander kanaal", - "IE2BzH": "Er zijn gebruikers die vooraf zijn toegewezen aan een of meer taken. Het uitschakelen van uitnodigingen zal alle vooraf toegewezen taken wissen.{br}{br}Weet je zeker dat u de uitnodigingen wilt uitschakelen?", - "DQn9Uj": "De gebruiker {name} is vooraf toegewezen aan een of meer taken. Als u deze gebruiker niet automatisch uitnodigt, worden zijn vooraf toegewezen taken gewist.{br}{br}Weet je zeker dat je deze gebruiker niet meer wilt uitnodigen als lid van de uitvoering?", - "BiQjuS": "Uitvoering verplaatst naar {channel}", - "9w0mDI": "Bevestig verwijderen van vooraf toegewezen lid", - "uCS6py": "Je hebt geen toestemming om deze playbook te zien", - "l3QwVw": "Kanaal selecteren", - "ksG35Q": "Je hebt geen rechten om playbooks aan te maken in deze werkruimte.", - "k7Nzfi": "Uitnodiging uitschakelen", - "YKLHXL": "Lopende uitvoeringen bekijken", - "mILd++": "De naam van de uitvoering mag niet langer zijn dan {maxLength} tekens", - "uYrkxy": "Het bestand moet een geldig JSON Playbook sjabloon zijn.", - "m4vqJl": "Bestanden", - "Zbk+OU": "De bestandsgrootte overschrijdt de limiet van 5MB.", - "MieztS": "Sleep een playbook export file om deze te importeren.", - "HGSVzc": "Kan geen meerdere bestanden tegelijk importeren.", - "LaseGE": "Je hebt geen toestemming om deze checklist te bewerken", - "Edy3wX": "Checklist verplaatst naar {channel}", - "8//+Yb": "Koppel checklist aan een ander kanaal", - "706Soh": "uitgevoerde taken", - "XHJUSG": "Automatisch uitvoeringen volgen", - "DqTQOp": "Eenmalig", - "vjb+hS": "{user} zette checklistitem \"{name}\" terug", - "OqWwvQ": "{user} vinkte checklistitem \"{name}\" uit", - "DKiv0o": "{user} sloeg checklist item \"{name}\" over", - "9M92On": "Kanalen selecteren", - "8FzC0B": "{user} vinkte checklistitem \"{name}\" af", - "3qPQMX": "{name} vroeg om een statusupdate", - "N7Ln74": "Opnieuw uitvoeren", - "DoskyC": "Alle teams", - "9S+ZiL": "Geen team geselecteerd" -} diff --git a/webapp/playbooks/i18n/pl.json b/webapp/playbooks/i18n/pl.json deleted file mode 100644 index 550fd4797db..00000000000 --- a/webapp/playbooks/i18n/pl.json +++ /dev/null @@ -1,840 +0,0 @@ -{ - "viXE32": "Prywatny", - "v3+TmO": "{members, plural, =0 {Nikt} =1 {Jedna osoba} other {#osoby}} mogą uzyskać dostęp do tego playbooka", - "uhu5aG": "Publiczny", - "uJ3bRR": "Ten szablon pomaga ustandaryzować format zwięzłego opisu, który wyjaśnia każde uruchomienie zainteresowanym stronom.", - "t6SiGO": "Aktualne Uruchomienia", - "soePYH": "{num_checklists, plural, =0 {brak list wyboru} one {# lista wyboru} other {# listy wyboru}}", - "sQu1rA": "{numTotalRuns, plural, =0 {brak rozpoczętych uruchomień} =1 {# rozpoczęte uruchomienie} other {# rozpoczęte uruchomiania}}", - "s3jjqi": "{num_actions, plural, =0 {brak działań} one {# działanie} other {# działania}}", - "recCg9": "Aktualizacje", - "rX08cW": "Data musi być przyszła.", - "oS0w4E": "Domyślny timer aktualizacji", - "lbhO3D": "kursywa", - "lZwZi+": "Dzień: {date}", - "jvo0vs": "Zapisz", - "jq4eWU": "Dostęp do Playbooka", - "jXT2++": "Przejdź do kanału", - "jIIWN+": "wstępnie sformatowany", - "hzt6l8": "Użyj Markdown do stworzenia szablonu.", - "hXIYHG": "Zainstaluj i włącz wtyczkę Channel Export do obsługi eksportu kanału", - "gy/Kkr": "(edytowany)", - "g5pX+a": "O nas", - "eiPBw7": "Interwał przypomnień retrospektywnych", - "ebkl6I": "Każdy w tym zespole może mieć dostęp do tego playbooka", - "eHAvFf": "pogrubiony", - "djXM+y": "Dostęp mają tylko wybrani użytkownicy.", - "dcV/DJ": "{timestamp}", - "d9epHh": "Eksportuj dziennik kanału", - "c8hxKk": "Tydzień {date}", - "bPLen5": "Uruchomienia ukończone w ciągu ostatnich 30 dni", - "bLK+Kr": "Przypomina na kanale w określonym odstępie czasu o konieczności wypełnienia retrospektywy.", - "b40Pr7": "Reporter", - "avPeEI": "Aktualizuj, aby zobaczyć trendy dla wszystkich uruchomień, aktywnych uruchomień oraz uczestników zaangażowanych w tego playbooka.", - "YDuW/T": "{num_runs, plural, =0 {Jeszcze nie uruchomiony} one {# uruchomiony} other {# wszystkie uruchomione}}", - "XmUdvV": "Wszystkie statystyki, których potrzebujesz", - "VmpFFw": "Brak dostępnego opisu.", - "UbTsGY": "Uruchomienia rozpoczęte między {start} a {end}", - "TZYiF/": "przekreślenie", - "TSSNg/": "OGÓŁEM URUCHOMIEŃ rozpoczętych tygodniowo w ciągu ostatnich 12 tygodni", - "TJo5E6": "Podgląd", - "T7Ry38": "Wiadomość", - "T5rX+W": "Jak często powinny być zamieszczane aktualizacje?", - "SFuk1v": "Uprawnienia", - "SENRqu": "Pomoc", - "RthEJt": "Retrospektywnie", - "RoGxij": "Będzie aktywne w dniu {date}", - "R+JQaJ": "Członkowie kanału", - "QnZAit": "Dodaj opcjonalny opis", - "QiKcO7": "Wprowadź szablon retrospektywy", - "Q8Qw5B": "Opis", - "Q7aZO4": "{numParticipants, plural, =0 {brak aktywnych uczestników} =1 {# aktywny uczestnik} other {# aktywnych uczestników}}", - "ObmjTB": "Polecenie po ukośniku", - "NE1OeI": "Każdy w zespole({team}) ma dostęp.", - "KiXNvz": "Uruchom", - "JCGvY/": "Ten szablon pomaga ustandaryzować format powtarzających się aktualizacji, które mają miejsce podczas każdego uruchomienia.", - "IuFETn": "Czas trwania", - "ICqy9/": "Listy kontrolne", - "HhLp57": "cytat", - "EC5MJD": "Brak dostępnych aktualizacji.", - "DnBhRg": "Dodaj Osoby", - "DCl7Vv": "kod w jednej linii", - "CL5OZP": "Tylko wybrani przez Ciebie użytkownicy będą mogli edytować lub uruchamiać ten playbook.", - "BD66u6": "Pobierz plik CSV zawierający wszystkie wiadomości z kanału", - "AS5kar": "Uczestnicy ({participants})", - "AF9wda": "Ta aktualizacja zostanie zapisana na stronie Przeglądu{hasBroadcast, select, true {i zostanie rozesłana do {broadcastChannelCount, plural, =1 {jednego kanału} other {{broadcastChannelCount, number} kanałów}}} other {}}.", - "A3ptul": "Szablony", - "9uOFF3": "Przegląd", - "6Lwe7T": "Każdy w {team} może mieć dostęp do tego playbooka", - "5Ot7cd": "Określ typ kanału, jaki utworzy ten playbook.", - "5FRgqE": "Pobieranie logu kanału", - "1MQ3XZ": "{numActiveRuns, plural, =0 {brak aktywnych działań} =1 {# aktywne działanie} other {# aktywne działania}}", - "/jUtaM": "AKTYWNE DZIAŁANIA dziennie w ciągu ostatnich 14 dni", - "/HtNUp": "Wybierz lub określ {mode, select, DurationValue {przedział czasu (\"4 godziny\", \"7 dni\"...)} DateTimeValue {czas (\"za 4 godziny\", \"1 maja\", \"jutro o 13:00\"...)} other {czas lub zakres czasu}}", - "z5RMPO": "Tylko Ty możesz uzyskać dostęp do tego playbooka", - "wbsq7O": "Wykorzystanie", - "5A46pW": "Dodaj polecenie z ukośnikiem", - "47FYwb": "Anuluj", - "3rCdDw": "Aktualizacje statusu", - "1I48bs": "Szablon retrospektywny", - "wEQDC6": "Edytuj", - "waVyVY": "Obecnie aktywni uczestnicy", - "wbwhbH": "Nazwa zadania", - "zINlao": "Właściciel", - "/1FEJW": "AKTYWNI UCZESTNICY dziennie w ciągu ostatnich 14 dni", - "+ZIXOR": "Dostęp do kanału", - "+8G9qr": "Domyślny tekst dla retrospektywy.", - "X3DLGJ": "Każdy w tym obszarze roboczym może tworzyć playbooki.", - "TyrY2b": "Tworzenie playbooka", - "D3idYv": "Ustawienia", - "AT2QBo": "Tylko wybrani użytkownicy mogą tworzyć playbooki.", - "zy3cJT": "Prompt do uruchomienia tego playbooka, gdy użytkownik napisze wiadomość zawierającą słowa kluczowe", - "wL7VAE": "Akcje", - "usa8vQ": "Wyślij wiadomość powitalną", - "nvy0pS": "Po zakończeniu uruchomienia należy wyeksportować kanał", - "lxfpbh": "Właściciel {reminderEnabled, select, true {będzie monitowany o aktualizację statusu co} other {nie będzie monitowany o aktualizację statusu}}", - "jS/UOn": "Aktualizacja szablonu", - "hO9EdA": "Zaproś {numInvitedUsers, plural, =0 {członków} =1 {członka} other {# członków}} na kanał", - "bGhCLX": "Kiedy aktualizacja jest wysyłana", - "b5FaCc": "Dodaj kanał do kategorii na pasku bocznym", - "Z/hwEf": "Kanał będzie otrzymywał przypomnienie o wykonaniu retrospektywy {reminderEnabled, select, true {w każdy} other {}}", - "Ui6GK/": "Kiedy nowy członek dołącza do kanału", - "SDSqfA": "Kiedy rozpoczyna się uruchomienie", - "OINwWS": "Tworzenie kanłu {isPublic, select, true {publicznego} other {prywatnego}}", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {zadanie} other {zadania}}", - "LRFvqz": "Ogłoszenie na {oneChannel, plural, one {kanale} other {kanałach}}", - "KUr+sG": "Aktualizacja podsumowania uruchomienia", - "Hzwzgs": "Rozsyłanie aktualizacji na {oneChannel, plural, one {kanale} other {kanałach}}", - "CjNrqO": "Wzór raportu retrospektywnego", - "8hDbW6": "Wyślij wychodzący webhook", - "+QgvjN": "Przypisanie roli właściciela do", - "zWkvNO": "Oś Czasu", - "zELxbG": "Zapisane wiadomości", - "yhzuSC": "Czas: {time}", - "x5Tz6M": "Raport", - "w7tf2z": "Opublikowany", - "vndQuC": "Wykonane Polecenie po Ukośniku", - "vOFN0m": "Post o statusie usunięty:", - "v1SpKO": "Zmiana ról", - "v1DNMW": "Retrospektywa opublikowana przez {name}", - "syEQFE": "Opublikuj", - "pKLw8O": "Czy na pewno chcesz usunąć to wydarzenie? Usunięte wydarzenia zostaną trwale usunięte z osi czasu.", - "o2eHmz": "Uruchomienie zakończone przez {name}", - "jnmORb": "W tym playbooku", - "fXGjhC": "Właściciel zmieniony z {summary}", - "fUEpLA": "Nie ma żadnych wydarzeń na osi czasu pasujących do tych filtrów.", - "egvJrY": "Zmieniony Adresat", - "aACJNp": "Utuchomienie rozpoczęte przez {name}", - "ZwlIYH": "{activeRuns, number} aktywne {activeRuns, plural, one {uruchomienie} other {uruchomienia}}", - "W9j0FJ": "{date}", - "TvihSy": "Ponownie opublikuj", - "OsDomv": "Wszystkie wydarzenia", - "OcpRSQ": "Usuń Wpis", - "N1U/QR": "Zmiany stanu zadania", - "MvEydR": "{name} zamieścił aktualizację statusu", - "LmhSmU": "Potwierdź Usunięcie", - "JeqL8w": "Retrospektywa odwołana przez {name}", - "I2zEie": "Świętuj sukces i ucz się na błędach dzięki raportom retrospektywnym. Filtrowanie zdarzeń na osi czasu w celu przeglądu procesu, zaangażowania interesariuszy i celów audytowych.", - "DXACD6": "Opublikuj raport retrospektywny i uzyskaj dostęp do osi czasu", - "ArpdYl": "Zdarzenia na osi czasu są wyświetlane tutaj w miarę ich występowania. Najedź kursorem na zdarzenie, aby je usunąć.", - "AML4RW": "Przydziały zadań", - "9Obw6C": "Filtr", - "4Hrh5B": "{name} zmienił status z {summary}", - "3/wF0G": "Polecenia po ukośniku", - "izWS4J": "Wyłącz obserwowanie", - "ieGrWo": "Obserwuj", - "FEGywG": "Proszę określić przyszłą datę/czas przypomnienia o aktualizacji.", - "2Qq4YX": "Czy na pewno chcesz odrzucić wprowadzone zmiany?", - "2QkJ4s": "Zapisuj ważne wiadomości, aby uzyskać pełny obraz, który usprawnia retrospektywy.", - "2PNrBQ": "Wyeksportuj kanał uruchomienia playbooka i zapisz go do późniejszej analizy.", - "15jbT0": "Dodaj więcej do swojej osi czasu", - "0wJ7N+": "Zadanie", - "0oLj/t": "Rozwiń", - "/YZ/sw": "Rozpoczęcie wersji trial", - "/MaJux": "Rozpocznij retrospektywę", - "+hddg7": "Dodaj do osi czasu uruchomienia", - "rDvvQs": "{completed, number} / {total, number} gotowe", - "nqVby7": "{numTasksChecked, number} z {numTasks, number} {numTasks, plural, =1 {zadanie} other {zadania}} sprawdzone", - "kDcpd/": "{numKeywords, plural, other {# słowa kluczowe}}", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {przebieg} other {przebiegów}} w trakcie", - "x8cvBr": "Zobacz przegląd uruchomień", - "wcWpGs": "Nieprawidłowy URL webhooka", - "w0muFd": "Wyślij wychodzący webhook (jeden na linię)", - "twieZh": "Przejdź do przeglądu uruchomienia", - "sqNmlF": "Pomiń retrospektywę", - "zz6ObK": "Przywróć", - "zx0myy": "Uczestnicy", - "ypIsVG": "Przywróc zadanie", - "z3A0LP": "Ostatni przebieg trwał {relativeTime}", - "yxguVq": "Odrzuc zmiany", - "yqpcOa": "Użyj", - "yhU1et": "Zadania", - "xvBDOH": "Czy na pewno chcesz zarchiwizować playbook {title}?", - "xmcVZ0": "Szukaj", - "wsUmh9": "Zespół", - "wX3k9U": "Playbook bez tytułu", - "wO6NOM": "Czy na pewno chcesz Przywrócić to zadanie? To Zadanie zostanie dodane do tego przebiegu", - "wZ83YL": "Nie teraz", - "vir0m9": "Nieprawidłowa nazwa kategorii.", - "vNiZXF": "W tej chwili nie ma żadnych uruchomień. Uruchom playbook, aby rozpocząć organizowanie przepływów pracy dla swojego zespołu i narzędzi.", - "v8ZnNc": "Wybierz zespół", - "uny3Zy": "Playbooki", - "uBLF+D": "Czym jest playbook?", - "u4MwUB": "Zapisz swoją historię uruchomień z playbooka", - "tzMNF3": "Status", - "scYyVv": "Czy chciałbyś wypełnić raport retrospektywny?", - "sVlNlY": "Struktura każdego zespołu jest inna. Możesz zarządzać, którzy użytkownicy w zespole mogą tworzyć playbooki.", - "sIX63S": "Twój Administrator Systemu został powiadomiony", - "sDKojV": "Archiwizuj playbook", - "ryrP8K": "Zarządzaj uprawnieniami określającymi, kto może wyświetlać, modyfikować i uruchamiać ten playbook.", - "rbrahO": "Zamknij", - "qyJtWy": "Pokaż mniej", - "qp3Fk4": "Playbook to przepływ pracy, którego powinny przestrzegać Twoje zespoły i narzędzia, w tym wszystko, od list kontrolnych, działań, szablonów i retrospektyw.", - "q6f8x9": "Zmiana od ostatniej aktualizacji", - "prYDT6": "Kanał ogłoszeń", - "pjt3qA": "Nowa lista kontrolna", - "oVHn4s": "Ostatnia aktualizacja", - "nmpevl": "Odrzuć", - "nkCCM2": "Nie otrzymasz ponownie przypomnienia.", - "lrbrjv": "Tak, rozpocznij retrospektywę", - "lJyq2a": "Nie znaleziono uruchomienia", - "l7zMH6": "Wybierz opcję lub określ niestandardowy czas trwania", - "l0hFoB": "Dodaj opis playbooka...", - "kvgvNW": "Wiedz, co się stało", - "kXFojL": "Możesz również utworzyć playbook z wyprzedzeniem, aby był dostępny, gdy go potrzebujesz.", - "kGI46P": "Opis zadania", - "iNU1lj": "Uruchomienie, o które prosisz, jest prywatne lub nie istnieje.", - "jIgqRa": "Właściciel / Uczestnicy", - "hVFgh4": "Uwzględnij gotowe", - "h+e7G+": "Prośba o uruchomienie tego poradnika, gdy wiadomość zawiera {numKeywords, select, 1 {theword} inne {co najmniej jedno z nich}}", - "fV6578": "Przypisz rolę właściciela", - "9PXW6Q": "Czas trwania / Rozpoczęty", - "D2CE02": "Wprowadź webhook", - "j7jdWG": "Konwertuj do edycji komercyjnej.", - "k9q07e": "Transmisja aktualizacji na inne kanały", - "jwimQJ": "Ok", - "ijAUQf": "Powiadom Administratora Systemu o uaktualnieniu.", - "fmylXu": "Monituj o uruchomienie playbooka, gdy użytkownik opublikuje wiadomość", - "gt6BhE": "Szczegóły uruchomienia", - "g4IF1x": "Brak uruchomień dla tego playbooka.", - "hrgo+E": "Archiwizuj", - "hfrrC7": "Inicjały Zespołu", - "guunZt": "Przypisz", - "edxtzC": "Utwórz playbook", - "eLeFE2": "Edytuj nazwę i opis", - "eKv7yX": "Opublikuj", - "dvhvum": "(Opcjonalnie) Opisz, jak należy korzystać z tego playbooka", - "dsTLW1": "Edytuj zadania", - "e/AZL5": "Rozpoczął się Twój 30-dniowy okres próbny", - "dSC1YD": "Pomiń zadanie", - "b3TdyZ": "Klikając Rozpocznij okres próbny, wyrażam zgodę na Umowę dotyczącą ewaluacji oprogramowania Mattermost, Politykę prywatności i otrzymywanie wiadomości e-mail dotyczących produktu.", - "d8KvXJ": "Twoja licencja próbna wygasa {expiryDate}. Możesz kupić licencję w dowolnym momencie za pośrednictwem Portalu klienta, aby uniknąć jakichkolwiek zakłóceń.", - "bE1Cro": "Tylko moje uruchomienia", - "b/QBNs": "Termin aktualizacji", - "aYIUar": "Dziękuję!", - "aWpBzj": "Pokaż więcej", - "ZdWYcm": "Nie, pomiń retrospektywę", - "ZWtlyd": "Uruchomienie przywrócone przez {name}", - "ZAJviT": "Nie byliśmy w stanie powiadomić Administratora Systemu.", - "Z7vWDQ": "Wystąpił błąd", - "YORRGQ": "Opublikuj aktualizację", - "YMrTRm": "Podsumowanie Przebiegu", - "YKn+7s": "Ten kanał nie ma uruchomionego żadnego playbooka.", - "XXbWAU": "Wybierz tę opcję, aby automatycznie otrzymywać aktualizacje po uruchomieniu tego playbooka.", - "W/V6+Y": "Zwiń", - "VmnoW8": "Sprawdź logi systemowe, aby uzyskać więcej informacji.", - "VOzlSL": "Prowadzenie playbooka porządkuje przepływy pracy dla Twojego zespołu i narzędzi.", - "V5TY0z": "Dodaj uczestników?", - "S0kWcH": "Zaległa aktualizacja", - "Qrl6bQ": "Usprawnij swoje procesy dzięki playbookom", - "Nh91Us": "{from, number}–{to, number} z {total, number} ogółem", - "X/koAN": "Nieprawidłowy wpis: maksymalna dozwolona liczba webhooków to 64", - "WTQpnI": "Podejmij działanie teraz, korzystając z playbooków", - "WIxhrv": "Nazwa uruchomienia musi mieć co najmniej dwa znaki", - "WAHCT2": "Powiadom Administratora Systemu", - "W1Qs5O": "Uruchomienia", - "TBez4r": "Nie ma playbooków do wyświetlenia. Nie masz uprawnień do tworzenia playbooków w tym obszarze roboczym.", - "TdTXXf": "Dowiedź się więcej", - "SmAUf9": "Przypomnienie zostanie wysłane {timestamp}", - "TDaF6J": "Odrzuć", - "Rgo4VW": "Każdy w tym obszarze roboczym może tworzyć playbooki. Administratorzy systemu mogą zmienić to ustawienie.", - "R4vA+C": "Tylko użytkownicy poniżej mogą tworzyć playbooki. Ci użytkownicy, a także administratorzy systemu, mogą zmienić to ustawienie.", - "QaZNp9": "Zakończ uruchomienie", - "Q7hMnp": "Uruchomienie Playbooka", - "Q67RuY": "Zobacz wszystkie przebiegi", - "OK8u0r": "Stwórz playbook, aby określić przepływ pracy, którego powinny przestrzegać Twoje zespoły i narzędzia, w tym wszystko, od list kontrolnych, działań, szablonów i retrospektyw.", - "Oo5sdB": "Nazwa playbooka", - "M/2yY/": "Nikt jeszcze.", - "JJMNME": "{withRunName, select, true {@{authorUsername} opublikował aktualizację dla [{runName}]({overviewURL})} other {@{authorUsername} opublikował aktualizację}}", - "KJu1sq": "Usuń listę kontrolną", - "L6k6aT": "...lub zacznij od szablonu", - "J1G4S4": "Nie zdefiniowano żadnego playbooka.", - "IwY/wg": "Playbook dla każdego procesu", - "IOnm/Z": "Brak dostępnego podsumowania przebiegu.", - "GRTyvN": "Przełącz Listę Playbooków", - "JXdbo8": "Gotowe", - "Lg3I1b": "@{targetUsername}, proszę podaj aktualizację statusu.", - "Leh2tk": "Kliknij tutaj,aby zobaczyć wszystkie przebiegi w zespole.", - "LVYPbG": "Przypisz Właściciela", - "MhKICa": "Twoja subskrypcja pozwala na jeden playbook na zespół. Uaktualnij swoją subskrypcję i twórz wiele podręczników z unikalnymi przepływami pracy dla każdego zespołu.", - "MDP9TS": "Usuń z playbooka", - "OHfpS1": "Zawierające którekolwiek z tych słów kluczowych", - "QVQrgH": "Gdy usuniesz własny dostęp do tego playbooka, nie będziesz mieć możliwości ponownego dodania siebie. Czy na pewno chcesz wykonać tę czynność?", - "QUwMsX": "Przypomnienie o wypełnieniu retrospektywy", - "N2IrpM": "Potwierdź", - "Mm1Gse": "Szukaj użytkownika", - "K4O03z": "Nowe zadanie", - "JJNc3c": "Poprzedni", - "IfxUgC": "Dodaj podsumowanie uruchomienia…", - "Ietscn": "Zadanie zakończone", - "I90sbW": "w tym momencie", - "GxJAK1": "Playbook o który prosisz, jest prywatny lub nie istnieje.", - "GwtR3W": "Przeciągnij i upuść istniejące zadanie lub kliknij, aby utworzyć nowe zadanie.", - "EQpfkS": "Zakończone", - "E0LnBo": "Możesz wybrać opcję lub określić niestandardowy czas trwania (\"2 tygodnie\", \"3 dni 12 godzin\", \"45 minut\", ...)", - "DtCplA": "{numParticipants, plural, =1 {# uczestnik} other {# uczestników}}", - "CkYhdY": "Dodaj kanał do kategorii na pasku bocznym", - "DSVJjB": "Aktualnie uruchomiony playbook {playbookTitle}", - "HSi3uv": "Brak przypisanej osoby", - "HAlOn1": "Nazwa", - "G/yZLu": "Usuń", - "DuRxjT": "Utwórz playbook", - "CyGaem": "Nazwa uruchomienia", - "D55vrs": "Nie można wygenerować Twojej licencji", - "BNB75h": "Playbook określa listy kontrolne, automatyzacje i szablony dla wszelkich powtarzalnych procedur. {br} Pomaga zespołom redukować błędy, zdobywać zaufanie interesariuszy i zwiększać efektywność z każdą iteracją.", - "Auj1ap": "Rozpocznij okres próbny lub uaktualnij swoją subskrypcję.", - "Cy1AK/": "Zobacz szczegóły przebiegu", - "CSts8B": "Ikona Zespołu", - "CBM4vh": "Zegar do następnej aktualizacji", - "C9NScU": "Przejmij kontrolę nad swoim zespołem", - "C1khRR": "Wróc do playbooków", - "BQtd5I": "Witaj w playbookach!", - "B487HA": "W Trakcie", - "ApULhK": "Zaproś użytkowników", - "A8dbCS": "Nie Znaleziono Playbooka", - "A21Mgv": "Uruchomienie zakończone", - "9tBhzB": "Aktualizuj teraz", - "9qc7BX": "Drzemka", - "9kCT7Q": "Ułatw retrospekcje dzięki osi czasu, która automatycznie śledzi kluczowe wydarzenia i wiadomości, aby zespoły miały je na wyciągnięcie ręki.", - "9TTfXU": "Twój Administrator Systemu został powiadomiony.", - "91Hr5f": "Przeciągnij mnie, aby zmienić kolejność", - "9+Ddtu": "Następny", - "7VTSeD": "Czy na pewno chcesz pominąć to zadanie? Zostanie to przekreślone z tego przebiegu, ale nie wpłynie na playbook.", - "6uhSSw": "Wybierz kanał", - "6n0XDG": "Czy na pewno chcesz usunąć listę kontrolną? Wszystkie zadania zostaną usunięte.", - "6jDabx": "Zostaw Opinię", - "6CGo3o": "Status / Ostatnia aktualizacja", - "5wqhGy": "Przełącz szczegóły uruchomienia", - "5qBEKB": "Czym są uruchomienia playbooków?", - "5j6GD/": "{numParticipants, plural, =0 {brak uczestników} =1 {# uczestnik} other {# uczestników}}", - "42qmJ5": "Nie masz uprawnień do publikowania aktualizacji.", - "5CI3KH": "Skontaktuj się z pomocą techniczną", - "4ltHYh": "Idź do playbooka", - "36GNZj": "Playbook {title} został pomyślnie zarchiwizowany.", - "3Psa+5": "Dodaj słowa kluczowe", - "2VrVHu": "Szukaj według nazwy uruchomienia", - "+Tmpup": "Automatycznie uzyskasz aktualizacje po uruchomieniu tego playbooka.", - "/4tOwT": "Pomiń", - "0HT+Ib": "Zarchiwizowane", - "Vhnd2J": "Przełącz opis", - "z3B83t": "Szukaj playbooka", - "vjzpnC": "Nie ma playbooków pasujących do tych filtrów.", - "fpuWL1": "Usuń playbook", - "Y+U8La": "Czy na pewno chcesz usunąć playbook {title}?", - "K3r6DQ": "Usuń", - "RO+BaS": "Kopiuj link do uruchomienia", - "NA7Cw1": "Skopiuj link do playbooka", - "0oL1zz": "Skopiowane!", - "fuDLDJ": "Utwórz kanał", - "UMoxP9": "Szablon nazwy kanału (opcjonalnie)", - "3MSGcL": "Nazwa kanału jest nieprawidłowa.", - "cp7KUI": "Playbok", - "C6Oghd": "Edytuj podsumowanie uruchomienia", - "cPIKU2": "Obserwowane", - "d4g2r8": "Usunięto: {timestamp}", - "4vuNrq": "{duration} po rozpoczęciu uruchomienia", - "/gbqA6": "{duration} przed rozpoczęciem uruchomienia", - "O8o2lE": "Dodaj kanał do kategorii", - "Mu2aDs": "Każdy w zespole({team}) ma dostęp.", - "q0cpUe": "Dodaj listę kontrolną", - "nSFBC2": "+ Dodaj zadanie", - "m/Q4ye": "Zmiana nazwy listy kontrolnej", - "k1djnL": "Usuń listę kontrolną", - "iXNbPf": "Zmień nazwę", - "X2K92H": "Nazwa listy kontrolnej", - "MrJPOh": "Włącz aktualizacje statusu", - "Ja1sVR": "Aktualizacje statusu zostały wyłączone dla tego uruchomienia playbooka.", - "I5NMJ8": "Więcej", - "D9IV7i": "Retrospektywy zostały wyłączone dla tego uruchomienia playbooka.", - "5Ofkag": "Rozpocznij retrospektywę", - "2/2yg+": "Dodaj", - "/ZsEUy": "Czy na pewno chcesz usunąć tę listę kontrolną? Zostanie ona usunięta z tego uruchomienia, ale nie będzie miała wpływu na playbook.", - "vaYTD+": "Jest {outstanding, plural, =1 {# zaległe zadanie} other {# zaległe zadania}}. Czy jesteś pewien, że chcesz zakończyć uruchomienie?", - "WbsomC": "Opublikuj retrospektywę", - "TxCTXQ": "Czy na pewno chcesz zakończyć uruchomienie?", - "QywYDe": "Oznacz również uruchomienie jako zakończone", - "D/wCS9": "Czy jesteś pewien, że chcesz opublikować retrospektywę?", - "2563nT": "Potwierdź zakończenie uruchomienia", - "pK6+CW": "@{displayName} nie jest członkiem kanału [{runName}]({overviewUrl}). Czy chciałbyś dodać go do tego kanału? Będą oni mieli dostęp do całej historii wiadomości.", - "iDMOiz": "CZŁONKOWIE KANAŁU", - "JqKASQ": "Dodaj @{displayName} do Kanału", - "5ciuDD": "NIE W KANALE", - "Lo10yH": "Nieznany kanał", - "wylJpv": "Każdy w {team} może zobaczyć tego playbooka.", - "tVPYMu": "Administrator Playbooka", - "ruJGqS": "Dostęp do Playbooka", - "osuP6z": "Przeciągnij, aby zmienić kolejność listy kontrolnej", - "o+ZEL3": "Opublikowano {timestamp}", - "lQT7iD": "Utwórz playbook", - "gGcNUr": "Nie masz uprawnień", - "g0mp+I": "Podczas konwersji do prywatnego playbooka, członkostwo i historia uruchomień są zachowane. Ta zmiana jest trwała i nie może być cofnięta. Czy na pewno chcesz przekonwertować {playbookTitle} do prywatnego playbooka?", - "SXJ98n": "Po opublikowaniu raportu retrospektywnego nie będzie można go edytować. Czy chcesz opublikować raport retrospektywny?", - "R/2lqw": "Wybierz szablon", - "QpUBDr": "{members, plural, =0 {Nikt nie może} =1 {Jedna osoba może} other {# osoby mogą}} uzyskać dostęp do tego playbooka.", - "MJ89uW": "Konwertuj do Rrywatnego playbooka", - "HLn43R": "Zarządzaj dostępem", - "EvBQLq": "Zrób Administratorem Playbooka", - "EWz2w5": "Uruchom Playbooka", - "8oCVbz": "Czy jesteś pewien, że chcesz opublikować", - "5BUxvl": "Każdy w tym zespole może przejrzeć ten playbook.", - "3Ls2m+": "Członek Playbooka", - "0tznw6": "Konwertuj do prywatnego playbooka", - "0Vvpht": "Zrób Członkiem Playbooka", - "0q+hj2": "Zdefiniuj szablon zwięzłego opisu, który wyjaśnia każde uruchomienie jego interesariuszom.", - "qsr3Zk": "Aktualizacja podsumowania uruchomienia", - "FXCLuZ": "{total, number} ogółem", - "3PoGhY": "Czy jesteś pewien, że chcesz opublikować?", - "4fHiNl": "Duplikuj", - "4alprY": "Szablony Playbooków", - "/urtZ8": "Twoje Playbooki", - "SVwJTM": "Eksportuj", - "9XUYQt": "Importuj", - "y7o4Rn": "Czy na pewno chcesz usunąć?", - "uT4ebt": "np. liczba zasobów, klienci, których dotyczy problem", - "tbjmvS": "Metryka o tej samej nazwie już istnieje. Proszę dodać unikalną nazwę dla każdej metryki.", - "rzbYbE": "Cel", - "rMhrJH": "Proszę dodać tytuł dla swojej metryki.", - "mbo96h": "Konfiguracja niestandardowych metryk do wypełnienia w raporcie retrospektywnym", - "mVpO8u": "Widziałeś to wcześniej?", - "lBqu4h": "Przywróć playbooka", - "gsMPAS": "Dolary", - "f+bqgK": "Nazwa metryki", - "bTgMQ2": "Ten playbook jest zarchiwizowany.", - "a0hBZ0": "Usuń metrykę", - "XpDetT": "Zrezygnuj z tych porad.", - "VZRWFk": "np. Koszt, Zakupy", - "TxmjKI": "Opisz, czego dotyczy ta metryka", - "Sx3lHL": "Integer", - "OyZnsJ": "przez uruchomienie", - "NYTGIb": "Jasne", - "NJ9uPu": "Kluczowe wskaźniki", - "MTzF3S": "Czy na pewno chcesz przywrócić playbook {title}?", - "LI7YlB": "Dodaj szczegóły dotyczące metryki i sposobu jej wypełniania. Opis ten będzie dostępny na stronie retrospektywnej dla każdego badania, gdzie będą wprowadzane wartości dla tych metryk.", - "LDYFkN": "Czas trwania (w dd:hh:mm)", - "JrZ2th": "Dodaj metrykę", - "FGzxgY": "np.: Czas na potwierdzenie, Czas na rozwiązanie", - "F4pfM/": "Proszę wpisać liczbę lub pozostawić cel pusty.", - "9SIW2x": "Wartość docelowa dla każdego uruchomienia", - "6D6ffM": "Proszę wprowadzić czas trwania w formacie: dd:hh:mm (np. 12:00:00), lub pozostawić cel pusty.", - "4cwL43": "Z archiwalnymi", - "4aupaG": "Playbook {title} został pomyślnie przywrócony.", - "4BN53Q": "Pokażemy Ci jak blisko lub daleko od celu znajduje się wartość każdego uruchomienia, a także przedstawimy ją na wykresie.", - "1ikfp3": "Jeśli usuniesz tę metrykę, wartości dla niej nie będą zbierane dla przyszłych uruchomień.", - "0Xt1ea": "Nadal będzie można uzyskać dostęp do danych historycznych dla tej metryki.", - "q/Qo8l": "Prywatne playbooki są dostępne tylko w Mattermost Enterprise", - "GjCS6U": "Wybierz szablon", - "lgZf0l": "Zacznij korzystać z Playbooków", - "HGdWwZ": "Tworzenie i przydzielanie zadań", - "ZkhArX": "Do dzieła!", - "GG1yhI": "Dostępne są szablony dla różnych przypadków i zdarzeń. Możesz używać playbooka w obecnej formie lub dostosować go do własnych potrzeb, a następnie udostępnić zespołowi.", - "lUfDe1": "Eksportuj kanał playbooka i zapisz go w celu późniejszej analizy.", - "vJ2SaW": "Zautomatyzuj niektóre elementy playbooka, takie jak wysyłanie wiadomości powitalnej, zapraszanie kluczowych członków i tworzenie kanału aktualizacji.", - "Q5hysF": "Zrób więcej dzięki Playbookom", - "6GTzTR": "W każdej chwili możesz sprawdzić, co jest w tym playbooku", - "1isgPF": "", - "I5DYM+": "Ucz się i zastanawiaj", - "GAuN6w": "Ustalenie założeń", - "Q3R9Uj": "W tym miejscu należy udokumentować kroki całego procesu. Przypisz każde zadanie do odpowiedzialnych osób i opcjonalnie dodaj harmonogramy lub powiązane działania.", - "R5Zh+l": "Dzięki temu można najpierw zapoznać się z przykładowym playbookiem, zanim zainwestuje się czas w tworzenie własnego.", - "fhMaTZ": "Krótka wycieczka", - "udrLSP": "Wykorzystanie metryk do zrozumienia wzorców i postępów w poszczególnych uruchomieniach oraz śledzenie wydajności.", - "wbdGb5": "Przydzielaj, sprawdzaj lub pomijaj zadania, aby zespół miał jasność, jak wspólnie dążyć do mety.", - "QbGfqo": "Przekazuj informacje zainteresowanym stronom w wielu miejscach i zachowaj dokumentację do celów retrospektywnych, publikując tylko jeden post.", - "dZmYk6": "Pomyślnie powielony playbook", - "q/VD+s": "Ustal harmonogramy i przygotuj szablon aktualizacji statusu, aby zainteresowani byli zawsze na bieżąco z rozwojem sytuacji.", - "8n24G2": "Wyświetlanie szczegółów uruchomienia w panelu bocznym", - "hw83pa": "Śledzenie kluczowych wskaźników i pomiar wartości", - "1QosTr": "Używane przez", - "cEWBE3": "Oceniaj swoje procesy za pomocą retrospektywy, aby udoskonalać je i poprawiać przy każdym uruchomieniu.", - "9m0I/B": "Informowanie na bieżąco zainteresowanych", - "vL4++D": "Śledzenie postępów i odpowiedzialności", - "dxyZg3": "Pozwól, że sam się przekonam", - "Tt04f1": "Zobacz, kto jest zaangażowany i co należy zrobić, nie odrywając się od rozmowy.", - "Pue+oV": "", - "HXvk56": "Zamieszczaj aktualizacje statusu", - "RzEVnf": "Playbooki zapewniają większą powtarzalność i rozliczalność ważnych procedur. Playbook może być uruchamiany wielokrotnie, a każde uruchomienie ma swój własny zapis i retrospektywę.", - "wPVxBN": "", - "vQqT/8": "", - "0EEIkR": "", - "/fU9y/": "Na tej stronie można szczegółowo zapoznać się z różnymi częściami playbooka.", - "Vf/QlZ": "Zakres wartości", - "NLeFGn": "do", - "KXVV4+": "Witamy na stronie z przeglądem playbooka!", - "fmbSyg": "Dodaj wartość (w dd:hh:mm)", - "M4gAc9": "Dodaj wartość", - "NiAH1z": "Wartość docelowa", - "lbs7UO": "na uruchomienie w ciągu ostatnich 10 uruchomień", - "xVyHgP": "Rozpocznij testowe uruchomienie", - "awG90C": "Cel na uruchomienie", - "69nlA3": "Wprowadź czas trwania w formacie: dd:hh:mm (np. 12:00:00).", - "NMxVd+": "Proszę wpisać wartość metryczną.", - "l5/RKZ": "W tym playbooku nie ma ukończonych uruchomień.", - "ZNNjWw": "Wprowadź numer.", - "ru+JCk": "Wartość średnia", - "mvZUm3": "W tym miejscu możesz szczegółowo zapoznać się z komponentami playbooka. Wybierz Edytuj, aby dostosować playbook do swoich procesów i modeli.", - "efeNi1": "Średnia wartość 10-ciu uruchomień", - "9a9+ww": "Tytuł", - "9j5KzL": "Wprowadź nazwę kategorii", - "5AJmOz": "Kiedy nowy użytkownik dołącza do kanału", - "2Q5PhZ": "Prośba o uruchomienie playbooka", - "0RlzlZ": "Wyślij tymczasowy komunikat powitalny do użytkownika", - "+/x2FM": "Wybierz playbooka", - "Ob5cSv": "Wprowadzone zmiany nie zostaną zapisane po opuszczeniu tej strony. Czy na pewno chcesz odrzucić zmiany i opuścić stronę?", - "MHzP9I": "Zdefiniuj wiadomość powitalną dla użytkowników dołączających do kanału.", - "MBNMo9": "Akcje na Kanałach", - "Ek1Fx2": "Gdy zostanie wysłana wiadomość zawierająca te słowa kluczowe", - "DPj6DM": "Wybierz opcję Przebieg, aby zobaczyć ją w działaniu.", - "B3Q5mz": "Wyzwalacz", - "+PMJAg": "Rozpocznij śledzenie dla {followers, plural, =1 {jednego użytkownika} other {# użytkowników}}", - "Y4MU/9": "Wybierz Uruchom testowy przebieg, aby zobaczyć go w działaniu.", - "RUlvbf": "Przetestuj swój nowy playbook!", - "Ppx673": "Raporty", - "MtrTNy": "Jutro", - "MbapTE": "{num} {num, plural, =1 {zadanie} other {zadań}} zaległych", - "I7+d55": "Określ datę/czas (\"za 4 godziny\", \"1 maja\"...)", - "AF7+5o": "Dodaj termin płatności", - "zWgbGg": "Dzisiaj", - "u7qh13": "Gotowy do uruchomienia swojego playbooka?", - "u4L4yd": "Masz niezapisane zmiany", - "p1I/Fx": "Automatycznie utworzyliśmy twoje uruchomienie", - "mw9jVA": "Dodaj tytuł", - "mLrh+0": "Brak terminu płatności", - "lyXljU": "Duplikuj zadanie", - "lglICE": "Dodaj opis (opcjonalnie)", - "iMjjOH": "W następnym tygodniu", - "e3z3P8": "Odrzuć & pozostaw", - "dCtjdj": "Gotowy do uruchomienia swojego playbooka?", - "c23IHq": "Akcje kanału umożliwiają zautomatyzowanie działań dla tego kanału", - "ao44YC": "Skonfiguruj metryki", - "aEhjYg": "Zarys", - "Z3ybv/": "Dodaj kanał do kategorii paska bocznego dla użytkownika", - "W0aij2": "Przypisz do...", - "UlJJ1i": "Dodaj polecenie po ukośniku", - "oBeKB4": "Do zapłaty w dniu {date}", - "lkv547": "Termin płatności (dostępny w planie Profesjonalnym)", - "g9pEhE": "Płatność", - "TTIQ6E": "Przypisuj zadania do terminów, aby osoby odpowiedzialne mogły ustalić priorytety i wykonać zadania.", - "NFyWnZ": "Pracuj bardziej efektywnie", - "371AC3": "Aktualizacja podsumowania uruchomienia", - "oAJsne": "Publiczny playbook", - "mm5vL8": "Tylko zaproszeni członkowie", - "lJ48wN": "Prywatny playbook", - "Xgxruo": "Pomiń listę kontrolną", - "RQl8IW": "Uśpij na…", - "OqCzNb": "Dodaj zadanie", - "JcefuP": "Dodaj opis (opcjonalnie)", - "9trZXa": "Każdy członek zespołu może przeglądać", - "7P5T3W": "Przywróć listę kontrolną", - "v5/Cox": "Duplikat listy kontrolnej", - "mCrdeS": "Razem Playbooków", - "4GjZsL": "Razem Playbooków", - "IxtSML": "Dodaj listę kontrolną", - "CwwzAU": "Dodaj nazwę listy kontrolnej", - "k12r+v": "Dodaj szablon podsumowania przebiegu...", - "cyR7Kh": "Wstecz", - "XF8rrh": "Skopiuj link do ''{name}''", - "RrCui3": "Podsumowanie", - "MyIJbr": "Zawartość", - "5ZIN3u": "Aktualizacje Statusu", - "xHNF7i": "Akcje Uruchomień", - "x1phlu": "Brak przedziału czasowego", - "sX5Mn5": "Proszę wpisać jeden webhook w wierszu", - "mkLeuq": "Rozesłanie aktualizacji na wybranych kanałach", - "kkw4kS": "Ta aktualizacja zostanie przesłana do {hasChannels, select, true {{broadcastChannelCount, plural, =1 {jednego kanału} other {{broadcastChannelCount, number} kanałów}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {jedna bezpośrednia wiadomość} other {{followersChannelCount, number} bezpośrednie wiadomości}}} other {}}.", - "kYCbJE": "Dodaj przedział czasowy", - "kV5GkX": "Kiedy aktualizacja statusu jest wysyłana", - "j940pJ": "Ta aktualizacja zostanie zapisana na stronie przeglądowej.", - "c6LNcW": "Usuń zadanie", - "HvAcYh": "{text}{rest, plural, =0 {} one { i inne} other { i {rest} inne}}", - "28FTjr": "Akcje uruchomień umożliwiają zautomatyzowanie działań dla tego kanału", - "/RnCQb": "Wyślij wychodzący webhook", - "uhDKO8": "Użyj markdown do utworzenia szablonu", - "giM/X9": "Oczekuje się aktualizacji statusu co . Nowe aktualizacje będą przesyłane do {channelCount, plural, =0 {brak kanałów} one {# kanału} other {# kanałów}} i {webhookCount, plural, =0 {brak wychodzących webhooków} one {# wychodzącego webhooka} other {# wychodzących webhooków}} .", - "aM44Z/": "Wybierz lub określ niestandardowy czas trwania…", - "YQOmSf": "Wprowadź jeden webhook w każdym wierszu", - "XRyRzf": "Nie oczekuje się aktualizacji statusu.", - "DaHpK1": "Wyszukaj kanał", - "F9LrJA": "Filtrowanie elementów", - "OuZhcQ": "Określ czas trwania (\"8 godzin\", \"3 dni\"...)", - "zl6378": "Skonfiguruj metryki w Retrospektywie", - "sGJpuF": "Dodaj opis…", - "aZGAOI": "Dodaj szablon aktualizacji stanu…", - "OKhRC6": "Udostępnij", - "LcC/pi": "Wyślij wiadomość powitalną…", - "Brya9X": "Dodaj szablon podsumowania uruchomienia…", - "9kQNdp": "Ten playbook jest prywatny.", - "3hBelc": "Nie przewiduje się retrospektywy.", - "yllba1": "Nie można zmienić nazwy tego zarchiwizowanego playbooka.", - "xEQYo5": "Konfiguracja niestandardowych metryk do wypełnienia w raporcie retrospektywnym.", - "vSMfYU": "Informacje o uruchomieniu", - "oL7YsP": "Ostatnio edytowane {timestamp}", - "Z2Hfu4": "Dodaj podsumowanie uruchomienia", - "TD8WrM": "Duplikowanie jest wyłączone dla tego zespołu.", - "OQplDX": "Oczekuje się aktualizacji statusu co . Nowe aktualizacje będą przesyłane do {channelCount, plural, =0 {brak kanałów} one {# kanału} other {# kanałów}} i {webhookCount, plural, =0 {brak wychodzących webhooków} one {# wychodzącego webhooka} other {# wychodzących webhooków}} .", - "opn6uf": "Zobacz oś czasu", - "o6N9pU": "Akcje Uruchomień", - "lbr3Lq": "Kopiuj link", - "iigkp8": "Czas na podsumowanie?", - "hjteuA": "Wszystkie playbooki, do których można uzyskać dostęp, będą widoczne tutaj", - "bf5rs0": "Wyświetl Informacje", - "ZJS10z": "Nie zamieszczono jeszcze żadnych aktualizacji", - "Q15rLN": "Prośba o aktualizację...", - "GDCpPr": "Ostatnia aktualizacja statusu", - "+qDKgW": "Wyświetl wszystkie aktualizacje", - "kEMvwX": "Nie ma żadnych uruchomień pasujących do tych filtrów.", - "GXjP8g": "Wszystkie dostępne uruchomienia będą wyświetlane tutaj", - "ocYb9S": "Kluczowe Wskaźniki", - "nc8QpJ": "Ostatnia Aktywność", - "m/KtHt": "Nie masz uprawnień do zmiany właściciela", - "RnOiCg": "Nie można było {isFollowing, select, true {wyłączyć obserwowanie} other {włączyć obserwowanie}} uruchomienia", - "4mCpAv": "Nie można było zmienić właściciela", - "lr1CUA": "Przeglądaj Playbooki", - "jboo9u": "Wniosek o aktualizację", - "Xx0WZV": "Wyślij wiadomość", - "VpQKQE": "{displayName} nie jest uczestnikiem uruchomienia. Czy chciałbyś uczynić go uczestnikiem? Będą mieli dostęp do całej historii wiadomości na kanale uruchomienia.", - "Ul0aFX": "Import Playbooka", - "UePrSL": "{num} {num, plural, one {Uczestnik} other {Uczestników}}", - "UMFnWV": "Zobacz Retrospektywę", - "RCT0Px": "Dodaj {displayName} do Kanału", - "P9PKvb": "Na uruchomiony kanał została wysłana wiadomość.", - "NGqzDU": "Potwierdź prośbę o aktualizację", - "LfhTNW": "Przeglądanie lub tworzenie Playbooków i Uruchomień", - "JvEwg/": "Nie było możliwe wystąpić o aktualizację", - "Jli9m7": "Do uruchomionego kanału zostanie wysłana wiadomość z prośbą o zamieszczenie aktualizacji.", - "GVpA4Q": "Utwórz nowy Playbook", - "CFysvS": "Utwórz rozwijany Playbook", - "9xs0pp": "Dodaj wartość...", - "/qDObA": "Przeglądaj Uruchomienia", - "/+8SGX": "Wyświetlanie {filteredNum} z {totalNum} zdarzeń", - "KeO51o": "Kanał", - "zW/5AB": " Funkcja profesjonalna Jest to funkcja płatna, dostępna z bezpłatnym 30-dniowym okresem próbnym", - "xfnuXm": "Weź udział", - "wGp7l3": "{icon} Dolary", - "wBZz47": "Opuściłeś uruchomienie.", - "vDvWJ6": "Wypróbuj aktualizację za pomocą bezpłatnej wersji próbnej", - "u6Fyic": "Twoja prośba została wysłana do kanału uruchomienia.", - "s+rSpl": "{icon} Integer", - "qp5G0Z": "Dostęp do funkcji retrospektywnych wymaga aktualizacji.", - "pzTOmv": "Obserwujący", - "pFK6bJ": "Zobacz wszystkie", - "ojQue/": "{icon} Czas trwania (w dd:hh:mm)", - "mNgqXf": "Aby odblokować tę funkcję:", - "lKeJ+i": "Brak podsumowania", - "j2VYGA": "Pokaż wszystkie playbooki", - "gfUBRi": "Przydziel nowego właściciela przed opuszczeniem uruchomienia.", - "fnihsY": "Opuść", - "ePhhuK": "Twoja prośba została wysłana do kanału uruchomienia.", - "ch4Vs1": "Zażądaj aktualizacji dla uruchomień playbooków jednym kliknięciem i otrzymaj powiadomienie bezpośrednio po opublikowaniu aktualizacji. Rozpocznij bezpłatny, 30-dniowy okres próbny, aby go wypróbować.", - "b+DwLA": "Wniosek o udział w tym uruchomieniu.", - "a1vQ5Q": "Potwierdź odejście", - "XS4umx": "{name} uśpił aktualizację statusu", - "SMrXWc": "Ulubione", - "SK5APX": "Nie można było opuścić uruchomienie.", - "PoX2HN": "Wyślij prośbę", - "PdRg+3": "Zobacz wszystkie...", - "PWmZrW": "Zobacz wszystkie uruchomienia", - "PW+sL4": "N/A", - "P6NEL/": "Polecenie...", - "OfN7IN": "Na kanał uruchomienia zostanie wysłany wniosek o aktualizację statusu.", - "N9CTUJ": "Opuść uruchomienie", - "Mjq//Y": "Cofnij ulubione", - "KzHQCQ": "Nie ma gotowych uruchomień pasujących do tych filtrów.", - "Gwmqz5": "Wniosek o aktualizację", - "F/HKIy": "Czy na pewno chcesz opuścić uruchomienie?", - "CV1ddt": "Weź udział w uruchomieniu", - "CUhlqp": "samouczek i porady", - "B9z0uZ": "Twoja prośba o dołączenie do uruchomienia nie powiodła się.", - "AH+V3r": "Zostań uczestnikiem uruchomienia.", - "5Hzwqs": "Ulubiony", - "5HXkY/": "Typ: {typeTitle}", - "4Iqlfe": "Dołączyłeś do tego uruchomienia.", - "3zF589": "Resetuj do wszystkich {filterName}", - "1fXVVz": "Termin...", - "1GOpgL": "Przypisany...", - "+6DCr9": "Jako uczestnik możesz zamieszczać aktualizacje statusu, przydzielać i realizować zadania oraz przeprowadzać retrospektywy.", - "wRM2AO": "Żądanie aktualizacji nie powiodło się.", - "mttASm": "Opuść i przestań obserwować uruchomienie", - "lpWBJE": "Potwierdź opuszczenie i zaprzestanie obserwowania", - "hnYSP3": "Kiedy opuścisz i przestaniesz obserwować uruchomienie, zostanie on usunięty z lewego paska bocznego. Możesz go ponownie znaleźć przeglądając wszystkie uruchomienia.", - "AhY0vJ": "Opuść i przestań obserwować", - "egUE/K": "Nadawanie na wybranych kanałach", - "Xm0L7N": "Kiedy aktualizuje się status, lub publikuje retrospekcję", - "iEtImk": "Kiedy opuścisz {isFollowing, select, true {i przestaniesz śledzić uruchomienie} other { uruchomienie}}, zostanie on usunięty z lewego paska bocznego. Możesz go ponownie znaleźć, przeglądając wszystkie uruchomienia.", - "cnfVhV": "Opuść{isFollowing, select, true { i wyłącz obserwowanie} other {}}", - "Suyx6A": "Import playbooka nie powiódł się. Proszę sprawdzić czy JSON jest poprawny i spróbować ponownie.", - "QegBKq": "Dołącz do playbooka", - "Q4sutg": "Potwierdź opuszczenie{isFollowing, select, true { i wyłączenie obserwowania} other {}}", - "P6PLpi": "Dołącz do", - "FgydNe": "Zobacz", - "5PpBsd": "Twoja prośba nie powiodła się.", - "qGlwfc": "Uruchom uruchomienie", - "j2FnDV": "Zostanie utworzony kanał o tej nazwie", - "iQhFxR": "Ostatnio używane", - "03oqA2": "Aktywuj Uruchomienia", - "vqmRBs": "Potwierdź ponowne uruchomienie", - "k5EChD": "Czy na pewno chcesz ponownie uruchomić uruchomienie?", - "Zg0obP": "Uruchom ponownie", - "KjNfA8": "Nieważny czas trwania", - "XnICdK": "Nie można było dołączyć do przebiuegu", - "unwVil": "Żądanie dołączenia do kanału nie powiodło się.", - "ZRv7Dm": "Prośba o Dołączenie", - "M9tXoZ": "Do kanału uruchomienia zostanie wysłane żądanie dołączenia.", - "0QD99o": "Prośba o dołączenie do kanału", - "q48ca7": "Przekazanie opinii na temat Playbooków.", - "bCmvTY": "Zostaw opinię", - "fVMECF": "Uczestnik", - "FLG4Iu": "Mianuj właścicielem uruchomienia", - "6rygzu": "Usuń z uruchomienia", - "0Azlrb": "Zarządzaj", - "/GCoTA": "Wyczyść", - "utHl3F": "Dodaj ludzi do {runName}", - "l/W5n7": "Uczestnicy zostaną również dodani do kanału połączonego z tym uruchomieniem", - "WC+NOj": "Dodaj również ludzi do kanału połączonego z tym uruchomieniem", - "1prgB2": "Wyszukiwanie osób", - "w4Nhhb": "Dodaj uczestnika", - "jrOlPO": "Otrzymuj powiadomienia o aktualizacjach statusu uruchomienia", - "cUCiWw": "Zostań uczestnikiem", - "1OVPiC": "Zostań uczestnikiem uruchomienia. Jako uczestnik możesz zamieszczać aktualizacje statusu, przydzielać i realizować zadania oraz przeprowadzać retrospektywy.", - "wCDmf3": "Włącz aktualizacje", - "qDxsQH": "Zostań uczestnikiem, aby wejść w interakcję z tym uruchomieniem", - "nsd54s": "Potwierdź wyłączenie aktualizacji statusu", - "jAo8dd": "Aktualizacje stanu uruchomienia wyłączona przez {name}", - "cpGAhx": "Czy na pewno chcesz włączyć aktualizacje statusu dla tego uruchomienia?", - "b8Gps8": "Aktualizacje stanu uruchomienia włączona przez {name}", - "WFA0Cg": "Czy na pewno chcesz włączyć aktualizacje statusu dla tego uruchomienia?", - "H7IzRB": "Wyłączenie aktualizacji statusu", - "9qqGGd": "Zaproś uczestników", - "1OluNs": "Potwierdź włączenie aktualizacji statusu", - "//o1Nu": "Wyłączenie aktualizacji", - "lqzBNa": "Usuń ich z kanału uruchomienia", - "ieL3dC": "Ustawianie działań na kanale", - "ha1TB3": "Kiedy uczestnik dołącza do uruchomienia", - "Z18I+c": "Akcje kanałów pozwalają na automatyzację działań dla kanału", - "Y1EoT/": "Gdy uczestnik opuszcza uruchomienie", - "5b1zuB": "Dodaj je do kanału uruchomienia", - "u/yGzS": "{name} dodał @{user} do uruchomienia", - "t6lwwM": "{requester} usunął {users} z uruchomienia", - "jfpnye": "@{user} opuścił uruchomienie", - "feNxoJ": "{requester} dodał {users} do uruchomienia", - "ecS/qx": "{name} dodał {num} uczestników do uruchomienia", - "VM75su": "{name} usunął z uruchomienia {num} uczestników", - "SwlL5j": "@{user} dołączył do uruchomienia", - "RXjd3Q": "{name} usunął @{user} z uruchomienia", - "zSOvI0": "Filtry", - "qxYWTy": "Pokaż wszystkie zadania z uruchomień, które posiadam", - "grv9Fm": "Wybierz, aby przełączyć listę zadań.", - "YBvwXR": "Brak przydzielonych zadań", - "WFd88+": "Pokaż sprawdzone zadania", - "TnUG7m": "Nie masz przypisanego żadnego oczekującego zadania.", - "SRqpbI": "{assignedNum, plural, =0 {Brak przydzielonych zadań} other {# przydzielone}}", - "I0NIMp": "Twoje zadania", - "DUU48k": "Nie ma zadania jednoznacznie przypisanego do Ciebie. Możesz rozszerzyć swoje poszukiwania za pomocą filtrów.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# zaległe}}", - "meD+1Q": "UCZESTNICY URUCHOMIENIA", - "Gg/nch": "NIEUCZESTNICZĄCY", - "L6vn9U": "Uczestnicy uruchomienia", - "36NwLv": "Zarządzanie listą uczestników uruchomienia", - "iH5e4J": "Zostaniesz również dodany do kanału powiązanego z tym uruchomieniem.", - "fBG/Ge": "Koszt", - "VjJYEV": "np. Wpływ sprzedaży, Zakupy", - "UAS7Bn": "Poproś o dostęp do kanału związanego z tym uruchomieniem", - "NGKqOC": "Dodaj mnie również do kanału połączonego z tym uruchomieniem", - "BJNrYQ": "Jako uczestnik będziesz mógł aktualizować podsumowanie uruchomienia, sprawdzać zadania, zamieszczać aktualizacje statusu i edytować retrospektywę.", - "9X3jwi": "{icon} Koszt", - "dK2JKl": "Link do istniejącego kanału", - "IdTL+v": "Utwórz kanał uruchomienia", - "2BCWLD": "Skonfiguruj kanał", - "lqceIp": "lub Importuj playbook", - "ORJ0Hb": "Jest {outstanding, plural, =1 {# zaległe zadanie} other {# zaległych zadań}}. Czy na pewno chcesz zakończyć uruchomienie dla wszystkich uczestników?", - "0boT49": "Czy na pewno chcesz zakończyć uruchomienie dla wszystkich uczestników?", - "AG7PKJ": "Zmiana nazwy uruchomienia", - "a2r7Vb": "Kanał prywatny", - "VA1Q/S": "Kanał publiczny", - "zxj2Gh": "Ostatnia aktualizacja {time}", - "yP3Ud4": "Z tym kanałem nie są związane żadne trwające uruchomienia", - "tqAmbk": "Uruchomienia w trakcie", - "Z1sgPO": "Pokaż zakończone uruchomienia", - "RC6rA2": "Ostatnio stworzone", - "Q/t0//": "Zakończone uruchomienia", - "NNksk4": "Alfabetycznie", - "AoNLta": "Z tym kanałem nie są związane żadne zakończone uruchomienia", - "2NDgJq": "Ostatnia aktualizacja statusu", - "RgQwWr": "Sortuj uruchomienia według", - "prs4kX": "Kiedy wiadomość z określonymi słowami kluczowymi zostanie wysłana", - "m8hzTK": "Ostatnio użyty {time}", - "kQAf2d": "Wybierz", - "gS1i4/": "Oznacz zadanie jako wykonane", - "gGtlrk": "Twoje playbooki", - "fvNMLo": "Akcje zadań", - "cGCoJe": "Wysłane przez", - "Wy3sw+": "{count, plural, =1{1 uruchomienie w trakcie} =0 {Brak uruchomień w trakcie} other {# uruchomienia w trakcie}}", - "W1EKh5": "Utwórz nowy playbook", - "SRbTcY": "Inne playbooki", - "L1tFef": "Proszę sprawdzić pisownię lub spróbować innego wyszukiwania", - "KQunC7": "Używane w tym kanale", - "HfjhwE": "Przeszukiwanie playbooków", - "GZoWl1": "Zautomatyzuj działania dla tego zadania", - "EVSn9A": "Rozpocznij uruchomienie", - "9AQ5FE": "Podsumowanie uruchomienia", - "95v+5O": "{actions, plural, =0 {Task Actions} one {# działanie} other {# działania}}", - "7KMbBa": "Nigdy nie używany", - "3sXVwy": "Działania w zakresie zadań...", - "3Yvt4d": "Playbooki to konfigurowalne listy kontrolne, które definiują powtarzalny proces dla zespołów w celu osiągnięcia określonych i przewidywalnych rezultatów", - "0CeyUV": "Brak wyników dla \"{searchTerm}\"", - "Bgt0C8": "Ta aktualizacja dla uruchomienia {runName} zostanie rozesłana do {hasChannels, select, true {{broadcastChannelCount, plural, =1 {jednego kanału} other {{broadcastChannelCount, number} kanałów}}} other {}}{hasFollowersAndChannels, select, true { and } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {jednej bezpośredniej wiadomości} other {{followersChannelCount, number} bezpośrednich wiadomości}}}other {}}.", - "zscc/+": "Jest {outstanding, plural, =1 {# zaległe zadanie} other {# zaległych zadań}}. Czy na pewno chcesz zakończyć uruchomienie {runName} dla wszystkich uczestników?", - "bEoDyV": "@{authorUsername} zamieścił aktualizację dla [{runName}]({overviewURL})", - "ZSa3cf": "@{targetUsername}, proszę podać aktualizację statusu dla [{runName}]({playbookURL}).", - "LKu0ex": "Czy na pewno chcesz zakończyć uruchomienie {runName} dla wszystkich uczestników?", - "uCS6py": "Nie masz uprawnień do oglądania tego playbooka", - "l3QwVw": "Wybierz kanał", - "ksG35Q": "Nie masz uprawnień do tworzenia playbooków w tej przestrzeni roboczej.", - "YKLHXL": "Wyświetlanie uruchomień w toku", - "QvEO6m": "Nie masz uprawnień do edycji tego uruchomienia", - "QJTSaI": "Podlinkuj uruchomienie do innego kanału", - "BiQjuS": "Uruchomienie przeniesione do {channel}", - "k7Nzfi": "Wyłącz zaproszenie", - "fwW0T1": "Potwierdzenie usunięcia wstępnie przypisanych członków", - "TP/O/b": "Usuń użytkownika", - "IE2BzH": "Istnieją użytkownicy, którzy są wstępnie przypisani do jednego lub więcej zadań. Wyłączenie zaproszeń wyczyści wszystkie wstępnie przypisane zadania.{br}{br}Czy na pewno chcesz wyłączyć zaproszenia?", - "DQn9Uj": "Użytkownik {name} jest wstępnie przypisany do jednego lub więcej zadań. Brak automatycznego zapraszania tego użytkownika spowoduje usunięcie jego wstępnych przydziałów.{br}{br}Czy na pewno chcesz przestać zapraszać tego użytkownika jako członka uruchomienia?", - "9w0mDI": "Potwierdzenie usunięcia wstępnie przydzielonego członka", - "mILd++": "Nazwa uruchomienia nie powinna przekraczać {maxLength} znaków", - "uYrkxy": "Plik musi być poprawnym szablonem playbooka JSON.", - "m4vqJl": "Pliki", - "Zbk+OU": "Rozmiar pliku przekracza limit 5MB.", - "MieztS": "Upuść plik eksportu playbooka, aby go zaimportować.", - "HGSVzc": "Nie można importować wielu plików jednocześnie.", - "LaseGE": "Nie masz uprawnień do edycji tej listy kontrolnej", - "Edy3wX": "Lista kontrolna przeniesiona do {channel}", - "8//+Yb": "Linkowanie listy kontrolnej do innego kanału", - "706Soh": "wykonane zadania", - "XHJUSG": "Automatyczne śledzenie uruvhomień", - "DqTQOp": "Raz", - "vjb+hS": "{user} przywrócił pozycję z listy kontrolnej \"{name}\"", - "OqWwvQ": "{user} odznaczył pozycję z listy kontrolnej \"{name}\"", - "DKiv0o": "{user} pominął pozycję z listy kontrolnej \"{name}\"", - "8FzC0B": "{user} odznaczył pozycję z listy kontrolnej \"{name}\"", - "3qPQMX": "{name} poprosił o aktualizację statusu", - "9M92On": "Wybierz kanały" -} diff --git a/webapp/playbooks/i18n/ru.json b/webapp/playbooks/i18n/ru.json deleted file mode 100644 index 605f873fa06..00000000000 --- a/webapp/playbooks/i18n/ru.json +++ /dev/null @@ -1,523 +0,0 @@ -{ - "0oL1zz": "Скопировано!", - "0Vvpht": "Сделать Участником Сценария", - "0HT+Ib": "В архиве", - "/jUtaM": "АКТИВНЫХ ЗАПУСКОВ в день за последние 14 дней", - "/gbqA6": "{duration} до начала запуска", - "/ZsEUy": "Вы уверены, что хотите удалить этот чек-лист? Он будет удален из этого запуска, но не повлияет на сценарий.", - "/YZ/sw": "Начать пробный период", - "/MaJux": "Запустить ретроспективу", - "/4tOwT": "Пропустить", - "/1FEJW": "АКТИВНЫХ УЧАСТНИКОВ в день за последние 14 дней", - "+hddg7": "Добавить на временную шкалу", - "+Tmpup": "Вы автоматически получаете обновления при запуске этого сценария.", - "+QgvjN": "Назначить роль владельца", - "+8G9qr": "Текст по умолчанию для ретроспективы.", - "9+Ddtu": "Следующий", - "7VTSeD": "Вы уверены, что хотите пропустить это задание? Это будет вычеркнуто из этого запуска, но не повлияет на сценарий.", - "6uhSSw": "Выберите канал", - "6n0XDG": "Вы уверены, что хотите удалить чек-лист? Все задания будут удалены.", - "6jDabx": "Дать обратную связь", - "6CGo3o": "Статус / Последнее обновление", - "5wqhGy": "Переключить сведения о запуске", - "5qBEKB": "Что такое запуск сценария?", - "5ciuDD": "НЕ НА КАНАЛЕ", - "5Ofkag": "Включить ретроспективу", - "5FRgqE": "Загрузить журнал канала", - "5CI3KH": "Контакт поддержки", - "5BUxvl": "Все в этой команде могут просматривать этот сценарий.", - "5A46pW": "Добавить быструю команду", - "4vuNrq": "{duration} после начала запуска", - "4ltHYh": "Перейти к сценарию", - "4fHiNl": "Дубликат", - "4alprY": "Шаблоны сценария", - "4Hrh5B": "{name} изменил статус с {summary}", - "47FYwb": "Отмена", - "42qmJ5": "У Вас нет разрешения на публикацию обновления.", - "3rCdDw": "Статус обновлений", - "3Psa+5": "Добавить ключевые слова", - "3PoGhY": "Вы уверены, что хотите опубликовать?", - "3MSGcL": "Недопустимое название канала.", - "3Ls2m+": "Участник сценария", - "36GNZj": "Сценарий {title} успешно архивирован.", - "3/wF0G": "Быстрые команды", - "2VrVHu": "Поиск по названию запуска", - "2Qq4YX": "Вы уверены, что хотите отменить изменения?", - "2QkJ4s": "Сохраняйте важные сообщения, чтобы получить полную картину, упрощающую ретроспективы.", - "2PNrBQ": "Экспортируйте канал запуска Вашего сценария и сохраните его для последующего анализа.", - "2563nT": "Подтвердить завершение запуска", - "2/2yg+": "Добавить", - "1I48bs": "Шаблон ретроспективы", - "15jbT0": "Добавьте больше на свою временную шкалу", - "0wJ7N+": "Задача", - "0tznw6": "Преобразование в частный сценарий", - "0q+hj2": "Определите шаблон для краткого описания, объясняющего каждый запуск заинтересованным сторонам.", - "0oLj/t": "Расширить", - "oS0w4E": "Таймер обновления по умолчанию", - "bTgMQ2": "Этот сценарий находится в архиве.", - "JeqL8w": "Ретроспектива отменена {name}", - "b/QBNs": "Срок обновления", - "yhU1et": "Задачи", - "zELxbG": "Сохраненные сообщения", - "hzt6l8": "Используйте Markdown для создания шаблона.", - "wPVxBN": "", - "IuFETn": "Длительность", - "1isgPF": "", - "Pue+oV": "", - "f+bqgK": "Название метрики", - "wcWpGs": "Недопустимые URL веб-хуков", - "DtCplA": "{numParticipants, plural, =1 {# участник} few{# участника} other {# участников}}", - "Ietscn": "Задачи завершены", - "zWkvNO": "Лента новостей", - "cPIKU2": "Следующий", - "0EEIkR": "", - "GAuN6w": "Настройка предположений", - "GwtR3W": "Перетащите существующую задачу или нажмите, чтобы создать новую задачу.", - "z3B83t": "Поиск сценария", - "KiXNvz": "Запуск", - "AS5kar": "Участники ({participants})", - "HAlOn1": "Имя", - "y7o4Rn": "Вы уверены, что хотите удалить?", - "A8dbCS": "Сценарий не найден", - "gsMPAS": "Доллары", - "q/Qo8l": "Частные сценарии доступны только в Mattermost Enterprise", - "GxJAK1": "Запрошенный вами сценарий является частным или не существует.", - "R5Zh+l": "Это позволит Вам сначала испытать образец сценария, прежде чем тратить время на создание собственного.", - "G/yZLu": "Удалить", - "9PXW6Q": "Продолжительность / Начало", - "sVlNlY": "Структура каждой команды разная. Вы можете управлять тем, какие пользователи в команде могут создавать сценарии.", - "AML4RW": "Назначения задач", - "fhMaTZ": "Краткий обзор", - "vJ2SaW": "Автоматизируйте аспекты Вашего сценария, как отправка приветственного сообщения, приглашение ключевых участников и создание обновлений канала.", - "1MQ3XZ": "{numActiveRuns, plural, =0 {нет активных запусков} =1 {# активный запуск} few {# активных запуска} other {# активных запусков}}", - "SDSqfA": "Когда начинается запуск", - "cEWBE3": "Оцените свои процессы, используя ретроспективу, чтобы уточнять и улучшать их с каждым запуском.", - "vL4++D": "Отслеживайте прогресс и право собственности", - "aACJNp": "Запуск начат {name}", - "B487HA": "Выполняется", - "C1khRR": "Назад к сценариям", - "ApULhK": "Пригласить участников", - "D9IV7i": "Ретроспективы были отключены для этого запуска сценария.", - "Mm1Gse": "Поиск участника", - "ZkhArX": "Поехали!", - "9uOFF3": "Обзор", - "T5rX+W": "Как часто нужно публиковать обновление?", - "MvEydR": "{name} обновил статус", - "vQqT/8": "", - "TSSNg/": "ВСЕГО ЗАПУСКОВ, начатых в неделю за последние 12 недель", - "GjCS6U": "Выберите шаблон", - "O8o2lE": "Добавить канал в категорию", - "sIX63S": "Ваш системный администратор был уведомлен", - "rMhrJH": "Пожалуйста, добавьте заголовок для вашей метрики.", - "z3A0LP": "Последний запуск был {relativeTime}", - "TJo5E6": "Предпросмотр", - "sQu1rA": "{numTotalRuns, plural, =0 {нет запусков} =1 {# запуск начат} few {# запуска начаты} other {# запусков начаты}}", - "wsUmh9": "Команда", - "uT4ebt": "например: количество ресурсов, затронутые клиенты", - "wbwhbH": "Имя задачи", - "vNiZXF": "На данный момент нет запусков в процессе. Запустите сценарий, чтобы начать организовывать рабочие процессы для Вашей команды и инструментов.", - "tbjmvS": "Метрика с таким названием уже существует. Укажите уникальное имя для каждой метрики.", - "IfxUgC": "Добавить сводку запуска…", - "IOnm/Z": "Нет доступной сводки о запуске.", - "HSi3uv": "Нет правопреемника", - "GRTyvN": "Переключить список Сценариев", - "DCl7Vv": "встроенный код", - "D55vrs": "Ваша лицензия не может быть сгенерирована", - "D2CE02": "Введите вебхук", - "CSts8B": "Иконка команды", - "9tBhzB": "Обновить сейчас", - "9qc7BX": "Вздремнуть", - "8hDbW6": "Отправить исходящий вебхук", - "Q3R9Uj": "Задокументируйте шаги для всего процесса здесь. Назначьте каждую задачу ответственным лицам и при желании добавьте временные рамки или связанные действия.", - "I5DYM+": "Учитесь И размышляйте", - "wbdGb5": "Назначайте, отмечайте или пропускайте задачи, чтобы команда понимала, как вместе двигаться к финишу.", - "dxyZg3": "Позвольте мне исследовать самому", - "QbGfqo": "Проводите трансляции для заинтересованных сторон в нескольких местах и сохраняйте документальный след для ретроспективы с помощью всего одной публикации.", - "lgZf0l": "Начните работу с Сценариями", - "RzEVnf": "Сценарии делают важные процедуры более повторяемыми и подотчетными. Сценарий можно запускать несколько раз, и каждый запуск имеет свою собственную запись и ретроспективу.", - "Q5hysF": "Делайте больше с сценариями", - "dZmYk6": "Сценарий успешно продублирован", - "6GTzTR": "Смотрите, что в этом сценарии в любое время", - "DnBhRg": "Добавить людей", - "ryrP8K": "Управляйте разрешениями для тех, кто может просматривать, изменять и запускать этот сценарий.", - "iXNbPf": "Переименовать", - "Sx3lHL": "Целое число", - "S0kWcH": "Обновление просрочено", - "RthEJt": "Ретроспектива", - "OyZnsJ": "за запуск", - "NJ9uPu": "Ключевые метрики", - "MTzF3S": "Вы уверены, что хотите восстановить сценарий {title}?", - "LI7YlB": "Добавьте подробности о том, что представляет собой эта метрика и как ее следует заполнять. Это описание будет доступно на странице ретроспективы для каждого запуска, где будут вводиться значения для этих метрик.", - "LDYFkN": "Длительность (в дд:чч:мм)", - "JrZ2th": "Добавить метрику", - "JJMNME": "{withRunName, select, true {@{authorUsername} опубликовал обновление для [{runName}]({overviewURL})} other {@{authorUsername} опубликовал обновление}}", - "FGzxgY": "например, время подтверждения, время разрешения", - "F4pfM/": "Введите число или оставьте поле пустым.", - "9SIW2x": "Целевое значение для каждого запуска", - "6D6ffM": "Введите продолжительность в формате: дд:чч:мм (например, 12:00:00) или оставьте поле пустым.", - "4BN53Q": "Мы покажем Вам, насколько близко или далеко от цели находится значение каждого запуска, а также нанесем его на график.", - "xvBDOH": "Вы уверены, что хотите архивировать сценарий {title}?", - "lBqu4h": "Восстановить сценарий", - "FXCLuZ": "{total, number} всего", - "FEGywG": "Укажите будущую дату/время для напоминания об обновлении.", - "9kCT7Q": "Упростите проведение ретроспектив с помощью временной шкалы, которая автоматически отслеживает ключевые события и сообщения, чтобы команды всегда были под рукой.", - "9Obw6C": "Фильтр", - "4cwL43": "С архивом", - "4aupaG": "Сценарий {title} успешно восстановлен.", - "wX3k9U": "Безымянный сценарий", - "vndQuC": "Быстрая команда выполнена", - "vjzpnC": "Нет сценариев, соответствующих этим фильтрам.", - "v1SpKO": "Изменения ролей", - "twieZh": "Перейти к обзору запуска", - "tVPYMu": "Админ сценария", - "t6SiGO": "Запуски сейчас в процессе", - "ruJGqS": "Доступ к сценарию", - "rDvvQs": "{completed, number} / {total, number} завершено", - "oVHn4s": "Последнее обновление", - "lrbrjv": "Да, запустить ретроспективу", - "lbhO3D": "курсив", - "lZwZi+": "День: {date}", - "l0hFoB": "Добавить описание сценария...", - "kvgvNW": "Узнать, что случилось", - "kDcpd/": "{numKeywords, plural, one {одно ключевое слово} few {# ключевых слова} other {# ключевых слов}}", - "jvo0vs": "Сохранить", - "jnmORb": "В этом сценарии", - "jXT2++": "Перейти на канал", - "j7jdWG": "Преобразование в коммерческую редакцию.", - "ijAUQf": "Сообщите системному администратору о необходимости обновления.", - "ieGrWo": "Следить", - "hO9EdA": "Пригласить {numInvitedUsers, plural, =0 {никого} =1 {одного участника} other {# участников}} в канал", - "gy/Kkr": "(отредактировано)", - "fV6578": "Назначение владельца", - "fUEpLA": "Нет событий временной шкалы, соответствующих этим фильтрам.", - "dvhvum": "(Необязательно) Опишите, как следует использовать этот сценарий", - "bE1Cro": "Только мои запуски", - "Z/hwEf": "Канал получит напоминание о выполнении ретроспективы {reminderEnabled, select, true {every} other {}}", - "YORRGQ": "Опубликовать обновление", - "YMrTRm": "Сводка по запуску", - "YKn+7s": "В этом канале нет ни одного сценария.", - "YDuW/T": "{num_runs, plural, =0 {Еще не запущено} one {# запуск} other {# всего запусков}}", - "XXbWAU": "Выберите это, чтобы получать обновления при запуске этого сценария.", - "Vhnd2J": "Переключить описание", - "V5TY0z": "Добавить участников?", - "UMoxP9": "Шаблон названия канала (необязательно)", - "SmAUf9": "Напоминание будет отправлено {timestamp}", - "SXJ98n": "Вы не сможете редактировать ретроспективный отчет после его публикации. Вы хотите опубликовать ретроспективный отчет?", - "SVwJTM": "Экспорт", - "SENRqu": "Помощь", - "RoGxij": "Действует {date}", - "RO+BaS": "Скопируйте ссылку для запуска", - "R/2lqw": "Выберите шаблон", - "R+JQaJ": "Участники канала", - "QpUBDr": "{members, plural, =0 {Никто не может} =1 {Один участник может} few{# человека могут} other {# человек могут}} получить доступ к этому сценарию.", - "Q8Qw5B": "Описание", - "Q7hMnp": "Запустить сценарий", - "OsDomv": "Все события", - "OK8u0r": "Создайте сценарий, чтобы предписать рабочий процесс, которому должны следовать Ваши команды и инструменты, включая все, от чек-листов, действий, шаблонов и ретроспектив.", - "Nh91Us": "{from, number}–{to, number} из {total, number} всего", - "NA7Cw1": "Скопировать ссылку на сценарий", - "N2IrpM": "Подтвердить", - "MrJPOh": "Включить обновления статуса", - "M/2yY/": "Еще никто.", - "Lo10yH": "Неизвестный канал", - "L6k6aT": "…или начните с шаблона", - "KJu1sq": "Удалить чек-лист", - "K4O03z": "Новая задача", - "Ja1sVR": "Обновления статуса были отключены для этого запуска сценария.", - "JXdbo8": "Выполнено", - "ICqy9/": "Чек-листы", - "I90sbW": "прямо сейчас", - "I5NMJ8": "Еще", - "Hzwzgs": "Транслируйте обновления в {oneChannel, plural, one {канал} few {канала} other {каналов}}", - "HhLp57": "цитата", - "EvBQLq": "Сделать админом сценария", - "EWz2w5": "Запустить Сценарий", - "EQpfkS": "Завершенный", - "EC5MJD": "Нет доступных обновлений.", - "DXACD6": "Публикация ретроспективного отчета и доступ к временной шкале", - "9XUYQt": "Импорт", - "VOzlSL": "Запуск сценария организует рабочие процессы для Вашей команды и инструментов.", - "/urtZ8": "Ваши Сценарии", - "hVFgh4": "Включить готовые", - "XmUdvV": "Вся необходимая статистика", - "DSVJjB": "Сейчас работает сценарий {playbookTitle}", - "Cy1AK/": "Посмотреть подробности запуска", - "CkYhdY": "Добавьте канал в категорию на боковой панели", - "CjNrqO": "Шаблон ретроспективного отчета", - "C9NScU": "Управляйте своей командой", - "C6Oghd": "Изменить сводку запуска", - "BQtd5I": "Добро пожаловать в Сценарии!", - "BNB75h": "Сценарий предписывает контрольные списки, средства автоматизации и шаблоны для любых повторяющихся процедур. {br} Это помогает командам сократить количество ошибок, завоевать доверие заинтересованных сторон и повысить эффективность с каждой итерацией.", - "Auj1ap": "Начните пробную версию или обновите подписку.", - "ArpdYl": "События временной шкалы отображаются здесь по мере их возникновения. Наведите курсор на событие, чтобы удалить его.", - "AF9wda": "Это обновление будет сохранено на странице обзора{hasBroadcast, select, true { и передано в {broadcastChannelCount, plural, =1 {один канал} other {{broadcastChannelCount, number} каналы}} } other {}}.", - "A21Mgv": "Запуск завершен", - "91Hr5f": "Перетащите меня, чтобы изменить порядок", - "zz6ObK": "Восстановить", - "zx0myy": "Участники", - "zINlao": "Владелец", - "yxguVq": "", - "yqpcOa": "Использовать", - "ypIsVG": "Восстановить задачу", - "xmcVZ0": "Поиск", - "x8cvBr": "Посмотреть обзор запуска", - "x5Tz6M": "Отчет", - "wylJpv": "Все в {team} могут просматривать этот сценарий.", - "wbsq7O": "Использование", - "waVyVY": "Активные участники", - "wZ83YL": "Не сейчас", - "wO6NOM": "", - "wL7VAE": "Действия", - "wEQDC6": "Изменить", - "w0muFd": "Отправка исходящего вебхука (По одному на строку)", - "viXE32": "Частный", - "vaYTD+": "Есть {outstanding, plural, =1 {# невыполненная задача} few {# невыполненные задачи} other {# невыполненных задач}}. Вы уверены, что хотите закончить запуск?", - "v1DNMW": "Ретроспектива опубликована {name}", - "usa8vQ": "Отправить приветственное сообщение", - "uny3Zy": "Сценарии", - "uhu5aG": "Общедоступный", - "u4MwUB": "Сохраните историю запуска вашего сценария", - "tzMNF3": "Статус", - "syEQFE": "Публиковать", - "sqNmlF": "Пропустить ретроспективу", - "soePYH": "{num_checklists, plural, =0 {нет чек-листов} one {# чек-лист} few {# чек-листа} other {# чек-листов}}", - "scYyVv": "Хотите заполнить ретроспективный отчет?", - "sDKojV": "Архивировать сценарий", - "s3jjqi": "{num_actions, plural, =0 {никаких действий} one {# действие} few {# действия} other {# действий}}", - "recCg9": "Обновления", - "rbrahO": "Закрыть", - "rX08cW": "Дата должна быть в будущем.", - "qyJtWy": "Показать меньше", - "qsr3Zk": "Обновить Сводку запуска", - "q6f8x9": "Изменения с последнего обновления", - "q0cpUe": "Добавить чек-лист", - "pjt3qA": "Новый чек-лист", - "pKLw8O": "Вы уверены, что хотите удалить это событие? Удаленные события будут навсегда удалены из временной шкалы.", - "pK6+CW": "@{displayName} не является участником канала [{runName}]({overviewUrl}). Хотите добавить на этот канал? У них будет доступ ко всей истории сообщений.", - "osuP6z": "Перетащите, чтобы изменить порядок чек-листа", - "o2eHmz": "Запуск завершен {name}", - "o+ZEL3": "Опубликовано {timestamp}", - "nqVby7": "{numTasksChecked, number} из {numTasks, number} {numTasks, plural, =1 {задача} other {задачи}} проверена(-ы)", - "nmpevl": "", - "nkCCM2": "Вам больше не напомнят.", - "nSFBC2": "+ Добавить задачу", - "m/Q4ye": "Переименовать чек-лист", - "lxfpbh": "Владелец {reminderEnabled, select, true {будет запрашивать обновление статуса каждый} other {не будет запрашивать обновление статуса}}", - "lQT7iD": "Создать Сценарий", - "lJyq2a": "Запуск не найден", - "l7zMH6": "Выберите вариант или укажите продолжительность", - "kXFojL": "Вы также можете создать сценарий заранее, чтобы он был доступен, когда потребуется.", - "kGI46P": "Описание задания", - "k9q07e": "Трансляция обновления на другие каналы", - "k1djnL": "Удалить чек-лист", - "jwimQJ": "Ок", - "jS/UOn": "Обновить шаблон", - "jIgqRa": "Владелец / Участники", - "jIIWN+": "преформатированный", - "iNU1lj": "Запуск, который вы запрашиваете, является частным или не существует.", - "iDMOiz": "УЧАСТНИКИ КАНАЛА", - "hrgo+E": "Архив", - "hfrrC7": "Инициалы Команды", - "hXIYHG": "Установите и включите плагин Экспорт Канала для поддержки экспорта канала", - "h+e7G+": "", - "guunZt": "Назначить", - "gt6BhE": "Детали запуска", - "gGcNUr": "У вас нет разрешений", - "g5pX+a": "О...", - "g4IF1x": "Для этого сценария нет запусков.", - "g0mp+I": "При преобразовании в частный сценарий история участия и запусков сохраняется. Это изменение является постоянным и не может быть отменено. Вы уверены, что хотите преобразовать {playbookTitle} в частный сценарий?", - "fuDLDJ": "Создать канал", - "fmylXu": "Предлагать запустить сценарий, когда пользователь отправляет сообщение", - "fXGjhC": "Сменился владелец с {summary}", - "eiPBw7": "Интервал ретроспективного напоминания", - "egvJrY": "Правопреемник изменен", - "edxtzC": "Создать сценарий", - "eLeFE2": "Изменить имя и описание", - "eHAvFf": "жирный", - "e/AZL5": "Ваш 30-дневный пробный период начался", - "dsTLW1": "Изменить задачу", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {запуск} few {запуска} other {запусков}} в ходе выполнения", - "dSC1YD": "Пропустить задачу", - "d9epHh": "Экспорт журнала канала", - "d8KvXJ": "Срок действия вашей пробной лицензии истекает {expiryDate}. Вы можете приобрести лицензию в любое время через Customer Portal, чтобы избежать сбоев.", - "d4g2r8": "Удалено: {timestamp}", - "cp7KUI": "Сценарий", - "bPLen5": "Запуски завершены за последние 30 дней", - "bLK+Kr": "Напоминает канал с заданным интервалом для заполнения ретроспективы.", - "bGhCLX": "Когда публикуется обновление", - "b40Pr7": "Репортер", - "b3TdyZ": "Нажимая Запустить пробную версию, я соглашаюсь с Соглашением об оценке программного обеспечения Mattermost, Политикой конфиденциальности и получением электронных писем о продуктах.", - "avPeEI": "Обновите, чтобы просмотреть тенденции для общего количества запусков, активных запусков и участников, участвующих в запусках этого сценария.", - "aYIUar": "Спасибо!", - "aWpBzj": "Показать больше", - "ZdWYcm": "Нет, пропустить ретроспективу", - "ZWtlyd": "Запуск восстановлен {name}", - "ZAJviT": "Мы не смогли уведомить системного администратора.", - "Z7vWDQ": "Была допущена ошибка", - "X2K92H": "Название чек-листа", - "X/koAN": "Неверная запись: максимально количество вебхуков – 64", - "WTQpnI": "Примите меры прямо сейчас, используя сценарии", - "WAHCT2": "Уведомить системного администратора", - "W1Qs5O": "Запуски", - "W/V6+Y": "Свернуть", - "VmnoW8": "Пожалуйста, проверьте системные журналы для получения дополнительной информации.", - "Ui6GK/": "Когда новый участник присоединится к каналу", - "TxCTXQ": "Вы уверены, что хотите закончить запуск?", - "8n24G2": "Просмотр сведений о запуске на боковой панели", - "GG1yhI": "Существуют шаблоны для различных вариантов использования и событий. Вы можете использовать сборник сценариев как есть или настроить его, а затем поделиться им со своей командой.", - "LmhSmU": "Подтвердить удаление записи", - "E0LnBo": "Вы можете выбрать вариант или указать свою продолжительность (\"2 недели\", \"3 дня 12 часов\", \"45 минут\", ...)", - "MJ89uW": "Преобразование в частный сценарий", - "9TTfXU": "Ваш системный администратор был уведомлен.", - "/fU9y/": "Вы можете подробно ознакомиться с различными разделами сценария на этой странице.", - "mVpO8u": "Видели это раньше?", - "WIxhrv": "Имя запуска должно содержать не менее двух символов", - "CBM4vh": "Таймер для следующего обновления", - "c8hxKk": "Неделя {date}", - "9m0I/B": "Держите заинтересованные стороны в курсе", - "OcpRSQ": "Удалить запись", - "0Xt1ea": "Вы по-прежнему сможете получить доступ к историческим данным для этой метрики.", - "BD66u6": "Загрузите CSV-файл, содержащий все сообщения с канала", - "a0hBZ0": "Удалить метрику", - "UbTsGY": "Запуски начались между {start} и {end}", - "lUfDe1": "Экспортируйте канал запуска сценария и сохраните его для последующего анализа.", - "NYTGIb": "Понятно", - "5j6GD/": "{numParticipants, plural, =0 {нет участников} =1 {# участник} few {# участника} other {# участников}}", - "HXvk56": "Опубликовать обновления статуса", - "HLn43R": "Управление доступом", - "1QosTr": "Использован", - "1ikfp3": "Если вы удалите эту метрику, ее значения не будут собираться для будущих запусков.", - "hw83pa": "Отслеживайте ключевые показатели и измеряйте ценность", - "KUr+sG": "Обновить сводку запуска", - "HGdWwZ": "Создавайте и назначайте задачи", - "I2zEie": "Отмечайте успехи и учитесь на ошибках с помощью ретроспективных отчетов. Отфильтруйте события временной шкалы для проверки процессов, взаимодействия с заинтересованными сторонами и целей аудита.", - "rzbYbE": "Цель", - "q/VD+s": "Установите таймеры и составьте шаблон для обновления статуса, чтобы заинтересованные стороны всегда были в курсе событий.", - "TxmjKI": "Опишите, о чем этот показатель", - "XpDetT": "Отказаться от этих советов.", - "N1U/QR": "Изменения состояния задачи", - "JCGvY/": "Этот шаблон помогает стандартизировать формат повторяющихся обновлений, которые происходят при каждом запуске.", - "b5FaCc": "Добавить канал в категорию боковой панели", - "VZRWFk": "например, стоимость, покупки", - "LRFvqz": "Объявить в {oneChannel, plural, one {канале} other {каналах}}", - "mbo96h": "Настройте пользовательские метрики для заполнения в ретроспективном отчете", - "udrLSP": "Используйте метрики, чтобы понять закономерности и прогресс в запусках, а также отслеживать производительность.", - "Tt04f1": "Смотрите, кто вовлечен и что нужно сделать, не выходя из беседы.", - "OHfpS1": "Содержащие любое из этих ключевых слов", - "Lg3I1b": "@{targetUsername}, обновите статус.", - "K3r6DQ": "Удалить", - "JqKASQ": "Добавить @{displayName} в канал", - "JJNc3c": "Предыдущий", - "TdTXXf": "Узнать больше", - "TZYiF/": "удар", - "TBez4r": "Нет сценариев для просмотра. У Вас недостаточно прав для создания сценариев в этом рабочем пространстве.", - "T7Ry38": "Сообщение", - "QywYDe": "Отметьте запуск как завершенный", - "Qrl6bQ": "Оптимизируйте процессы с помощью сценариев", - "QnZAit": "Добавить необязательное описание", - "QiKcO7": "Введите ретроспективный шаблон", - "QaZNp9": "Завершить запуск", - "QUwMsX": "Напоминание о заполнении ретроспективы", - "Q7aZO4": "{numParticipants, plural, =0 {нет активных участников} =1 {# активный участник} few {# активных участника} other {# активных участников}}", - "Oo5sdB": "Имя сценария", - "ObmjTB": "Быстрая команда", - "OINwWS": "Создайте {isPublic, select, true {общедоступный} other {частный}} канал", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {задача} few {задачи} other {задач}}", - "CyGaem": "Название запуска", - "/HtNUp": "Выберите или укажите {mode, select, DurationValue {time span (\"4 hours\", \"7 days\"...)} DateTimeValue {time (\"in 4 hours\", \"May 1\", \"Tomorrow at 1 PM\"...)} other {time or time span}}", - "lbs7UO": "за запуск по последним 10 запускам", - "Vf/QlZ": "Диапазон значений", - "ZNNjWw": "Пожалуйста, введите число.", - "mvZUm3": "Здесь вы можете подробно изучить компоненты вашего сценария. Выберите «Изменить», чтобы настроить сценарий в соответствии с вашими процессами и моделями.", - "KXVV4+": "Добро пожаловать на страницу предварительного просмотра сценария!", - "ru+JCk": "Среднее значение", - "69nlA3": "Введите продолжительность в формате: дд:чч:мм (например, 12:00:00).", - "M4gAc9": "Добавить значение", - "fmbSyg": "Добавить значение (в дд:чч:мм)", - "NiAH1z": "Целевое значение", - "NMxVd+": "Пожалуйста, заполните значение метрики.", - "NLeFGn": "на", - "xVyHgP": "Начать тестовый запуск", - "l5/RKZ": "Для этого сценария нет готовых запусков.", - "efeNi1": "10-проходное среднее значение", - "9a9+ww": "Заголовок", - "awG90C": "Цель на запуск", - "B3Q5mz": "Триггер", - "5AJmOz": "Когда пользователь присоединяется к каналу", - "0RlzlZ": "Отправить временное приветственное сообщение пользователю", - "DPj6DM": "Выберите Выполнить, чтобы увидеть его в действии.", - "Y4MU/9": "Выберите Начать тестовый запуск, чтобы увидеть его в действии.", - "RUlvbf": "Протестируйте новый сценарий!", - "Ob5cSv": "Внесенные Вами изменения не будут сохранены, если Вы покинете эту страницу. Вы уверены, что хотите отменить изменения и уйти?", - "MHzP9I": "Определите приветственное сообщение для пользователей, присоединяющихся к каналу.", - "MBNMo9": "Действия канала", - "c23IHq": "Действия канала позволяют автоматизировать действия для этого канала", - "ao44YC": "Настроить метрики", - "Ek1Fx2": "Когда сообщение с этими ключевыми словами публикуется", - "9j5KzL": "Введите название категории", - "+/x2FM": "Выберите сценарий", - "u4L4yd": "У вас есть несохраненные изменения", - "+PMJAg": "Подписаться на {followers, plural, =1 {одного пользователя} other {# пользователей}}", - "dCtjdj": "Готовы запустить свой сценарий?", - "Z3ybv/": "Добавьте канал в боковую панель для пользователя", - "p1I/Fx": "Мы автоматически создали Ваш запуск", - "e3z3P8": "Отменить и уйти", - "aEhjYg": "Сюжет", - "Ppx673": "Отчеты", - "2Q5PhZ": "Подсказка как запустить сценарий", - "u7qh13": "Готовы запустить свой сценарий?", - "mLrh+0": "Нет срока", - "iMjjOH": "Следующая неделя", - "MtrTNy": "Завтра", - "MbapTE": "{num} {num, plural, =1 {задача} few {задачи} other {задач}} просрочена(-ы)", - "I7+d55": "Укажите дату/время (\"через 4 часа\", \"1 мая\"...)", - "AF7+5o": "Добавить срок", - "zWgbGg": "Сегодня", - "oBeKB4": "Крайний срок {date}", - "mw9jVA": "Добавить заголовок", - "lyXljU": "Дублировать задачу", - "lkv547": "Крайний срок (доступно в плане Professional)", - "g9pEhE": "Срок", - "lglICE": "Добавить описание (необязательное)", - "W0aij2": "Назначить для...", - "UlJJ1i": "Добавить слэш-команду", - "TTIQ6E": "Назначайте сроки выполнения задачам, чтобы исполнители могли расставлять приоритеты и успевать выполнять задачи.", - "NFyWnZ": "Работайте эффективнее", - "371AC3": "Обновить сводку запуска", - "Xgxruo": "Пропустить чек-лист", - "7P5T3W": "Восстановить чек-лист", - "oAJsne": "Общий сценарий", - "mm5vL8": "Только приглашенные участники", - "lJ48wN": "Частный сценарий", - "RQl8IW": "Отложить на…", - "9trZXa": "Любой участник команды может просматривать", - "OqCzNb": "Добавить задачу", - "JcefuP": "Добавить описание (необязательно)", - "v5/Cox": "Дублировать чек-лист", - "mCrdeS": "Всего запусков сценария", - "IxtSML": "Добавить чек-лист", - "CwwzAU": "Добавить название чек-листа", - "4GjZsL": "Всего сценариев", - "/+8SGX": "Показано {filteredNum} из {totalNum} событий", - "+qDKgW": "Просмотреть все обновления", - "/RnCQb": "Отправить исходящий Webhook", - "/GCoTA": "Очистить", - "//o1Nu": "Отключить обновления", - "0Azlrb": "Управление", - "03oqA2": "Активные запуски", - "/qDObA": "Обзор запусков", - "2BCWLD": "Конфигурация канала", - "3Yvt4d": "Playbooks - это настраиваемые контрольные списки, которые определяют повторяющийся процесс для команд. Цель - достижение конкретных и предсказуемых результатов", - "36NwLv": "Управление списком участников запуска", - "2NDgJq": "Последнее обновление статуса", - "28FTjr": "Запуск действий позволяет автоматизировать активности для этого канала", - "1prgB2": "Поиск людей", - "1fXVVz": "Срок исполнения...", - "1OluNs": "Подтвердите включение обновления статуса", - "1GOpgL": "Получатель...", - "0QD99o": "Запрос на присоединение к каналу", - "0CeyUV": "Нет результатов для \"{searchTerm}\"", - "3sXVwy": "Действия по заданию...", - "3qPQMX": "{name} запросил обновление статуса", - "3hBelc": "Ретроспектива не ожидается." -} diff --git a/webapp/playbooks/i18n/sl.json b/webapp/playbooks/i18n/sl.json deleted file mode 100644 index 5ccf224dbde..00000000000 --- a/webapp/playbooks/i18n/sl.json +++ /dev/null @@ -1,444 +0,0 @@ -{ - "/1FEJW": "AKTIVNIH UDELEŽENCEV na dan v zadnjih 14 dneh", - "+hddg7": "Dodaj v časovno os zagona", - "+Tmpup": "Ko zaženete ta priročnik, samodejno prejemate posodobitve.", - "+QgvjN": "Dodeli vlogo lastnika", - "+8G9qr": "Privzeto besedilo za retrospektivo.", - "4alprY": "", - "42qmJ5": "", - "1ikfp3": "", - "JJNc3c": "", - "wL7VAE": "", - "LDYFkN": "", - "2Qq4YX": "", - "2563nT": "", - "0wJ7N+": "", - "WAHCT2": "", - "1isgPF": "", - "JrZ2th": "", - "N2IrpM": "", - "rbrahO": "", - "NJ9uPu": "", - "QbGfqo": "", - "Z/hwEf": "", - "hXIYHG": "", - "Q8Qw5B": "", - "XpDetT": "", - "dxyZg3": "", - "5A46pW": "", - "1QosTr": "", - "/MaJux": "", - "MJ89uW": "", - "GRTyvN": "", - "G/yZLu": "", - "F4pfM/": "", - "Sx3lHL": "", - "1MQ3XZ": "", - "iDMOiz": "", - "Q5hysF": "", - "LI7YlB": "", - "w0muFd": "", - "0Vvpht": "", - "lUfDe1": "", - "3Psa+5": "", - "0EEIkR": "", - "5qBEKB": "", - "5FRgqE": "", - "6D6ffM": "", - "V5TY0z": "", - "KiXNvz": "", - "M/2yY/": "", - "IfxUgC": "", - "5Ofkag": "", - "mVpO8u": "", - "0q+hj2": "", - "lxfpbh": "", - "9kCT7Q": "", - "q/Qo8l": "", - "wZ83YL": "", - "K4O03z": "", - "Ui6GK/": "", - "9XUYQt": "", - "0tznw6": "", - "c8hxKk": "", - "2/2yg+": "", - "4BN53Q": "", - "Lo10yH": "", - "QaZNp9": "", - "b5FaCc": "", - "jXT2++": "", - "EWz2w5": "", - "wEQDC6": "", - "wX3k9U": "", - "KUr+sG": "", - "CBM4vh": "", - "kGI46P": "", - "CjNrqO": "", - "6uhSSw": "", - "wPVxBN": "", - "LmhSmU": "", - "udrLSP": "", - "vndQuC": "", - "4fHiNl": "", - "4vuNrq": "", - "iXNbPf": "", - "AF9wda": "", - "d8KvXJ": "", - "GjCS6U": "", - "f+bqgK": "", - "6n0XDG": "", - "D55vrs": "", - "EQpfkS": "", - "Ietscn": "", - "gsMPAS": "", - "cPIKU2": "", - "dSC1YD": "", - "wO6NOM": "", - "bTgMQ2": "", - "hw83pa": "", - "vJ2SaW": "", - "q/VD+s": "", - "cEWBE3": "", - "Q3R9Uj": "", - "I5DYM+": "", - "HGdWwZ": "", - "GAuN6w": "", - "9m0I/B": "", - "wbdGb5": "", - "vL4++D": "", - "fhMaTZ": "", - "R5Zh+l": "", - "8n24G2": "", - "lgZf0l": "", - "GG1yhI": "", - "dZmYk6": "", - "vQqT/8": "", - "Pue+oV": "", - "6GTzTR": "", - "0HT+Ib": "", - "/urtZ8": "", - "/fU9y/": "", - "y7o4Rn": "", - "3rCdDw": "", - "3PoGhY": "", - "2VrVHu": "", - "0Xt1ea": "", - "X2K92H": "", - "uT4ebt": "", - "tbjmvS": "", - "soePYH": "", - "rzbYbE": "", - "rMhrJH": "", - "mbo96h": "", - "VZRWFk": "", - "TxmjKI": "", - "Qrl6bQ": "", - "Nh91Us": "", - "NA7Cw1": "", - "KJu1sq": "", - "B487HA": "", - "zz6ObK": "", - "zx0myy": "", - "zWkvNO": "", - "zINlao": "", - "zELxbG": "", - "z3B83t": "", - "yxguVq": "", - "yqpcOa": "", - "ypIsVG": "", - "xvBDOH": "", - "wsUmh9": "", - "wcWpGs": "", - "wbwhbH": "", - "wbsq7O": "", - "vjzpnC": "", - "viXE32": "", - "vaYTD+": "", - "vNiZXF": "", - "uhu5aG": "", - "u4MwUB": "", - "sqNmlF": "", - "scYyVv": "", - "sVlNlY": "", - "sQu1rA": "", - "sIX63S": "", - "sDKojV": "", - "s3jjqi": "", - "ryrP8K": "", - "rX08cW": "", - "q6f8x9": "", - "q0cpUe": "", - "pjt3qA": "", - "pKLw8O": "", - "pK6+CW": "", - "oVHn4s": "", - "oS0w4E": "", - "o2eHmz": "", - "o+ZEL3": "", - "nqVby7": "", - "m/Q4ye": "", - "lQT7iD": "", - "lJyq2a": "", - "lBqu4h": "", - "l7zMH6": "", - "l0hFoB": "", - "kvgvNW": "", - "kXFojL": "", - "kDcpd/": "", - "k9q07e": "", - "jvo0vs": "", - "jnmORb": "", - "jS/UOn": "", - "jIgqRa": "", - "jIIWN+": "", - "hzt6l8": "", - "hO9EdA": "", - "h+e7G+": "", - "gy/Kkr": "", - "guunZt": "", - "gt6BhE": "", - "gGcNUr": "", - "g5pX+a": "", - "g4IF1x": "", - "fXGjhC": "", - "fV6578": "", - "fUEpLA": "", - "egvJrY": "", - "edxtzC": "", - "e/AZL5": "", - "dvhvum": "", - "dsTLW1": "", - "djALPR": "", - "bPLen5": "", - "bLK+Kr": "", - "bGhCLX": "", - "b3TdyZ": "", - "b/QBNs": "", - "avPeEI": "", - "aYIUar": "", - "aWpBzj": "", - "YMrTRm": "", - "YKn+7s": "", - "YDuW/T": "", - "XmUdvV": "", - "XXbWAU": "", - "UbTsGY": "", - "UMoxP9": "", - "TxCTXQ": "", - "SENRqu": "", - "SDSqfA": "", - "S0kWcH": "", - "RthEJt": "", - "RoGxij": "", - "QywYDe": "", - "QpUBDr": "", - "QnZAit": "", - "QiKcO7": "", - "QUwMsX": "", - "Q7aZO4": "", - "OsDomv": "", - "OINwWS": "", - "OHfpS1": "", - "O8o2lE": "", - "MrJPOh": "", - "MTzF3S": "", - "Lg3I1b": "", - "LRFvqz": "", - "L6k6aT": "", - "K3r6DQ": "", - "JqKASQ": "", - "Ja1sVR": "", - "JXdbo8": "", - "JJMNME": "", - "JCGvY/": "", - "IuFETn": "", - "IOnm/Z": "", - "I2zEie": "", - "Hzwzgs": "", - "HAlOn1": "", - "FXCLuZ": "", - "FEGywG": "", - "EvBQLq": "", - "EC5MJD": "", - "DXACD6": "", - "DSVJjB": "", - "DCl7Vv": "", - "D9IV7i": "", - "9TTfXU": "", - "9+Ddtu": "", - "8hDbW6": "", - "7VTSeD": "", - "5j6GD/": "", - "4cwL43": "", - "4aupaG": "", - "v1SpKO": "", - "v1DNMW": "", - "usa8vQ": "", - "uny3Zy": "", - "tzMNF3": "", - "twieZh": "", - "tVPYMu": "", - "t6SiGO": "", - "syEQFE": "", - "ruJGqS": "", - "recCg9": "", - "rDvvQs": "", - "qyJtWy": "", - "qsr3Zk": "", - "osuP6z": "", - "nmpevl": "", - "nSFBC2": "", - "lrbrjv": "", - "lbhO3D": "", - "lZwZi+": "", - "k1djnL": "", - "jwimQJ": "", - "ijAUQf": "", - "ieGrWo": "", - "hrgo+E": "", - "Auj1ap": "", - "ZkhArX": "", - "eiPBw7": "", - "j7jdWG": "", - "a0hBZ0": "", - "W/V6+Y": "", - "waVyVY": "", - "FGzxgY": "", - "/4tOwT": "", - "iNU1lj": "", - "NYTGIb": "", - "5BUxvl": "", - "SXJ98n": "", - "TZYiF/": "", - "RzEVnf": "", - "W1Qs5O": "", - "VmnoW8": "", - "OK8u0r": "", - "nkCCM2": "", - "GxJAK1": "", - "X/koAN": "", - "x8cvBr": "", - "5ciuDD": "", - "d9epHh": "", - "3MSGcL": "", - "R/2lqw": "", - "b40Pr7": "", - "47FYwb": "", - "4Hrh5B": "", - "/HtNUp": "", - "TSSNg/": "", - "HXvk56": "", - "0oLj/t": "", - "0oL1zz": "", - "MFpAtm": "", - "9SIW2x": "", - "Tt04f1": "", - "OyZnsJ": "", - "yhU1et": "", - "hfrrC7": "", - "hVFgh4": "", - "g0mp+I": "", - "fuDLDJ": "", - "fmylXu": "", - "eLeFE2": "", - "eHAvFf": "", - "d4g2r8": "", - "cp7KUI": "", - "bE1Cro": "", - "aACJNp": "", - "ZdWYcm": "", - "ZWtlyd": "", - "ZAJviT": "", - "Z7vWDQ": "", - "YORRGQ": "", - "WTQpnI": "", - "WIxhrv": "", - "Vhnd2J": "", - "VOzlSL": "", - "TdTXXf": "", - "TJo5E6": "", - "TBez4r": "", - "T7Ry38": "", - "T5rX+W": "", - "SmAUf9": "", - "SVwJTM": "", - "RO+BaS": "", - "R+JQaJ": "", - "Q7hMnp": "", - "Oo5sdB": "", - "OcpRSQ": "", - "ObmjTB": "", - "N1U/QR": "", - "MvEydR": "", - "Mm1Gse": "", - "JeqL8w": "", - "ICqy9/": "", - "I90sbW": "", - "I5NMJ8": "", - "HhLp57": "", - "HSi3uv": "", - "HLn43R": "", - "GwtR3W": "", - "E0LnBo": "", - "DtCplA": "", - "DnBhRg": "", - "CSts8B": "", - "BNB75h": "", - "BD66u6": "", - "A8dbCS": "", - "A21Mgv": "", - "9PXW6Q": "", - "9Obw6C": "", - "91Hr5f": "", - "6CGo3o": "", - "5wqhGy": "", - "5CI3KH": "", - "D2CE02": "", - "CyGaem": "", - "Cy1AK/": "", - "CkYhdY": "", - "C9NScU": "", - "C6Oghd": "", - "C1khRR": "", - "BQtd5I": "", - "ArpdYl": "", - "ApULhK": "", - "AS5kar": "", - "AML4RW": "", - "9uOFF3": "", - "9tBhzB": "", - "9qc7BX": "", - "z3A0LP": "", - "xmcVZ0": "", - "x5Tz6M": "", - "wylJpv": "", - "4ltHYh": "", - "3Ls2m+": "", - "36GNZj": "", - "3/wF0G": "", - "2QkJ4s": "", - "1I48bs": "", - "15jbT0": "", - "/jUtaM": "", - "/gbqA6": "", - "/ZsEUy": "", - "/YZ/sw": "", - "M4gAc9": "", - "lbs7UO": "", - "NMxVd+": "", - "KXVV4+": "", - "6jDabx": "", - "awG90C": "", - "69nlA3": "", - "NLeFGn": "", - "mvZUm3": "", - "Vf/QlZ": "", - "9a9+ww": "", - "xVyHgP": "", - "ru+JCk": "", - "l5/RKZ": "", - "fmbSyg": "", - "efeNi1": "", - "ZNNjWw": "", - "NiAH1z": "" -} diff --git a/webapp/playbooks/i18n/sv.json b/webapp/playbooks/i18n/sv.json deleted file mode 100644 index 50a9583553b..00000000000 --- a/webapp/playbooks/i18n/sv.json +++ /dev/null @@ -1,797 +0,0 @@ -{ - "XmUdvV": "All statistik du behöver", - "VmpFFw": "Ingen beskrivning finns tillgänglig.", - "UbTsGY": "Körningar startade mellan {start} och {end}", - "TZYiF/": "genomstruken", - "TSSNg/": "TOTALT ANTAL KÖRNINGAR per vecka under de senaste 12 veckorna", - "TJo5E6": "Förhandsgranskning", - "T7Ry38": "Meddelande", - "T5rX+W": "Hur ofta ska en uppdatering publiceras?", - "SFuk1v": "Behörigheter", - "SENRqu": "Hjälp", - "RthEJt": "Retrospektiv", - "RoGxij": "Aktiv den {date}", - "R+JQaJ": "Kanalmedlemmar", - "QnZAit": "Lägg till valfri beskrivning", - "QiKcO7": "Ange en mall för retrospektiv granskning", - "Q8Qw5B": "Beskrivning", - "Q7aZO4": "{numParticipants, plural, =0 {inga aktiva deltagare} =1 {# aktiv deltagare} other {# aktiva deltagare}}", - "ObmjTB": "Slashkommando", - "NE1OeI": "All i team({team}) har åtkomst.", - "KiXNvz": "Kör", - "JCGvY/": "Den här mallen hjälper till att standardisera formatet för återkommande uppdateringar som sker under varje körning.", - "IuFETn": "Varaktighet", - "ICqy9/": "Checklistor", - "HhLp57": "citat", - "EC5MJD": "Det finns inga uppdateringar tillgängliga.", - "DnBhRg": "Lägg till personer", - "DCl7Vv": "kodformatering i textflödet", - "CL5OZP": "Endast användare du valt kommer kunna ändra eller köra den här spelboken.", - "BD66u6": "Ladda ner en CSV-fil som innehåller alla meddelanden från kanalen", - "AS5kar": "Deltagare ({participants})", - "AF9wda": "Uppdateringen kommer sparas till översikten{hasBroadcast, select, true { och publiceras i {broadcastChannelCount, plural, =1 {en kanal} other {{broadcastChannelCount, number} kanaler}}} other {}}.", - "A3ptul": "Mallar", - "9uOFF3": "Översikt", - "6Lwe7T": "Alla i {team} har åtkomst till den här spelboken", - "5Ot7cd": "Bestäm vilken typ av kanal som den här spelboken skapar.", - "5FRgqE": "Hämtar kanal-logg", - "5A46pW": "Lägg till slash-kommandon", - "47FYwb": "Avbryt", - "3rCdDw": "Statusuppdateringar", - "1MQ3XZ": "{numActiveRuns, plural, =0 {inga aktiva körningar} =1 {# aktiv körning} other {# aktiva körningar}}", - "1I48bs": "Mall för retrospektiv", - "/jUtaM": "AKTIVA KÖRNINGAR per dag de senaste 14 dagarna", - "/HtNUp": "Välj eller ange {mode, select, DurationValue {tidsspann (\"4 timmar\", \"7 dagar\"...)} DateTimeValue {tid (\"inom 4 immar\", \"1 maj\", \"13:00 imorgon\"...)} other {tid eller tidsspann}}", - "/1FEJW": "AKTIVA DELTAGARE per dag de senaste 14 dagarna", - "+ZIXOR": "Kanalåtkomst", - "+8G9qr": "Standardtext för retrospektiv.", - "TyrY2b": "Skapa spelbok", - "D3idYv": "Inställningar", - "AT2QBo": "Endast utpekade användare kan skapa spelböcker.", - "zINlao": "Ägare", - "z5RMPO": "Endast du har åtkomst till den här spelboken", - "wbwhbH": "Uppgiftsnamn", - "wbsq7O": "Användning", - "waVyVY": "Aktiva deltagare", - "wEQDC6": "Ändra", - "viXE32": "Privat", - "v3+TmO": "{members, plural, =0 {Ingen} =1 {En person} other {# personer}} har tillgång till spelboken", - "uhu5aG": "Publik", - "uJ3bRR": "Den här mallen hjälper till att standardisera formatet för en kortfattad beskrivning som förklarar varje körning för intressenterna.", - "t6SiGO": "Pågående körningar", - "soePYH": "{num_checklists, plural, =0 {ingen checklista} one {# checklista} other {# checklistor}}", - "sQu1rA": "{numTotalRuns, plural, =0 {inga körningar startade} =1 {# körning startad} other {# körningar startade}}", - "s3jjqi": "{num_actions, plural, =0 {inga åtgärder} one {# åtgärd} other {# åtgärder}}", - "recCg9": "Uppdateringar", - "rX08cW": "Datumet måste ligga i framtiden.", - "oS0w4E": "Standardtid för uppdatering", - "lbhO3D": "kursiv", - "lZwZi+": "Dag: {date}", - "jvo0vs": "Spara", - "jq4eWU": "Tillgång till spelbok", - "jXT2++": "Gå till kanal", - "jIIWN+": "förformatterad", - "hzt6l8": "Använd Markdown för att skapa en mall.", - "hXIYHG": "Installera och aktivera plugin Channel Export för att få möjlighet att exportera kanalen", - "gy/Kkr": "(redigerad)", - "g5pX+a": "Om", - "eiPBw7": "Intervall för retrospektivpåminnelse", - "ebkl6I": "Alla i teamet har åtkomst till den här spelboken", - "eHAvFf": "fet", - "djXM+y": "Endast utvalda användare får tillgång.", - "dcV/DJ": "{timestamp}", - "d9epHh": "Exportera kanalloggen", - "c8hxKk": "Vecka för {date}", - "bPLen5": "Slutförda körningar de senaste 30 dagarna", - "bLK+Kr": "Påminner kanalen med visst intervall om att fylla i retrospektiv.", - "b40Pr7": "Reporter", - "avPeEI": "Uppgradera för att se trender över antal körningar, aktiva körningar och deltagare involverade i körningar av den här spelboken.", - "YDuW/T": "{num_runs, plural, =0 {Har inte körts ännu} one {# körning} other {# totala körningar}}", - "X3DLGJ": "Alla i arbetsytan kan skapa spelböcker.", - "MvEydR": "{name} postade en statusuppdatering", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {uppgift} other {uppgifter}}", - "LmhSmU": "Bekräfta radera", - "LRFvqz": "Meddela i {oneChannel, plural, one {kanalen} other {kanalerna}}", - "KUr+sG": "Uppdatera sammanfattningen av körningar", - "JeqL8w": "Retrospektiv avbruten av {name}", - "I2zEie": "Fira framgångar och lär dig av misstag med retrospektiva rapporter. Filtrera händelser i tidslinjen efter processgranskning, engagemang från intressenter och granskningsändamål.", - "Hzwzgs": "Sänd uppdateringar i {oneChannel, plural, one {kanal} other {kanaler}}", - "FEGywG": "Ange en kommande datum/tid för uppdateringspåminnelsen.", - "DXACD6": "Publicera en retrospektivrapport och få tillgång till tidslinjen", - "CjNrqO": "Mall för retrospektivrapport", - "ArpdYl": "Här visas händelser i tidslinjen när de inträffar. Håll muspekaren över en händelse för att ta bort den.", - "AML4RW": "Aktivitetstilldelningar", - "9Obw6C": "Filter", - "8hDbW6": "Skicka en utgående webhook", - "4Hrh5B": "{name} ändrade status från {summary}", - "3/wF0G": "Slash-kommandon", - "+QgvjN": "Tilldela rollen ägare till", - "lQT7iD": "Skapa en playbook", - "EQpfkS": "Slutförd", - "42qmJ5": "Du har inte behörighet att skicka en uppdatering.", - "l0hFoB": "Lägg till en beskrivning av playbooken...", - "/MaJux": "Starta retrospektivt", - "e/AZL5": "Din 30-dagars prova-på-period har börjat", - "IfxUgC": "Och lägg till en sammanfattning av körningen…", - "Auj1ap": "Starta en prova-på-period eller uppgradera ditt abonnemang.", - "hO9EdA": "Bjud in {numInvitedUsers, plural, =0 {inga medlemmar} =1 {en medlem} other {# medlemmar}} till kanalen", - "9TTfXU": "Din systemadministratör har blivit notifierad.", - "5qBEKB": "Vad är playbook-körningar?", - "ijAUQf": "Meddela din systemadministratör att uppgradera.", - "Z/hwEf": "Kanalen kommer att påminnas om att utföra retrospektiv {reminderEnabled, select, true {varje} other {}}", - "W1Qs5O": "Körs", - "rDvvQs": "{completed, number} / {total, number} klar", - "YORRGQ": "Publicera en uppdatering", - "aYIUar": "Tack!", - "6uhSSw": "Välj en kanal", - "fmylXu": "", - "SmAUf9": "En påminnelse kommer att skickas {timestamp}", - "5j6GD/": "{numParticipants, plural, =0 {inga aktiva deltagare} =1 {# aktiv deltagare} other {# aktiva deltagare}}", - "5Ofkag": "Aktivera retrospektivt", - "wX3k9U": "Playbook utan titel", - "0tznw6": "Konvertera till en privat playbook", - "kvgvNW": "Vet vad som hände", - "lJyq2a": "Körningen hittas inte", - "kDcpd/": "{numKeywords, plural, other {# nyckelord}}", - "wsUmh9": "", - "/ZsEUy": "Är du säker på att du vill ta bort den här checklistan? Den tas bort från den här körningen men påverkar inte denna playbook.", - "+hddg7": "Lägg till i tidslinjen för körning", - "D9IV7i": "Retrospektiv var inaktiverad för den här körningen.", - "cp7KUI": "Playbook", - "j7jdWG": "Konvertera till en kommersiell version.", - "u4MwUB": "Spara din playbooks körningshistorik", - "DSVJjB": "För närvarande körs playbook {playbookTitle}", - "D55vrs": "Din licens kunde inte genereras", - "Cy1AK/": "Visa information om körning", - "jS/UOn": "Uppdatera mallen", - "9+Ddtu": "Nästa", - "9PXW6Q": "Varaktighet / påbörjad den", - "FXCLuZ": "{total, number} totalt", - "91Hr5f": "Ta tag och drag för att organisera om", - "hfrrC7": "Team-initialer", - "fV6578": "Tilldela rollen ägare till", - "d4g2r8": "Borttagen: {timestamp}", - "vjzpnC": "Det finns inga playbooks som motsvarar valt filter.", - "vaYTD+": "Det {outstanding, plural, =1 {finns # utestående uppgift} other {finns # utestående uppgifter}}. Är du säker att du vill avsluta körningen?", - "vNiZXF": "Inga körningar pågår för närvarande. Kör en playbook för att börja orkestrera ditt teams arbetsflöden och dina verktyg.", - "sIX63S": "Din systemadministratör har blivit notifierad", - "sDKojV": "Arkivera playbook", - "ryrP8K": "Hantera behörigheter för vem som kan visa, ändra och köra denna playbook.", - "ruJGqS": "Tillgång till playbook", - "qyJtWy": "Visa mindre", - "qp3Fk4": "", - "lrbrjv": "Ja, starta retrospektiv", - "b5FaCc": "Lägg till kanalen i en kategori i sidofältet", - "b3TdyZ": "Genom att klicka på Starta prova-på-period godkänner jag Mattermost Software Evaluation Agreement, Privacy Policy och att ta emot mejl med produktinformation.", - "b/QBNs": "Uppdatering är planerad", - "ZAJviT": "Vi kunde inte meddela systemadministratören.", - "Z7vWDQ": "Ett fel har uppstått", - "YKn+7s": "Ingen playbook kör i denna kanal.", - "Y+U8La": "", - "XXbWAU": "Välj detta om du vill få automatiska uppdateringar när denna playbook körs.", - "W/V6+Y": "dra ihop", - "SDSqfA": "När en körning startar", - "RO+BaS": "Kopiera länken för att köra", - "JJNc3c": "Föregående", - "zx0myy": "Deltagare", - "z3B83t": "Sök efter en playbook", - "yxguVq": "", - "wZ83YL": "Inte just nu", - "wL7VAE": "Händelser", - "w0muFd": "Skicka utgående webhook (en per rad)", - "syEQFE": "Publicera", - "sVlNlY": "Alla team har olika struktur. Du kan hantera vilka användare i teamet som kan skapa playbooks.", - "q6f8x9": "Förändring sedan den senaste uppdateringen", - "oVHn4s": "Senaste uppdatering", - "nmpevl": "", - "nkCCM2": "Du kommer inte bli påmind igen.", - "k9q07e": "Sänd uppdatering till andra kanaler", - "iNU1lj": "Den körning du efterfrågar är privat eller existerar inte.", - "fuDLDJ": "Skapa en kanal", - "fpuWL1": "", - "eLeFE2": "Redigera namn och beskrivning", - "d8KvXJ": "Din testlicens löper ut den {expiryDate}. Du kan köpa en licens när som helst via Kundportalen för att undvika avbrott.", - "VmnoW8": "Kontrollera systemloggarna för mer information.", - "Ui6GK/": "När en ny medlem går med i kanalen", - "UMoxP9": "Mall för kanalnamn (valfritt)", - "OINwWS": "Skapa en {isPublic, select, true {publik} other {privat}} kanal", - "NA7Cw1": "Kopiera länken till playbook", - "Mm1Gse": "Sök efter medlem", - "M/2yY/": "Ingen ännu.", - "KJu1sq": "Ta bort checklistan", - "ypIsVG": "Återställ uppgiften", - "qsr3Zk": "", - "q0cpUe": "Lägg till en checklista", - "pKLw8O": "Är du säker på att du vill ta bort den här händelsen? Radade händelser kommer att tas bort från tidslinjen permanent.", - "nSFBC2": "", - "dSC1YD": "Hoppa över uppgiften", - "0q+hj2": "Definiera en mall för en kortfattad beskrivning som förklarar varje körning för intressenterna.", - "o+ZEL3": "Publicerad {timestamp}", - "SXJ98n": "Du kan inte redigera retrospekt-rapporten efter att du publicerat den. Vill du publicera retrospekt-rapporten?", - "8oCVbz": "", - "wylJpv": "Alla i {team} kan se denna playbook.", - "tVPYMu": "Playbook administratör", - "gGcNUr": "Du har inga behörigheter", - "g0mp+I": "När du konverterar till en privat playbook bevaras medlemskap och körhistorik. Den här ändringen är permanent och kan inte göras ogjord. Är du säker på att du vill konvertera {playbookTitle} till en privat playbook?", - "R/2lqw": "Välj en mall", - "QpUBDr": "{members, plural, =0 {Ingen} =1 {En person} other {# personer}} har tillgång till denna playbook.", - "OsDomv": "Alla händelser", - "MJ89uW": "Konvertera till en privat playbook", - "HLn43R": "Hantera åtkomst", - "EvBQLq": "Gör till Playbook-admin", - "EWz2w5": "Kör playbook", - "C1khRR": "Tillbaka till playbooks", - "5BUxvl": "Alla i teamet har åtkomst till denna playbook.", - "3Ls2m+": "Medlem i playbook", - "0Vvpht": "Gör till Playbook-medlem", - "osuP6z": "Dra för att ändra ordningen i checklistan", - "zz6ObK": "Återställ", - "wO6NOM": "", - "tzMNF3": "Status", - "twieZh": "Gå till översikten över körning", - "sqNmlF": "Hoppa över retrospektivt", - "pK6+CW": "@{displayName} är inte medlem i kanalen [{runName}]({overviewUrl}). Vill du lägga till dem i den här kanalen? De kommer att ha tillgång till all meddelandehistorik.", - "Lo10yH": "Okänd kanal", - "iDMOiz": "KANALMEDLEMMAR", - "hrgo+E": "Arkiv", - "hVFgh4": "Inkludera slutförda", - "dvhvum": "(Valfritt) Beskriv hur denna playbook ska användas", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {körning} other {körningar}} pågår", - "JqKASQ": "Lägg till @{displayName} i kanalen", - "HSi3uv": "Ingen mottagare", - "HAlOn1": "Namn", - "C9NScU": "Ge kontroll till teamet", - "5ciuDD": "INTE I KANALEN", - "TxCTXQ": "Är du säker på att du vill avsluta körningen?", - "QywYDe": "Markera också körningen som avslutad", - "2563nT": "Bekräfta att avsluta körningen", - "MrJPOh": "Aktivera statusuppdateringar", - "Ja1sVR": "Statusuppdateringar var inaktiverade för den här playbook-körningen.", - "v1DNMW": "Retrospektiv publicerad av {name}", - "m/Q4ye": "Byt namn på checklistan", - "k1djnL": "Ta bort checklistan", - "iXNbPf": "Byt namn", - "fUEpLA": "Det finns inga händelser på tidslinjen som matchar dessa filter.", - "X2K92H": "Namn på checklista", - "OK8u0r": "Skapa en playbook för att beskriva arbetsflödet som dina team och verktyg ska följa, inklusive checklistor, åtgärder, mallar och retrospektiva möten.", - "I5NMJ8": "Mer", - "9kCT7Q": "Gör retrospektiven enkla med en tidslinje som automatiskt håller reda på händelser och meddelanden så att teamet har dem nära till hands.", - "5CI3KH": "Kontakta support", - "2/2yg+": "Lägg till", - "O8o2lE": "", - "vndQuC": "Slash-kommando utfört", - "o2eHmz": "Körning avslutad av {name}", - "fXGjhC": "Ägare ändrades från {summary}", - "bGhCLX": "När en uppdatering publiceras", - "4vuNrq": "{duration} efter att körningen startade", - "4ltHYh": "Gå till playbook", - "3Psa+5": "", - "2VrVHu": "Sök efter körningsnamn", - "/gbqA6": "{duration} innan körningen startade", - "cPIKU2": "Följer", - "C6Oghd": "Redigera sammanfattningen av körningen", - "usa8vQ": "Skicka ett välkomstmeddelande", - "lxfpbh": "Ägaren kommer {reminderEnabled, select, true {bli efterfrågad att ge statusuppdateringar varje} other {inte bli efterfrågad att ge statusuppdatering}}", - "kXFojL": "Du kan också skapa en playbook i förväg så att den finns tillgänglig när du behöver den.", - "jIgqRa": "Ägare / Deltagare", - "L6k6aT": "...eller börja med en mall", - "3MSGcL": "Kanalnamnet är inte giltigt.", - "kGI46P": "", - "K4O03z": "Ny uppgift", - "0oL1zz": "Kopierad!", - "K3r6DQ": "Radera", - "Vhnd2J": "Växla beskrivningen", - "ZdWYcm": "Nej, hoppa över retrospekt", - "YMrTRm": "Sammanfattning av körning", - "X/koAN": "Ogiltig post: det högsta tillåtna antalet webhooks är 64", - "7VTSeD": "", - "/4tOwT": "", - "h+e7G+": "", - "v1SpKO": "Rollförändringar", - "36GNZj": "Playbook {title} arkiverades.", - "+Tmpup": "Du får automatiskt uppdateringar när den här spelboken körs.", - "l7zMH6": "Välj ett alternativ eller ange en egen varaktighet", - "0HT+Ib": "Arkiverad", - "rbrahO": "Stäng", - "pjt3qA": "Ny checklista", - "nqVby7": "{numTasksChecked, number} av {numTasks, number} {numTasks, plural, =1 {uppgift} other {uppgifter}} markerad", - "ZWtlyd": "Körning återställd av {name}", - "z3A0LP": "Den senaste körningen var {relativeTime}", - "xmcVZ0": "Sök", - "x8cvBr": "Visa en översikt över körningar", - "WTQpnI": "Agera med hjälp av playbooks", - "WIxhrv": "Körnamnet måste ha minst två tecken", - "WAHCT2": "Meddela systemadministratören", - "VOzlSL": "Genom att köra en playbook kan du orkestrera arbetsflöden för teamet och dina verktyg.", - "Qrl6bQ": "Effektivisera dina processer med playbooks", - "Q67RuY": "", - "Oo5sdB": "Playbook namn", - "N1U/QR": "Uppgiftsstatus ändrad", - "IOnm/Z": "Det finns ingen sammanfattning av körningen.", - "jwimQJ": "Ok", - "edxtzC": "Skapa en playbook", - "bE1Cro": "Endast mina körningar", - "V5TY0z": "Lägg till deltagare?", - "E0LnBo": "Du kan välja ett alternativ eller ange en anpassad varaktighet (\"2 veckor\", \"3 dagar 12 timmar\", \"45 minuter\", ...)", - "TdTXXf": "Mer information", - "TBez4r": "Det finns inga playbooks att visa. Du har inte behörighet att skapa playbooks i denna arbetsyta.", - "QaZNp9": "Slutför körningen", - "QUwMsX": "Påminnelse om att fylla i retrospektiv", - "Q7hMnp": "Kör playbook", - "OcpRSQ": "Radera posten", - "OHfpS1": "", - "Nh91Us": "{from, number}–{to, number} av totalt {total, number}", - "N2IrpM": "Bekräfta", - "Lg3I1b": "Vänligen ge en statusuppdatering @{targetUsername}.", - "Leh2tk": "", - "JJMNME": "{withRunName, select, true {@{authorUsername} har postat en uppdatering för [{runName}]({overviewURL})} other {@{authorUsername} har postat en uppdatering}}", - "GxJAK1": "Den playbook som du efterfrågar är privat eller existerar inte.", - "GwtR3W": "Dra och släpp en befintlig uppgift eller klicka för att skapa en ny uppgift.", - "GRTyvN": "Växla till Playbook-listan", - "G/yZLu": "Ta bort", - "DuRxjT": "", - "DtCplA": "{numParticipants, plural, =1 {# deltagare} other {# deltagare}}", - "D2CE02": "Ange webhook", - "CyGaem": "Namn på körning", - "CkYhdY": "Lägg till kanalen i en kategori i sidofältet", - "CSts8B": "Team-ikon", - "CBM4vh": "Timer för nästa uppdatering", - "BQtd5I": "Välkommen till Playbooks!", - "BNB75h": "En playbook innehåller checklistor, automatiseringar och mallar för alla upprepningsbara förfaranden. {br} Den hjälper team att minska antalet fel, vinna förtroende hos intressenterna och bli effektivare för varje iteration.", - "B487HA": "Pågår", - "ApULhK": "Bjud in medlemmar", - "9tBhzB": "Uppgradera nu", - "9qc7BX": "", - "yqpcOa": "Använd", - "yhU1et": "Uppgifter", - "scYyVv": "Vill du fylla i retrospektiv-rapporten?", - "guunZt": "Tilldela", - "gt6BhE": "Information om körning", - "g4IF1x": "Denna playbook har aldrig körts.", - "dsTLW1": "", - "aWpBzj": "Visa mer", - "S0kWcH": "Försenad uppdatering", - "JXdbo8": "Klar", - "Ietscn": "Uppgifter avslutade", - "I90sbW": "just nu", - "A8dbCS": "Playbook hittades inte", - "A21Mgv": "Körning avslutad", - "6n0XDG": "Är du säker på att du vill ta bort checklistan? Alla uppgifter kommer att tas bort.", - "6jDabx": "Ge återkoppling", - "6CGo3o": "Status / Senaste uppdatering", - "5wqhGy": "Växla detaljer om körning", - "2Qq4YX": "", - "2QkJ4s": "Spara viktiga meddelanden för att få en fullständig bild som förenklar retrospektiven.", - "2PNrBQ": "", - "15jbT0": "Lägg till mer i tidslinjen", - "0wJ7N+": "", - "0oLj/t": "Expandera", - "/YZ/sw": "Starta prova-på-period", - "zWkvNO": "Tidslinje", - "wcWpGs": "Ogiltiga URL:er för webhooks", - "uny3Zy": "Playbooks", - "uBLF+D": "", - "zELxbG": "Sparade meddelanden", - "x5Tz6M": "Rapportera", - "ieGrWo": "Följ", - "egvJrY": "Mottagare Ändrat", - "aACJNp": "Körning påbörjad av {name}", - "jnmORb": "I den här playbooken", - "HGdWwZ": "Skapa och tilldela uppgifter", - "HXvk56": "Skicka statusuppdateringar", - "Pue+oV": "", - "q/Qo8l": "Privata playbooks är endast tillgängliga i Mattermost Enterprise", - "dxyZg3": "Låt mig utforska själv", - "Q5hysF": "Gör mer med Playbooks", - "f+bqgK": "Namn på måttet", - "wPVxBN": "", - "vL4++D": "Uppföljning av framsteg och ägarskap", - "1QosTr": "Används av", - "rzbYbE": "Mål", - "8n24G2": "Visa detaljer om körning i en sidopanel", - "rMhrJH": "Sätt en rubrik för ditt mått.", - "mbo96h": "", - "y7o4Rn": "Är du säker på att du vill radera?", - "Tt04f1": "Se vem som är inblandad och vad som behöver göras utan att lämna konversationen.", - "6D6ffM": "Ange en varaktighet i formatet dd:hh:mm (t.ex. 31:23:59) eller lämna fältet tomt.", - "NYTGIb": "Jag fattar", - "mVpO8u": "Sett detta tidigare?", - "0Xt1ea": "Du kommer fortfarande att kunna få tillgång till historiska data för denna mätning.", - "4BN53Q": "Vi visar hur nära eller långt ifrån målet varje körning är och visar även värdet på ett diagram.", - "1isgPF": "", - "VZRWFk": "t.ex. kostnad, inköp", - "hw83pa": "Följ upp statistik och viktiga mätvärden", - "TxmjKI": "Beskriv vad mätetalet handlar om", - "a0hBZ0": "Ta bort mätvärden", - "vQqT/8": "", - "gsMPAS": "Kostnad", - "6GTzTR": "Se vad som finns i denna playbook när som helst", - "/fU9y/": "På den här sidan kan du läsa mer detaljer om olika delar av playbooken.", - "lBqu4h": "Återställa playbook", - "0EEIkR": "", - "RzEVnf": "Med hjälp av playbooks blir viktiga procedurer upprepbara och möjliga att följa i efterhand. En playbook kan köras flera gånger, och varje körning har sin egen logg och retrospekt.", - "GG1yhI": "Det finns mallar för en rad olika användningsområden och händelser. Du kan använda en playbook som den är eller anpassa den och sedan dela den med ditt team.", - "XpDetT": "Välj bort att få tips.", - "udrLSP": "Använd mätvärden för att förstå mönster och framsteg i olika körningar och spåra resultat.", - "lUfDe1": "Exportera körningens kanal och spara den för senare analys.", - "vJ2SaW": "Automatisera delar av din playbook, t.ex. genom att skicka välkomstmeddelande, bjuda in viktiga medlemmar och skapa en kanal med uppdateringar.", - "q/VD+s": "Sätt upp tidsramar och skapa en mall för statusuppdateringar så att intressenterna alltid är uppdaterade om vad som händer.", - "cEWBE3": "Utvärdera dina processer med hjälp av retrospektiv för att förfina och förbättra dem för varje körning.", - "Q3R9Uj": "Dokumentera alla steg för hela processen här. Tilldela respektive uppgift till ansvariga personer och lägg till eventuella tidslinjer eller länkade åtgärder.", - "I5DYM+": "Lär dig OCH reflektera", - "GAuN6w": "Ange antaganden", - "9m0I/B": "Håll intressenterna uppdaterade", - "wbdGb5": "Tilldela, bocka av eller hoppa över uppgifter för att se till att teamet vet hur kommer i mål tillsammans.", - "fhMaTZ": "Ta en snabb rundtur", - "R5Zh+l": "Här kan du först uppleva ett exempel på en playbook innan du lägger tid på att skapa din egen.", - "QbGfqo": "Sänd till intressenter på flera olika ställen och behåll spårning för retrospektiv med ett enda inlägg.", - "lgZf0l": "Kom igång med Playbooks", - "ZkhArX": "Kör igång!", - "GjCS6U": "Välj en mall", - "dZmYk6": "Playbook duplicerad", - "1ikfp3": "Om du raderar den här mätningen kommer värdena för den inte att samlas in för framtida körningar.", - "uT4ebt": "t.ex. antal resurser, berörda kunder", - "tbjmvS": "Det finns redan ett mätvärde med samma namn. Ange ett unikt namn för varje mätetal.", - "Sx3lHL": "Heltal", - "OyZnsJ": "per körning", - "NJ9uPu": "Viktiga mätvärden", - "LI7YlB": "Lägg till detaljer om vad mätvärdet handlar om och hur det ska fyllas i. Denna beskrivning kommer att finnas tillgänglig på sidan för retrospektivt arbete för varje körning där värden för dessa mätvärden kommer att anges.", - "LDYFkN": "Varaktighet (i dd:hh:mm)", - "JrZ2th": "Lägg till mätvärden", - "FGzxgY": "t.ex. Tid för att bekräfta, Tid för att lösa problemet", - "F4pfM/": "Ange ett nummer eller lämna fältet tomt.", - "9SIW2x": "Målvärde för varje körning", - "xvBDOH": "Är du säker på att du vill arkivera playbook {title}?", - "bTgMQ2": "Denna playbook är arkiverad.", - "MTzF3S": "Är du säker på att du vill återställa playbook {title}?", - "4cwL43": "Med arkiverade", - "4aupaG": "Playbook {title} återställdes framgångsrikt.", - "SVwJTM": "Exportera", - "9XUYQt": "Importera", - "4alprY": "Playbookmallar", - "/urtZ8": "Dina playbooks", - "4fHiNl": "Kopiera", - "3PoGhY": "Är du säker på att du vill publicera?", - "NMxVd+": "Fyll i mätvärdet.", - "lbs7UO": "per körning under de senaste 10 körningarna", - "69nlA3": "Ange en varaktighet i formatet: dd:hh:mm (t.ex. 31:23:59).", - "l5/RKZ": "Denna playbook har inga slutförda körningar.", - "awG90C": "Mål per körning", - "efeNi1": "Genomsnittligt värde över 10 körningar", - "fmbSyg": "Lägg till värde (i dd:hh:mm)", - "mvZUm3": "Här du kan utforska komponenterna i din playbook i detalj. Välj Redigera för att anpassa din playbook så att den passar dina processer och modeller.", - "xVyHgP": "Starta en testkörning", - "KXVV4+": "Välkommen till förhandsgranskning av playbook!", - "NiAH1z": "Målvärde", - "M4gAc9": "Lägg till värde", - "ZNNjWw": "Ange ett nummer.", - "ru+JCk": "Genomsnittligt värde", - "Vf/QlZ": "Värdeintervall", - "NLeFGn": "till", - "9a9+ww": "Titel", - "+PMJAg": "Börja följa {followers, plural, =1 {en användare} other {# användare}}", - "5ZIN3u": "Statusuppdateringar", - "5AJmOz": "När en användare går med i kanalen", - "4GjZsL": "Totalt antal spelböcker", - "3hBelc": "En retrospektiv förväntas inte.", - "371AC3": "Uppdatera sammanfattningen av körningen", - "2Q5PhZ": "Uppmaning att köra en playbook", - "28FTjr": "Med kör-uppgifter kan du automatisera aktiviteter för denna kanal", - "0RlzlZ": "Skicka ett tillfälligt välkomstmeddelande till användaren", - "/RnCQb": "Skicka utgående webhook", - "+/x2FM": "Välj en Playbook", - "7P5T3W": "Återställ checklistan", - "I7+d55": "Ange datum/tid (\"om 4 timmar\", \"1 maj\"...)", - "HvAcYh": "{text}{rest, plural, =0 {} one { och andra} other { och {rest} andra}}", - "GXjP8g": "Alla körningar som du har tillgång till visas här", - "GDCpPr": "Senaste statusuppdatering", - "F9LrJA": "Filtrera objekt", - "Ek1Fx2": "När ett meddelande med dessa nyckelord postas", - "DaHpK1": "Sök efter en kanal", - "DPj6DM": "Välj Kör för att se hur det fungerar.", - "CwwzAU": "Lägg till checklistans namn", - "Brya9X": "Lägg till en mall för sammanfattning av körningar…", - "B3Q5mz": "Utlösare", - "AF7+5o": "Lägg till förfallodag", - "9trZXa": "Vem som helst i teamet kan se", - "9kQNdp": "Denna playbook är privat.", - "9j5KzL": "Ange kategorinamn", - "+qDKgW": "Visa alla uppdateringar", - "IxtSML": "Lägg till en checklista", - "ZJS10z": "Inga uppdateringar har publicerats ännu", - "Z3ybv/": "Lägg till kanalen i en kategori i sidofältet för användaren", - "Z2Hfu4": "Lägg till en sammanfattning av körningen", - "YQOmSf": "Ange en webhook per rad", - "Y4MU/9": "Välj Starta en testkörning för att se hur det fungerar.", - "Xgxruo": "Hoppa över checklistan", - "XRyRzf": "Inga statusuppdateringar förväntas.", - "XF8rrh": "Kopiera länken till ''{name}''", - "W0aij2": "Tilldela till...", - "UlJJ1i": "Lägg till slash-kommando", - "TTIQ6E": "Ange förfallodatum på uppgifter så att de som tilldelas uppgifterna kan prioritera och få saker gjorda.", - "TD8WrM": "Duplicering är inaktiverad för teamet.", - "RrCui3": "Sammanfattning", - "RnOiCg": "Det var inte möjligt att {isFollowing, select, true {sluta följa} other {följa}} körningen", - "RUlvbf": "Testa din nya playbook!", - "RQl8IW": "Snooza i…", - "Q15rLN": "Begär uppdatering...", - "Ppx673": "Rapporter", - "OuZhcQ": "Ange varaktighet (\"8 timmar\", \"3 dagar\"...)", - "Ob5cSv": "De ändringar som du har gjort kommer inte att sparas om du lämnar den här sidan. Är du säker på att du vill kasta ändringarna och lämna sidan?", - "OQplDX": "En statusuppdatering förväntas varje . Nya uppdateringar kommer {channelCount, plural, =0 {inte skickas till någon kanal} one {skickas till # kanal} other {skickas till # kanaler}} och {webhookCount, plural, =0 {inga utgående webhooks} one {# utgående webhook} other {# utgående webhooks}}.", - "OKhRC6": "Dela", - "NFyWnZ": "Arbeta mer effektivt", - "MyIJbr": "Innehåll", - "MbapTE": "{num} {num, plural, =1 {uppgift försenad} other {uppgifter försenade}}", - "MHzP9I": "Definiera ett välkomstmeddelande för att välkomna användare som ansluter sig till kanalen.", - "MBNMo9": "Åtgärder i kanalen", - "LcC/pi": "Skicka ett välkomstmeddelande…", - "JcefuP": "Lägg till en beskrivning (valfritt)", - "4mCpAv": "Det gick inte att byta ägare", - "OqCzNb": "Lägg till en uppgift", - "MtrTNy": "Imorgon", - "Ul0aFX": "Importera playbook", - "LfhTNW": "Bläddra bland eller skapa playbooks och körningar", - "GVpA4Q": "Skapa en ny playbook", - "/qDObA": "Bläddra bland körningar", - "dCtjdj": "Är du redo att köra din playbook?", - "cyR7Kh": "Tillbaka", - "c6LNcW": "Ta bort uppgift", - "c23IHq": "Med kanal-händelser kan du automatisera aktiviteter i den här kanalen", - "bf5rs0": "Visa info", - "aZGAOI": "Lägg till mall för statusuppdatering…", - "aEhjYg": "Översikt", - "CFysvS": "Skapa Playbook dropdown", - "/+8SGX": "Visar {filteredNum} av {totalNum} händelser", - "g9pEhE": "Förfallna", - "e3z3P8": "Ignorera ändringar och lämna sidan", - "Xx0WZV": "Skicka meddelande", - "UePrSL": "{num} {num, plural, one {deltagare} other {deltagare}}", - "UMFnWV": "Visa retrospektiv", - "9xs0pp": "Lägg till värde...", - "gfUBRi": "Utse en ny ägare innan du lämnar körningen.", - "fnihsY": "Lämna", - "ePhhuK": "Din begäran skickades till körningens kanal.", - "cnfVhV": "Lämna {isFollowing, select, true {och sluta följa } other {}}körningen", - "ch4Vs1": "Begär uppdateringar för playbook-körningar med ett enda klick och få direkt meddelande när en uppdatering publiceras. Starta en kostnadsfri 30-dagars prova-på-period för testa.", - "b+DwLA": "Fråga om att få delta i denna körning.", - "XS4umx": "{name} snoozade en statusuppdatering", - "VpQKQE": "{displayName} är inte en deltagare i körningen. Vill du göra dem till deltagare? De kommer att ha tillgång till all meddelandehistorik i körningens kanal.", - "Suyx6A": "Importen av playbook har misslyckats. Kontrollera att JSON-filen är felfri och försök igen.", - "SMrXWc": "Favoriter", - "SK5APX": "Det var inte möjligt att lämna körningen.", - "RCT0Px": "Lägg till {displayName} till kanalen", - "QegBKq": "Gå med i playbook", - "Q4sutg": "Bekräfta att lämna{isFollowing, select, true { och att sluta följa} other {}}", - "PoX2HN": "Skicka förfrågan", - "PdRg+3": "Visa alla...", - "PWmZrW": "Visa alla körningar", - "PW+sL4": "N/A", - "P6PLpi": "Gå med", - "P6NEL/": "Kommando...", - "OfN7IN": "En begäran om statusuppdatering skickas till körningens kanal.", - "Mjq//Y": "Ta bort favorit", - "KzHQCQ": "Det finns inga slutförda körningar som motsvarar valt filter.", - "KeO51o": "Kanal", - "Gwmqz5": "Begär en uppdatering", - "FgydNe": "Visa", - "CV1ddt": "Delta i körningen", - "CUhlqp": "bild för produktrundtur", - "B9z0uZ": "Din begäran om att få delta i körningen misslyckades.", - "AhY0vJ": "Lämna och avfölj", - "AH+V3r": "Bli deltagare i körningen.", - "5PpBsd": "Din förfrågan lyckades inte.", - "5Hzwqs": "Markera som favorit", - "5HXkY/": "Typ: {typeTitle}", - "4Iqlfe": "Du har deltagit i denna körning.", - "3zF589": "Återställ till alla {filterName}", - "1fXVVz": "Förfallodag...", - "1GOpgL": "Mottagare...", - "+6DCr9": "Som deltagare kan du uppdatera status, tilldela och slutföra uppgifter och göra retrospektiv.", - "lr1CUA": "Bläddra bland playbooks", - "kYCbJE": "Lägg till en tidsram", - "kV5GkX": "När en statusuppdatering publiceras", - "j940pJ": "Uppdateringen sparas på översiktssidan.", - "iMjjOH": "Nästa vecka", - "iEtImk": "När du lämnar{isFollowing, select, true { och avföljer en körning} other { en körning}} tas den bort från den vänstra sidofältet. Du kan hitta den igen genom att visa alla körningar.", - "hjteuA": "Alla playbooks som du har tillgång till visas här", - "m/KtHt": "Du har inte behörighet att ändra ägare", - "lyXljU": "Duplicera uppgiften", - "lkv547": "Förfallodag (finns tillgänglig i Professional abonnemang)", - "lbr3Lq": "Kopiera länken", - "lKeJ+i": "Det finns ingen sammanfattning", - "lJ48wN": "Privat playbook", - "kkw4kS": "Den här uppdateringen kommer publiceras i {hasChannels, select, true {{broadcastChannelCount, plural, =1 {en kanal} other {{broadcastChannelCount, number} kanaler}}} other {}}{hasFollowersAndChannels, select, true { och } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {ett direktmeddelande} other {{followersChannelCount, number} direktmeddelanden}}} other {}}.", - "kEMvwX": "Det finns inga körningar som motsvarar valt filter.", - "j2VYGA": "Visa alla playbooks", - "iigkp8": "Är det dags att avsluta?", - "zWgbGg": "Idag", - "zW/5AB": "Professionel-funktion Detta är en betal-funktion som är tillgänglig med en gratis 30-dagars prova-på-period", - "wGp7l3": "{icon} Dollar", - "u6Fyic": "Din begäran skickades till körningens kanal.", - "s+rSpl": "{icon} heltal", - "opn6uf": "Visa tidslinje", - "ojQue/": "{icon} Varaktighet (i dd:hh:mm)", - "ocYb9S": "Viktiga nyckeltal", - "oL7YsP": "Senast redigerad {timestamp}", - "oAJsne": "Publik playbook", - "o6N9pU": "Kör åtgärder", - "mm5vL8": "Endast inbjudna medlemmar", - "mkLeuq": "Sänd uppdatering till utvalda kanaler", - "mCrdeS": "Totalt antal körningar i playbook", - "zl6378": "Konfigurera mätvärden för retrospektiv", - "yllba1": "Den här arkiverade playbook kan inte byta namn.", - "xfnuXm": "Delta", - "xHNF7i": "Kör åtgärder", - "xEQYo5": "Konfigurera anpassade mätvärden som ska fyllas i den retrospektiva rapporten.", - "x1phlu": "Ingen tidsram", - "wRM2AO": "Uppdateringsbegäran misslyckades.", - "wBZz47": "Du har lämnat körningen.", - "vSMfYU": "Information om körning", - "vDvWJ6": "Prova uppdateringsbegäran med en gratis prova-på-period", - "v5/Cox": "Duplicera checklistan", - "u4L4yd": "Du har ej sparade förändringar", - "sX5Mn5": "Ange en webhook per rad", - "sGJpuF": "Lägg till en beskrivning…", - "qp5G0Z": "Uppgradering krävs för att få tillgång till retrospektiva funktioner.", - "pzTOmv": "Följare", - "pFK6bJ": "Visa alla", - "p1I/Fx": "Vi har automatiskt skapat din körning", - "oBeKB4": "Förfaller den {date}", - "nc8QpJ": "Senast aktivitet", - "mw9jVA": "Lägg till en titel", - "mNgqXf": "För att låsa upp den här funktionen:", - "mLrh+0": "Ingen förfallodag", - "qGlwfc": "Starta körning", - "j2FnDV": "Alla kanaler kommer skapas med detta namn", - "vqmRBs": "Bekräfta återstart", - "k5EChD": "Är du säker på att du vill återstarta körningen?", - "izWS4J": "Avbryt följandet", - "iQhFxR": "Senast använd", - "Zg0obP": "Återstarta körningen", - "XnICdK": "Det gick inte att ansluta till körningen", - "KjNfA8": "Felaktig tids-längd", - "03oqA2": "Aktiva körningar", - "wCDmf3": "Aktivera uppdateringar", - "w4Nhhb": "Lägg till deltagare", - "utHl3F": "Lägg till personer till {runName}", - "unwVil": "Begäran om att ansluta till en kanal misslyckades.", - "qDxsQH": "Bli en deltagare för att interagera med denna körning", - "q48ca7": "Ge feedback om Playbooks.", - "nsd54s": "Bekräfta att statusuppdateringar ska inaktiveras", - "lqzBNa": "Ta bort dem från kanalen för körningen", - "l/W5n7": "Deltagarna kommer också att läggas till i den kanal som är kopplad till denna körning", - "jrOlPO": "Få meddelanden om uppdatering av status på körningen", - "jAo8dd": "Statusuppdateringar i körningen inaktiverades av {name}", - "ieL3dC": "Konfigurera kanal-aktiviteter", - "ha1TB3": "När en deltagare ansluter sig till körningen", - "fVMECF": "Deltagare", - "cpGAhx": "Är du säker på att du vill inaktivera statusuppdateringar för den här körningen?", - "cUCiWw": "Bli en deltagare", - "bCmvTY": "Ge återkoppling", - "b8Gps8": "Statusuppdateringar i körningen aktiverades av {name}", - "ZRv7Dm": "Begäran om medlemskap", - "Z18I+c": "Med kanal-händelser kan du automatisera aktiviteter i den här kanalen", - "Y1EoT/": "När en deltagare lämnar körningen", - "WFA0Cg": "Är du säker på att du vill aktivera statusuppdateringar för den här körningen?", - "WC+NOj": "Lägg också till personer i kanalen som är länkad till denna körning", - "M9tXoZ": "Din begäran om deltagande skickades till körningens kanal.", - "H7IzRB": "Inaktivera statusuppdateringar", - "FLG4Iu": "Sätt som ägare av körningen", - "9qqGGd": "Bjud in deltagare", - "6rygzu": "Ta bort från körning", - "5b1zuB": "Lägg till dem i körningens kanal", - "1prgB2": "Sök efter personer", - "1OluNs": "Bekräfta att statusuppdateringar ska aktiveras", - "1OVPiC": "Bli deltagare i körningen. Som deltagare kan du skicka statusuppdateringar, tilldela och slutföra uppgifter och göra retrospektiv.", - "0QD99o": "Förfrågan om att ansluta till kanalen", - "0Azlrb": "Hantera", - "/GCoTA": "Nollställ", - "//o1Nu": "Inaktivera uppdateringar", - "u/yGzS": "{name} la till @{user} till körningen", - "t6lwwM": "{requester} tog bort {users} från körningen", - "jfpnye": "@{user} lämnade körningen", - "feNxoJ": "{requester} la till {users} till körningen", - "ecS/qx": "{name} la till {num} deltagare till körningen", - "VM75su": "{name} tog bort {num} deltagare från körningen", - "SwlL5j": "@{user} anslöt till körningen", - "RXjd3Q": "{name} tog bort @{user} från körningen", - "L1tFef": "Kontrollera stavningen eller sök efter något annat", - "KQunC7": "Används i denna kanal", - "IdTL+v": "Skapa en kanal för körning", - "I0NIMp": "Dina uppgifter", - "HfjhwE": "Sök playbooks", - "Gg/nch": "DELTAR INTE", - "GZoWl1": "Automatisera aktiviteter för den här uppgiften", - "EVSn9A": "Starta körning", - "DUU48k": "Det finns ingen uppgift som uttryckligen har tilldelats dig. Du kan utöka din sökning med hjälp av filtren.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {# försenade}}", - "Bgt0C8": "Denna uppdatering för körningen {runName} kommer att publiceras i {hasChannels, select, true {{broadcastChannelCount, plural, =1 {en kanal} other {{broadcastChannelCount, number} kanaler}}} other {}}{hasFollowersAndChannels, select, true { och som } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {ett direktmeddelande} other {{followersChannelCount, number} direktmeddelanden}}} other {}}.", - "BJNrYQ": "Som deltagare kan du uppdatera sammanfattningen av körningen, bocka av uppgifter, skicka statusuppdateringar och redigera retrospekt.", - "AoNLta": "Det finns inga avslutade körningar kopplade till denna kanal", - "AG7PKJ": "Byt namn på körning", - "9X3jwi": "{icon} Kostnad", - "9AQ5FE": "Sammanfattning av körning", - "95v+5O": "{actions, plural, =0 {Task Actions} one {# åtgärd} other {# åtgärder}}", - "7KMbBa": "Har aldrig använts", - "3sXVwy": "Åtgärder för uppgiften...", - "3Yvt4d": "Playbooks är konfigurerbara checklistor som definierar en upprepningsbar process för att teamet ska uppnå specifika och förutsägbara resultat", - "36NwLv": "Hantera listan över deltagare i körningen", - "2NDgJq": "Senaste statusuppdatering", - "2BCWLD": "Konfigurera kanal", - "0CeyUV": "Inga sökresultat för \"{searchTerm}\"", - "zxj2Gh": "Senast uppdaterad {time}", - "zscc/+": "Det {outstanding, plural, =1 {finns # utestående uppgift} other {finns # utestående uppgifter}}. Är du säker att du vill avsluta körningen {runName} för alla deltagare?", - "zSOvI0": "Filter", - "yP3Ud4": "Det finns inga pågående körningar som är kopplade till denna kanal", - "uCS6py": "Du har inte behörighet att se denna playbook", - "tqAmbk": "Körningar som pågår", - "qxYWTy": "Visa alla uppgifter från körningar som jag äger", - "prs4kX": "När ett meddelande med specifika nyckelord publiceras", - "meD+1Q": "DELTAGARE I KÖRNINGEN", - "m8hzTK": "Senast använd {time}", - "lqceIp": "eller Importera en playbook", - "l3QwVw": "Välj kanal", - "ksG35Q": "Du har inte behörighet att skapa playbooks i den här arbetsytan.", - "kQAf2d": "Välj", - "k7Nzfi": "Inaktivera inbjudan", - "iH5e4J": "Du kommer också att läggas till i den kanal som är kopplad till denna körning.", - "grv9Fm": "Välj för att växla mellan en lista med uppgifter.", - "gS1i4/": "Markera uppgiften som klar", - "gGtlrk": "Dina playbooks", - "fwW0T1": "Bekräfta att ta bort för-angivna medlemmar", - "fvNMLo": "Åtgärder för uppgiften", - "fBG/Ge": "Kostnad", - "dK2JKl": "Länka till en befintlig kanal", - "cGCoJe": "Skriven av", - "bEoDyV": "@{authorUsername} har postat en uppdatering för [{runName}]({overviewURL})", - "a2r7Vb": "Privat kanal", - "ZSa3cf": "@{targetUsername}, ge en statusuppdatering för [{runName}]({playbookURL}).", - "Z1sgPO": "Visa avslutade körningar", - "YKLHXL": "Visa pågående körningar", - "YBvwXR": "Inga tilldelade uppgifter", - "Wy3sw+": "{count, plural, =1{1 körning pågår} =0 {Inga körningar pågår} other {# körningar pågår}}", - "WFd88+": "Visa utförda uppgifter", - "W1EKh5": "Skapa en ny playbook", - "VjJYEV": "t.ex. försäljningsresultat, inköp", - "VA1Q/S": "Publik kanal", - "UAS7Bn": "Begär tillgång till den kanal som är kopplad till denna körning", - "TnUG7m": "Du har ingen pågående uppgift tilldelad.", - "TP/O/b": "Ta bort användare", - "SRqpbI": "{assignedNum, plural, =0 {Inga tilldelade uppgifter} other {# tilldelade}}", - "SRbTcY": "Andra playbooks", - "RgQwWr": "Sortera körningar efter", - "RC6rA2": "Nyligen skapad", - "QvEO6m": "Du har inte behörighet att redigera denna körning", - "QJTSaI": "Länka körning till en annan kanal", - "Q/t0//": "Slutförda körningar", - "NNksk4": "Alfabetiskt", - "NGKqOC": "Lägg också till mig i kanalen som är länkad till denna körning", - "LKu0ex": "Är du säker på att du vill avsluta körningen {runName} för alla deltagare?", - "L6vn9U": "Deltagare i körningen", - "IE2BzH": "Det finns användare som har tilldelats en eller flera uppgifter i förväg. Om du inaktiverar inbjudningar rensar du alla förtilldelade uppgifter.{br}{br}Är du säker på att du vill inaktivera inbjudningar?", - "DQn9Uj": "Användaren {name} har tilldelats en eller flera uppgifter i förväg. Om du inte automatiskt bjuder in denna användare kommer uppgifterna sakna denne som tilldelad.{br}{br}Är du säker på att du vill avbryta inbjudan av den här användaren som medlem i körningen?", - "BiQjuS": "Körning flyttad till {channel}", - "9w0mDI": "Bekräfta att ta bort en för-angiven medlem", - "mILd++": "Namnet på körningen bör inte vara längre än {maxLength} tecken", - "uYrkxy": "Filen måste vara en giltig JSON-playbook-mall.", - "m4vqJl": "Filer", - "Zbk+OU": "Filstorleken överskrider gränsen på 5MB.", - "MieztS": "Släpp en exporterad playbook-fil för att importera den.", - "HGSVzc": "Det går inte att importera flera filer samtidigt.", - "LaseGE": "Du har inte behörighet att redigera den här checklistan", - "Edy3wX": "Checklistan flyttad till {channel}", - "8//+Yb": "Länka checklistan till en annan kanal", - "706Soh": "utförda uppgifter", - "vjb+hS": "{user} återförde punkten \"{name}\" på checklistan", - "XHJUSG": "Följ körningar automatiskt", - "DqTQOp": "En gång", - "DKiv0o": "{user} skippade punkten \"{name}\" på checklistan", - "8FzC0B": "{user} checkade av punkten \"{name}\" på checklistan", - "3qPQMX": "{name} begärde en statusuppdatering", - "9M92On": "Välj kanaler", - "OqWwvQ": "{user} avmarkerade punkten \"{name}\"", - "N7Ln74": "Kör igen", - "DoskyC": "Alla team", - "9S+ZiL": "Inget team är valt" -} diff --git a/webapp/playbooks/i18n/tr.json b/webapp/playbooks/i18n/tr.json deleted file mode 100644 index 8759f4c7709..00000000000 --- a/webapp/playbooks/i18n/tr.json +++ /dev/null @@ -1,765 +0,0 @@ -{ - "/1FEJW": "Son 14 gündeki ETKİN KATILIMCILAR", - "+hddg7": "Oyun zaman çizelgesine ekle", - "+Tmpup": "Bu senaryo oynandığında güncellemeleri otomatik olarak alırsınız.", - "+QgvjN": "Şu kullanıcıya sahip rolü ata", - "+PMJAg": "{followers, plural, =1 {Bir kullanıcıyı} other {# kullanıcıyı}} izlemeye başla", - "+8G9qr": "Geçmiş değerlendirmesi için varsayılan metin.", - "+/x2FM": "Bir senaryo seçin", - "/YZ/sw": "Denemeyi başlat", - "/RnCQb": "Giden web bağlantısı gönder", - "/MaJux": "Geçmiş değerlendirmesini başlat", - "/HtNUp": "Bir {mode, select, DurationValue {zaman aralığı (\"4 saat\", \"7 gün\"...)} DateTimeValue {zaman (\"4 saat içinde\", \"1 Mayıs\", \"Yarın 13:00\"...)} other {zaman ya da zaman aralığı}} seçin veya belirtin", - "/ZsEUy": "Bu kontrol listesini silmek istediğinize emin misiniz? Bu oyunda kaldırılacak ancak senaryo etkilenmeyecek.", - "/fU9y/": "Senaryonun farklı bölümlerini bu sayfadan inceleyebilirsiniz.", - "47FYwb": "İptal", - "42qmJ5": "Güncelleme gönderme izniniz yok.", - "3rCdDw": "Durum güncellemeleri", - "3PoGhY": "Yayınlamak istediğinize emin misiniz?", - "3MSGcL": "Kanal adı geçersiz.", - "3Ls2m+": "Senaryo üyesi", - "371AC3": "Oyun özetini güncelle", - "36GNZj": "{title} senaryosu arşivlendi.", - "3/wF0G": "Bölü komutları", - "2VrVHu": "Oyun adına göre arama", - "2QkJ4s": "Geçmiş değerlendirmesini kolaylaştıran eksiksiz bir tablo için önemli iletileri kaydedin.", - "2Q5PhZ": "Bir senaryoyu oynama isteği", - "28FTjr": "Oyun işlemleri, bu kanal için etkinlikleri otomatikleştirmenizi sağlar", - "2563nT": "Oyunun tamamlandığını onayla", - "2/2yg+": "Ekle", - "1ikfp3": "Bu ölçümü silerseniz, gelecekteki oyunlarda bu ölçüm değeri toplanmaz.", - "1QosTr": "Kullanan", - "1MQ3XZ": "{numActiveRuns, plural, =0 {etkin bir oyun yok} =1 {# oyun etkin} other {# oyun etkin}}", - "1I48bs": "Geçmiş değerlendirmesi kalıbı", - "15jbT0": "Zaman çizelgenize daha fazlasını ekleyin", - "0tznw6": "Özel senaryoya dönüştür", - "0q+hj2": "Her oyunu paydaşlarına açıklayan kısa açıklama için bir kalıp tanımlayın.", - "0oLj/t": "Genişlet", - "0oL1zz": "Kopyalandı!", - "0Xt1ea": "Bu ölçüm için geçmiş verilere erişmeyi sürdürebileceksiniz.", - "0Vvpht": "Senaryo üyesi olarak ekle", - "0RlzlZ": "Kullanıcıya geçici bir hoş geldiniz mesajı gönderin", - "0HT+Ib": "Arşivlenmiş", - "/urtZ8": "Senaryolarınız", - "/jUtaM": "Son 14 gündeki günlük ETKİN OYUNLAR", - "/gbqA6": "Oyuna başlamadan {duration} önce", - "JeqL8w": "Geçmiş değerlendirmesi {name} tarafından iptal edildi", - "I2zEie": "Geçmiş değerlendirmesi raporlarıyla başarının hakkını verin ve hatalardan ders alın. Süreç incelemesi, paydaş katılımı ve denetim amaçları için zaman çizelgesi olaylarını süzün.", - "DXACD6": "Geçmiş değerlendirmesi raporunu yayınla ve zaman çizelgesine eriş", - "D9IV7i": "Geçmiş değerlendirmesi bu senaryonun oynanması için devre dışı bırakılmış.", - "CjNrqO": "Geçmiş değerlendirmesi raporu kalıbı", - "9kCT7Q": "Ekiplerin parmaklarının ucunda olması için önemli olayları ve iletileri otomatik olarak izleyen bir zaman çizelgesiyle geçmiş değerlendirmesini kolaylaştırın.", - "5Ofkag": "Geçmiş değerlendirmesini etkinleştir", - "3hBelc": "Bir geçmiş değerlendirmesi beklenmiyor.", - "GAuN6w": "Varsayımları kur", - "G/yZLu": "Kaldır", - "FXCLuZ": "toplam {total, number}", - "FGzxgY": "Örnek: Onay zamanı, Çözümlenme zamanı", - "FEGywG": "Lütfen güncelleme anımsatıcısı için gelecekte bir tarih ve saat yazın.", - "F9LrJA": "Ögeleri süz", - "F4pfM/": "Lütfen bir sayı yazın ya da hedefi bış bırakın.", - "EvBQLq": "Senaryo yöneticisi yap", - "Ek1Fx2": "Şu anahtar sözcüklerin geçtiği bir ileti gönderildiğinde", - "EWz2w5": "Senaryoyu oyna", - "EQpfkS": "Tamamlanmış", - "EC5MJD": "Kullanılabilecek bir güncelleme yok.", - "E0LnBo": "Bir seçim yapabilir ya da özel bir süre belirtebilirsiniz (\"2 hafta\", \"3 gün 12 saat\", \"45 dakika\", ...)", - "DtCplA": "{numParticipants, plural, =1 {# katılımcı} other {# katılımcı}}", - "DnBhRg": "Kişiler ekle", - "DaHpK1": "Kanal arama", - "DSVJjB": "Şu anda {playbookTitle} senaryosu oynanıyor", - "DPj6DM": "İş başında görmek istediğiniz oyunu seçin.", - "DCl7Vv": "satır arası kod", - "D55vrs": "Lisansınız üretilemedi", - "CwwzAU": "Kontrol listesi adını yazın", - "D2CE02": "Web bağlantısını yazın", - "CyGaem": "Oyun adı", - "Cy1AK/": "Oyun bilgilerini görüntüle", - "CkYhdY": "Kanalı bir yan çubuk kategorisi olarak ekle", - "CSts8B": "Takım simgesi", - "CBM4vh": "Sonraki güncelleme zamanlayıcısı", - "C9NScU": "Takımınızı kontrol altına alın", - "C6Oghd": "Oyun özetini düzenle", - "C1khRR": "Senaryolara geri dön", - "Brya9X": "Bir oyun özeti kalıbı ekleyin…", - "BQtd5I": "Senaryolara hoş geldiniz!", - "BNB75h": "Bir senaryo, yinelenebilecek herhangi bir işlem için kontrol listelerini, otomasyonları ve kalıpları belirler. {br} Takımların hataları azaltmasına, paydaşlarının güvenini kazanmasına ve her yinelenen işlemde daha etkili olmasına yardımcı olur.", - "BD66u6": "Kanaldaki tüm iletileri CSV dosyası biçiminde indirin", - "B487HA": "Sürüyor", - "B3Q5mz": "Tetikleyici", - "Auj1ap": "Ücretsiz deneme süresini başlatın ya da aboneliğinizi üst tarifeye geçirin.", - "ArpdYl": "Zaman çizelgesi etkinlikleri gerçekleştikçe burada görüntülenir. Bir etkinliği kaldırmak için üzerine gidin.", - "ApULhK": "Üyeler çağır", - "AS5kar": "Katılımcılar ({participants})", - "AML4RW": "Görev atamaları", - "AF7+5o": "Bitiş tarihi ekle", - "9SIW2x": "Her oyundaki hedef değeri", - "8n24G2": "Oyun bilgileri yan panoda görüntülensin", - "5wqhGy": "Oyun bilgilerini aç/kapat", - "4vuNrq": "Oyuna başladıktan {duration} sonra", - "4BN53Q": "Her bir oyunun değerinin hedefe ne kadar yakın veya uzak olduğunu göstereceğiz ve ayrıca bir grafik üzerinde çizeceğiz.", - "A8dbCS": "Senaryo bulunamadı", - "A21Mgv": "Oyun tamamlandı", - "9uOFF3": "Özet", - "9trZXa": "Takımdaki herkes görebilir", - "9tBhzB": "Üst tarifeye geçin", - "9m0I/B": "Paydaşlar bilgilendirilsin", - "9kQNdp": "Bu senaryo özeldir.", - "9j5KzL": "Kategori adını yazın", - "9a9+ww": "Başlık", - "9XUYQt": "İçe aktar", - "9TTfXU": "Sistem yöneticinize bildirim gönderildi.", - "9PXW6Q": "Süre / Başlangıç", - "9Obw6C": "Süz", - "91Hr5f": "Sürükleyerek sıralamayı değiştirin", - "9+Ddtu": "Sonraki", - "8hDbW6": "Bir giden web bağlantısı gönder", - "6GTzTR": "Bu senaryoda herhangi bir zamanda ne olduğuna bakın", - "7P5T3W": "Kontrol listesini geri yükle", - "6uhSSw": "Bir kanal seçin", - "6n0XDG": "Kontrol listesini silmek istediğinize emin misiniz? Tüm görevler silinecek.", - "6jDabx": "Geri bildirim verin", - "6D6ffM": "Lütfen şu biçimde bir süre yazın: gg:ss:dd (12:00:00 gibi) ya da hedefi boş bırakın.", - "6CGo3o": "Durum / Son güncelleme", - "69nlA3": "Lütfen şu biçimde bir süre yazın: gg:ss:dd (12:00:00 gibi).", - "5qBEKB": "Senaryoyu oynamak nedir?", - "5j6GD/": "{numParticipants, plural, =0 {no participants} =1 {# katılımcı} other {# katılımcı}}", - "5ciuDD": "KANALDA DEĞİL", - "5ZIN3u": "Durum güncellemeleri", - "5FRgqE": "Kanal günlüğü indiriliyor", - "5CI3KH": "Destek ekibi ile görüşün", - "5BUxvl": "Bu takımdaki herkes bu senaryoyu görebilir.", - "5AJmOz": "Bir kullanıcı kanala katıldığında", - "5A46pW": "Bir bölü komutu ekleyin", - "4ltHYh": "Senaryoya git", - "4fHiNl": "Kopyala", - "4cwL43": "Arşivlenmişler ile", - "4aupaG": "{title} senaryosu geri yüklendi.", - "4alprY": "Senaryo kalıpları", - "4Hrh5B": "{name}, {summary} olan durumu değiştirdi", - "4GjZsL": "Senaryo sayısı", - "kkw4kS": "Bu güncelleme {hasChannels, select, true {{broadcastChannelCount, plural, =1 {bir kanalda} other {{broadcastChannelCount, number} kanalda}}} other {}}{hasFollowersAndChannels, select, true { ve } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {bir doğrudan iletide} other {{followersChannelCount, number} doğrudan iletide}}} other {}} yayınlanacak.", - "wbdGb5": "Takımın bitiş çizgisine birlikte nasıl ulaşacağını net bir şekilde belirlemek için görevler atayın, tamamlayın ya da atlayın.", - "vNiZXF": "Şu anda süren bir oyun yok. Takımınız ve araçlarınız için iş akışlarını düzenlemeye başlamak için bir senaryoyu oynayın.", - "vJ2SaW": "Senaryonuzun karşılama iletisi gönderme, önemli üyeleri çağırma ve bir güncelleme kanalı oluşturma gibi özelliklerini otomatikleştirin.", - "zINlao": "Sahip", - "zELxbG": "Kaydedilmiş iletiler", - "z3B83t": "Senaryo arama", - "yqpcOa": "Kullan", - "ypIsVG": "Görevi geri yükle", - "yhU1et": "Görevler", - "y7o4Rn": "Silmek istediğinize emin misiniz?", - "xvBDOH": "{title} senaryosunu arşivlemek istediğinize emin misiniz?", - "xmcVZ0": "Arama", - "x5Tz6M": "Rapor", - "x1phlu": "Zaman aralığı belirtilmemiş", - "wylJpv": "Tüm {team} üyeleri bu senaryoyu görebilir.", - "wsUmh9": "Takım", - "wcWpGs": "Web bağlantısı adresleri geçersiz", - "wbwhbH": "Görev adı", - "wbsq7O": "Kullanım", - "waVyVY": "Şu andaki etkin katılımcılar", - "wZ83YL": "Şimdi değil", - "wX3k9U": "Adsız senaryo", - "wL7VAE": "İşlemler", - "wEQDC6": "Düzenle", - "w0muFd": "Giden web bağlantısı gönder (her satıra bir tane)", - "vndQuC": "Bölü komutu yürütüldü", - "vjzpnC": "Bu süzgeçlere uygun bir senaryo bulunamadı.", - "viXE32": "Özel", - "vL4++D": "İlerlemeyi ve sahipliği izleyin", - "v5/Cox": "Kontrol listesini kopyala", - "v1SpKO": "Rol değişiklikleri", - "usa8vQ": "Bir karşılama iletisi gönderin", - "uny3Zy": "Senaryolar", - "uhu5aG": "Herkese açık", - "uT4ebt": "Örnek: Kaynak sayısı, Etkilenen müşteriler", - "u4L4yd": "Kaydedilmemiş değişiklikleriniz var", - "tzMNF3": "Durum", - "tVPYMu": "Senaryo yöneticisi", - "syEQFE": "Yayınla", - "sqNmlF": "Geçmiş değerlendirmesini atla", - "sX5Mn5": "Lütfen her satıra bir web bağlantısı yazın", - "sIX63S": "Sistem yöneticinize bildirim gönderildi", - "sDKojV": "Senaryoyu arşivle", - "rzbYbE": "Hedef", - "ruJGqS": "Senaryo erişimi", - "ru+JCk": "Ortalama değer", - "recCg9": "Güncellemeler", - "rbrahO": "Kapat", - "rX08cW": "Tarih gelecekte olmalı.", - "rDvvQs": "{completed, number} / {total, number} tamamlandı", - "aWpBzj": "Ayrıntıları göster", - "qyJtWy": "Ayrıntıları gizle", - "q6f8x9": "Son değişiklikten bu yana geçen zaman", - "pjt3qA": "Yeni kontrol listesi", - "q0cpUe": "Kontrol listesi ekle", - "q/Qo8l": "Özel senaryolar yalnızca Mattermost Enterprise sürümü ile kullanılabilir", - "osuP6z": "Kontrol listesinin sırasını değiştirmek için ögeleri sürükleyin", - "oVHn4s": "Son güncelleme", - "oS0w4E": "Varsayılan güncelleme zamanlayıcısı", - "oBeKB4": "Bitiş tarihi {date}", - "oAJsne": "Herkese açık senaryo", - "o+ZEL3": "Yayınlanma {timestamp}", - "mm5vL8": "Yalnızca çağrılmış kullanıcılar", - "tbjmvS": "Aynı adlı bir ölçüm zaten var. Lütfen her ölçüm için eşsiz bir ad kullanın.", - "sGJpuF": "Bir açıklama yazın…", - "rMhrJH": "Lütfen ölçümünüzün başlığını yazın.", - "aZGAOI": "Bir durum güncelleme kalıbı ekleyin…", - "OqCzNb": "Bir görev ekleyin", - "JqKASQ": "Kanala @{displayName} ekleyin", - "IxtSML": "Bir kontrol listesi ekleyin", - "IfxUgC": "Bir oyun özeti ekleyin…", - "mw9jVA": "Bir başlık ekleyin", - "mkLeuq": "Güncellemeyi seçilmiş kanallara yayınla", - "mVpO8u": "Bunu daha önce gördünüz mü?", - "mLrh+0": "Bitiş tarihi yok", - "m/Q4ye": "Kontrol listesini yeniden adlandır", - "lyXljU": "Görevi kopyala", - "lrbrjv": "Evet, geçmiş değerlendirmesini başlat", - "lkv547": "Bitiş tarihi (Professional tarifesinde kullanılabilir)", - "lgZf0l": "Senaryolarla başla", - "lbhO3D": "Yatık", - "lZwZi+": "Gün: {date}", - "lQT7iD": "Senaryo oluştur", - "lJ48wN": "Özel senaryo", - "lBqu4h": "Senaryoyu geri yükle", - "l7zMH6": "Bir süre seçin ya da özel bir süre belirtin", - "l0hFoB": "Senaryo açıklaması ekleyin...", - "kvgvNW": "Neler olduğunu bilin", - "kYCbJE": "Zaman aralığı ekle", - "kV5GkX": "Bir durum güncellemesi gönderildiğinde", - "k9q07e": "Güncellemeyi diğer kanallara yayınla", - "k1djnL": "Kontrol listesini sil", - "jwimQJ": "Tamam", - "jvo0vs": "Kaydet", - "jnmORb": "Bu senaryoda", - "jXT2++": "Kanala git", - "jS/UOn": "Kalıbı güncelle", - "jIgqRa": "Sahip / Katılımcı", - "jIIWN+": "hazır biçimde", - "j940pJ": "Bu güncelleme özet sayfasına kaydedilecek.", - "j7jdWG": "Ticari bir sürüme dönüştür.", - "g0mp+I": "Bir senaryoyu özel bir senaryoya dönüştürdüğünüzde üyelik ve oyun geçmişi korunur. Bu değişiklik kalıcıdır ve geri alınamaz. {playbookTitle} senaryosunu özel bir senaryoya dönüştürmek istediğinize emin misiniz?", - "d8KvXJ": "Deneme lisansınızın süresi {expiryDate} tarihinde dolacak. Kullanımınızda bir kesinti olmaması için istediğiniz zaman Müşteri Portali üzerinden bir lisans satın alabilirsiniz.", - "sQu1rA": "{numTotalRuns, plural, =0 {oyun henüz başlatılmamış} =1 {# oyun başlatılmış} other {# oyun başlatılmış}}", - "hO9EdA": "Kanala {numInvitedUsers, plural, =0 {hiç bir üye çağırmayın} =1 {bir üye çağırın} other {# üye çağırın}}", - "giM/X9": " sıklıkla bir durum güncellemesi beklenecek. Yeni güncellemeler {channelCount, plural, =0 {hiç bir kanala} one {# kanala} other {# kanala}} ve {webhookCount, plural, =0 {hiç bir web bağlantısına} one {# web bağlantısına} other {# web bağlantısına}} gönderilecek .", - "kDcpd/": "{numKeywords, plural, other {# anahtar sözcük}}", - "s3jjqi": "{num_actions, plural, =0 {işlem yok} one {# işlem} other {# işlem<}}", - "vaYTD+": "{outstanding, plural, =1 {# tamamlanmamış görev} other {# tamamlanmamış görev}} var. Oyunu bitirmek istediğinize emin misiniz?", - "soePYH": "{num_checklists, plural, =0 {kontrol listesi yok} one {# kontrol listesi} other {# kontrol listesi}}", - "YDuW/T": "{num_runs, plural, =0 {Henüz oynanmamış} one {# oyun} other {# oyun}}", - "QpUBDr": "{members, plural, =0 {No one} =1 {Bir kişi} other {# kişi}} bu senaryoya erişebilir.", - "Q7aZO4": "{numParticipants, plural, =0 {Etkin katılımcı yok} =1 {# katılımcı etkin} other {katılımcı etkin}}", - "cPIKU2": "İzlenenler", - "ijAUQf": "Sistem yöneticinizden aboneliğinizi yüklseltmesini isteyin.", - "avPeEI": "Bu senaryonun oyunlarındaki tüm oyunların eğilimini, etkin oyunları ve katılımcıları görmek için aboneliğinizi üst tarifeye geçirin.", - "ieGrWo": "İzle", - "iXNbPf": "Yeniden adlandır", - "iMjjOH": "Sonraki hafta", - "iDMOiz": "KANAL ÜYELERİ", - "hzt6l8": "Bir kalıp oluşturmak için Markdown kullanın.", - "hw83pa": "Anahtar ölçümler ve ölçüm değerleri izlensin", - "hrgo+E": "Arşivle", - "hfrrC7": "Takım imzası", - "hXIYHG": "Kanal günlüğünü indirmek için Channel Export uygulama ekini kurup etkinleştirin", - "gy/Kkr": "(düzenlendi)", - "guunZt": "Ata", - "gsMPAS": "Dolar", - "gGcNUr": "İzniniz yok", - "g9pEhE": "Bitiş tarihi", - "g5pX+a": "Hakkında", - "fuDLDJ": "Kanal ekle", - "fmbSyg": "Değer ekle (gg:ss:dd)", - "fhMaTZ": "Tura çıkın", - "fXGjhC": "{summary} olan sahip değiştirildi", - "fV6578": "Sahip rolünü ata", - "fUEpLA": "Bu süzgeçlere uygun bir zaman çizelgesi etkinliği bulunamadı.", - "f+bqgK": "Ölçüm adı", - "eiPBw7": "Geçmiş değerlendirmesi anımsatma sıklığı", - "egvJrY": "Atanmış değiştirildi", - "edxtzC": "Senaryo oluştur", - "eLeFE2": "Adı ve açıklamayı düzenle", - "TZYiF/": "üzeri çizili", - "eHAvFf": "koyu", - "e3z3P8": "Yok sayıp ayrıl", - "e/AZL5": "30 günlük ücretsiz deneme süreniz başladı", - "dxyZg3": "Kendim keşfedeceğim", - "dvhvum": "(İsteğe bağlı) Bu senaryonun nasıl kullanılacağını açıklayın", - "dZmYk6": "Senaryo kopyalandı", - "dSC1YD": "Görevi atla", - "d9epHh": "Kanal günlüğünü indir", - "d4g2r8": "Silindi: {timestamp}", - "cyR7Kh": "Geri", - "cp7KUI": "Senaryo", - "cEWBE3": "Her oynamada süreçleri daha iyi kılmak için geçmiş değerlendirmesi yapın.", - "b3TdyZ": "Denemeyi başlat üzerine tıklayarak, Mattermost yazılım değerlendirme sözleşmesi ve Kişisel verilerin gizliliği ilkesi metinleri ile ürün tanıtımı e-postalarını almayı kabul ediyorum.", - "c8hxKk": "{date}. hafta", - "c6LNcW": "Görevi sil", - "c23IHq": "Kanal işlemleri ile bu kanaldaki işlemler otomatikleştirilebilir", - "bTgMQ2": "Bu senaryo arşivlenmiş.", - "bLK+Kr": "Kanala belirli bir sıklıkla geçmiş değerlendirmesinin doldurulmasını anımsatır.", - "aEhjYg": "Taslak", - "Z/hwEf": "Kanala şu zamanda geçmiş değerlendirmesinin yapılması hatırlatılacak:{reminderEnabled, select, true {her} other {}}", - "z3A0LP": "Son kez {relativeTime} önce oynandı", - "xVyHgP": "Deneme oyunu başlat", - "xHNF7i": "Oyun işlemleri", - "x8cvBr": "Oyun özetini görüntüle", - "u4MwUB": "Senaryo oyun geçmişinizi kaydedin", - "twieZh": "Oyun özetine git", - "t6SiGO": "Şu anda oynanan oyunlar", - "ryrP8K": "Bu senaryoyu kimlerin görüntüleme, düzenleme ve oynama izni olduğunu belirtin.", - "p1I/Fx": "Oyununuzu otomatik olarak oluşturduk", - "o2eHmz": "{name} oyunu tamamladı", - "mCrdeS": "Toplam senaryo oyunu", - "lbs7UO": "son 10 oyundaki her oyun", - "lUfDe1": "Senaryo oyunu kanalını dışa aktar ve daha sonra incelemek için kaydet.", - "lJyq2a": "Oyun bulunamadı", - "l5/RKZ": "Bu senaryonun henüz tamamlanmış bir oyunu yok.", - "iNU1lj": "İstediğiniz oyun özel ya da bulunamadı.", - "gt6BhE": "Oyun bilgileri", - "g4IF1x": "Bu senaryo henüz oynanmamış.", - "efeNi1": "10 oyunluk ortalama değer", - "djALPR": "{activeRuns, number} {activeRuns, plural, one {oyun} other {oyun}} sürüyor", - "dCtjdj": "Senaryoyu oynamaya hazır mısınız?", - "bPLen5": "Son 30 günde tamamlanmış oyunlar", - "Y4MU/9": "İş başında görmek için Deneme oyunu başlat seçin.", - "hVFgh4": "Tamamlanmışlar katılsın", - "bE1Cro": "Yalnızca benimkiler", - "bGhCLX": "Bir güncelleme gönderildiğinde", - "b5FaCc": "Kanalı yan çubuk kategorisine ekle", - "b40Pr7": "Bildiren", - "b/QBNs": "Güncelleme tarihi", - "awG90C": "Her oyundaki hedef", - "aYIUar": "Teşekkürler!", - "aACJNp": "{name} oyunu başlattı", - "a0hBZ0": "Ölçümü sil", - "ZkhArX": "Başlayalım!", - "ZdWYcm": "Hayır, geçmiş değerlendirmesini atla", - "ZWtlyd": "{name} oyunu geri yükledi", - "ZNNjWw": "Lütfen bir sayı yazın.", - "ZAJviT": "Sistem yöneticisine bildirim gönderilemedi.", - "Z7vWDQ": "Bir sorun çıktı", - "Z3ybv/": "Kullanıcı için kanalı bir yan çubuk kategorisine ekle", - "YQOmSf": "Her satıra bir web bağlantısı yazın", - "YORRGQ": "Güncelleme gönder", - "YMrTRm": "Oyun özeti", - "YKn+7s": "Bu kanalda oynanan bir senaryo yok.", - "XpDetT": "Bu ipuçlarını gösterme.", - "XmUdvV": "Gerek duyduğunuz tüm istatistikler", - "Xgxruo": "Kontrol listesini atla", - "XXbWAU": "Bu senaryo oynanırken güncellemeleri otomatik olarak almak için bu seçeneği işaretleyin.", - "XRyRzf": "Durum güncellemeleri beklenmiyor.", - "XF8rrh": "Bağlantıyı ''{name}'' üzerine kopyala", - "X2K92H": "Kontrol listesi adı", - "X/koAN": "Kayıt geçersiz: En fazla 64 web bağlantısı kullanılabilir", - "WTQpnI": "Senaryoları kullanmaya başlayın", - "WIxhrv": "Oyun adı en az iki karakter uzunluğunda olmalıdır", - "WAHCT2": "Sistem yöneticisi bilgilendirilsin", - "W1Qs5O": "Oyunlar", - "W0aij2": "Atama...", - "W/V6+Y": "Daralt", - "Vf/QlZ": "Değer aralığı", - "UlJJ1i": "Bölü komutu ekle", - "Ui6GK/": "Kanala yeni bir üye katıldığında", - "UbTsGY": "{start} ile {end} arasında başlatılmış oyunlar", - "UMoxP9": "Kanal adı kalıbı (isteğe bağlı)", - "TxCTXQ": "Oyunu bitirmek istediğinize emin misiniz?", - "TdTXXf": "Ayrıntılı bilgi alın", - "S0kWcH": "Güncelleme bitiş tarihi geçmiş", - "MbapTE": "{num} {num, plural, =1 {görevin} other {görevin}} bitiş tarihi geçmiş", - "TTIQ6E": "Görevlere bitiş tarihleri atayın. Böylece sorumlular önceliklerini belirleyerek işleri bitirebilir.", - "TJo5E6": "Ön izleme", - "TBez4r": "Görüntülenebilecek bir senaryo yok. Bu çalışma alanında senaryo oluşturma izniniz yok.", - "T5rX+W": "Hangi sıklıkla bir güncelleme gönderilsin?", - "Sx3lHL": "Tamsayı", - "SmAUf9": "{timestamp} zamanında bir anımsatıcı gönderilecek", - "SXJ98n": "Geçmiş değerlendirmesi raporunu yayınladıktan sonra düzenleyemezsiniz. Raporu yayınlamak istediğinize emin misiniz?", - "RzEVnf": "Senaryolar önemli iş akışlarını daha yinelenebilir ve kaydı tutulabilir kılar. Bir senaryo defalarca oynanabilir ve her oyunun kendi kaydı ve geçmiş değerlendirmesi vardır.", - "RthEJt": "Geçmiş değerlendirmesi", - "RrCui3": "Özet", - "RoGxij": "{date} tarihindeki etkin oyunlar", - "R5Zh+l": "Bunu kullanarak, kendinizinkini oluşturmak için zaman ayırmadan önce örnek bir senaryoyu deneyimleyebilirsiniz.", - "R/2lqw": "Bir kalıp seçin", - "R+JQaJ": "Kanal üyeleri", - "QywYDe": "Ayrıca oyun da tamamlanmış olarak işaretlensin", - "Qrl6bQ": "Senaryolar ile süreçlerinizi kolaylaştırın", - "QnZAit": "İsteğe bağlı bir açıklama yazın", - "QbGfqo": "Farklı yerlerdeki paydaşlara duyurun ve yalnızca bir gönderiyle geçmiş değerlendirmesinin kaydını tutun.", - "QaZNp9": "Oyunu tamamla", - "QUwMsX": "Geçmiş değerlendirmesini doldurma anımsatıcısı", - "Q8Qw5B": "Açıklama", - "Q7hMnp": "Senaryoyu oyna", - "OyZnsJ": "her oyunda", - "Oo5sdB": "Senaryo adı", - "OcpRSQ": "Kaydı sil", - "ObmjTB": "Bölü komutu", - "Ob5cSv": "Bu sayfadan ayrılırsanız değişiklikler kaydedilmeyecek. Değişiklikleri yok sayarak sayfadan ayrılmak istediğinize emin misiniz?", - "OINwWS": "{isPublic, select, true {Herkese açık} other {Kişisel}} bir kanal oluştur", - "Nh91Us": "{from, number}–{to, number} / {total, number}", - "NYTGIb": "Anladım", - "NMxVd+": "Lütfen ölçüm değerini yazın.", - "NLeFGn": "kime", - "NFyWnZ": "Daha etkin çalışın", - "NA7Cw1": "Bağlantıyı senaryoya kopyala", - "N2IrpM": "Onayla", - "N1U/QR": "Görev durumu değişiklikleri", - "MyIJbr": "İçerikler", - "MvEydR": "{name} bir durum güncellemesi gönderdi", - "MtrTNy": "Yarın", - "MrJPOh": "Durum güncellemeleri kullanılsın", - "Mm1Gse": "Üye arama", - "MTzF3S": "{title} senaryosunu geri yüklemek istediğinize emin misiniz?", - "MJ89uW": "Özel senaryoya dönüştür", - "MHzP9I": "Kanala katılan kullanıcılara görüntülenecek hoş geldiniz iletisini yazın.", - "MFpAtm": "{numTasks, number} {numTasks, plural, one {görev} other {görev}}", - "MBNMo9": "Kanal işlemleri", - "M4gAc9": "Değer ekle", - "M/2yY/": "Henüz hiç kimse.", - "Lo10yH": "Kanal bilinmiyor", - "LmhSmU": "Kayıt silme işlemini onayla", - "Lg3I1b": "@{targetUsername}, lütfen bir durum güncellemesi belirtin.", - "LcC/pi": "Bir hoş geldiniz iletisi gönderin…", - "LRFvqz": "{oneChannel, plural, one {Kanal} other {Kanallar}} içinde duyur", - "LI7YlB": "Bu ölçümün ne hakkında olduğu ve nasıl doldurulması gerektiğiyle ilgili bilgileri yazın. Bu açıklama, bu ölçüm değerlerinin yazılacağı her oyun için geçmiş değerlendirmesi sayfasında bulunacaktır.", - "LDYFkN": "Süre (gg:ss:dd)", - "L6k6aT": "…ya da bir kalıp ile başla", - "KiXNvz": "Oyna", - "KXVV4+": "Senaryo ön izleme sayfasına hoş geldiniz!", - "KUr+sG": "Oyun özetini güncelle", - "KJu1sq": "Kontrol listesini sil", - "K4O03z": "Görev ekle", - "K3r6DQ": "Sil", - "JrZ2th": "Ölçüm ekle", - "JcefuP": "Bir açıklama ekleyin (isteğe bağlı)", - "Ja1sVR": "Bu senaryo oyunu için durum güncellemeleri devre dışı bırakılmış.", - "JXdbo8": "Tamam", - "JJNc3c": "Önceki", - "JCGvY/": "Bu kalıp, tutmak için her oyun boyunca gerçekleşen yinelenen güncellemelerin biçiminin standartlaştırılmasını sağlar.", - "IuFETn": "Süre", - "Ietscn": "Görevler tamamlandı", - "IOnm/Z": "Kullanılabilecek herhangi bir oyun özeti yok.", - "ICqy9/": "Kontrol listeleri", - "I90sbW": "şimdi", - "I7+d55": "Tarih/saat belirtin (“4 saat içinde”, “1 Mayıs”...)", - "I5NMJ8": "Diğer", - "I5DYM+": "Öğren VE yansıt", - "Hzwzgs": "Güncellemeleri {oneChannel, plural, one {kanal} other {kanal}} içinde yayınla", - "HvAcYh": "{text}{rest, plural, =0 {} one { ve diğer} other { ve {rest} diğer}}", - "HhLp57": "alıntı", - "HXvk56": "Gönderi durum güncellemeleri", - "HSi3uv": "Atanmış kimse yok", - "HLn43R": "Erişimi yönetin", - "HGdWwZ": "Görevler oluşturup atayın", - "HAlOn1": "Ad", - "GxJAK1": "İstediğiniz senaryo özel ya da bulunamadı.", - "GwtR3W": "Var olan bir görevi sürükleyip bırakın ya da tıklayarak yeni bir görev oluşturun.", - "GjCS6U": "Bir kalıp seçin", - "GRTyvN": "Senaryo listesini aç/kapat", - "GG1yhI": "Kullanım örnekleri ve işlemler için birçok kalıp vardır. Bir senaryoyu olduğu gibi kullanabilir ya da değiştirdikten sonra takımınızla paylaşabilirsiniz.", - "udrLSP": "Çalışma modellerini ve ilerlemeyi anlayarak başarımı izlemek için ölçümleri kullanın.", - "q/VD+s": "Paydaşların gelişmeleri her zaman izleyebilmesi için zamanlayıcılar ayarlayın ve durum güncellemeleri için bir kalıp oluşturun.", - "pK6+CW": "@{displayName}, [{runName}]({overviewUrl}) kanalının üyesi değil. Bu kanala eklemek ister misiniz? Tüm ileti geçmişini görebilecek.", - "mvZUm3": "Senaryo bileşenlerinizi ayrıntılı olarak buradan keşfedebilirsiniz. Senaryonuzu süreçlerinize ve modellerinize uyacak şekilde özelleştirmek için Düzenle komutunu seçin.", - "mbo96h": "Geçmiş değerlendirmesi raporunda doldurulacak özel ölçümleri yapılandırın", - "lxfpbh": "Sahipten {reminderEnabled, select, true {şu sıklıkla durum güncellemesi yapması istenecek} other {bir durum güncellemesi yapması istenmeyecek}}", - "kXFojL": "Ayrıca, gerek duyduğunuzda hazır olması için önceden bir senaryo oluşturabilirsiniz.", - "v1DNMW": "{name} geçmiş değerlendirmesini yayınladı", - "scYyVv": "Geçmiş değerlendirmesi raporunu doldurmak ister misiniz?", - "sVlNlY": "Her takımın yapısı farklıdır. Takımdaki hangi kullanıcıların senaryolar oluşturabileceğini belirleyebilirsiniz.", - "pKLw8O": "Bu etkinliği silmek istediğinize emin misiniz? Silinen etkinlikler zaman çizelgesinden kalıcı olarak kaldırılır.", - "nqVby7": "{numTasksChecked, number} / {numTasks, number} {numTasks, plural, =1 {görev} other {görev}} kontrol edildi", - "nkCCM2": "Başka bir anımsatma almayacaksınız.", - "zz6ObK": "Geri yükle", - "zx0myy": "Katılımcılar", - "zl6378": "Geçmiş değerlendirmesi içinde ölçümleri yapılandır", - "zWkvNO": "Zaman çizelgesi", - "zWgbGg": "Bugün", - "VmnoW8": "Lütfen ayrıntılı bilgi almak için sistem günlüklerine bakın.", - "Vhnd2J": "Açıklamayı aç/kapat", - "VZRWFk": "Örnek: Maliyet, Satın alma", - "VOzlSL": "Bir senaryoyu oynamak, takımlarınız ve araçlarınız için iş akışlarını düzenler.", - "V5TY0z": "Katılımcılar eklensin mi?", - "TxmjKI": "Bu ölçümün ne hakkında olduğunu açıklayın", - "TSSNg/": "Son 12 haftada her hafta başlatılmış TOPLAM OYUN", - "Tt04f1": "Görüşmeden ayrılmadan kimlerin işin içinde olduğunu ve ne yapılacağını görün.", - "JJMNME": "{withRunName, select, true {@{authorUsername}, [{runName}]({overviewURL} için bir güncelleme gönderdi)} other {@{authorUsername} bir güncelleme gönderdi}}", - "OK8u0r": "Kontrol listeleri, işlemler, kalıplar ve geçmiş değerlendirmeleri gibi özelliklerle ekiplerinizin ve araçlarınızın izlemesi gereken iş akışını belirlemek için bir senaryo oluşturun.", - "SVwJTM": "Dışa aktar", - "SENRqu": "Yardım", - "SDSqfA": "Bir oyun başlatıldığında", - "RUlvbf": "Yeni senaryonuzu deneyin!", - "RQl8IW": "Bir süre sustur…", - "RO+BaS": "Oynanacak bağlantıyı kopyala", - "QiKcO7": "Geçmiş değerlendirmesi kalıbını yazın", - "Q5hysF": "Senaryolar ile daha fazlasını yapın", - "Q3R9Uj": "Tüm sürecin belge adımlarını burada görebilirsiniz. Her görevi ilgili kişilere atayın ve isteğe bağlı olarak zaman çizelgeleri ya da ilişkili işlemler ekleyin.", - "Ppx673": "Raporlar", - "OuZhcQ": "Süreyi belirtin (\"8 saat\", \"3 gün\"...)", - "OsDomv": "Tüm etkinlikler", - "OKhRC6": "Paylaş", - "NiAH1z": "Hedef değer", - "NJ9uPu": "Anahtar ölçümler", - "yllba1": "Arşivlenmiş bu senaryo yeniden adlandırılamaz.", - "xEQYo5": "Geçmiş değerlendirmesi raporunda doldurulacak özel ölçümleri yapılandırın.", - "TD8WrM": "Kopyalama bu takım için devre dışı bırakıldı.", - "OQplDX": " sıklıkla bir durum güncellemesi beklenecek. Yeni güncellemeler {channelCount, plural, =0 {hiç bir kanala} one {# kanala} other {# kanala}} ve {webhookCount, plural, =0 {hiç bir web bağlantısına} one {# web bağlantısına} other {# web bağlantısına}} gönderilecek.", - "oL7YsP": "Son düzenlenme {timestamp}", - "Z2Hfu4": "Bir oyun özeti ekle", - "vSMfYU": "Oyun bilgileri", - "opn6uf": "Zaman çizelgesini görüntüle", - "o6N9pU": "Rolü oyuna", - "lbr3Lq": "Bağlantıyı kopyala", - "iigkp8": "Toplanma zamanı geldi mi?", - "hjteuA": "Erişebileceğiniz tüm senaryolar burada görüntülenir", - "bf5rs0": "Bilgileri görüntüle", - "ZJS10z": "Henüz bir güncelleme gönderilmemiş", - "Q15rLN": "Güncelleme isteğinde bulun...", - "GDCpPr": "Son durum güncellemesi", - "+qDKgW": "Tüm güncellemeleri görüntüle", - "kEMvwX": "Bu süzgeçlere uygun bir rol bulunamadı.", - "GXjP8g": "Erişebileceğiniz tüm roller burada görüntülenir", - "ocYb9S": "Anahtar ölçümler", - "nc8QpJ": "Son işlemler", - "m/KtHt": "Sahibi değiştirme izniniz yok", - "RnOiCg": "Oyun {isFollowing, select, true {izlemesi bırakılamıyor} other {izlenemiyor}}", - "4mCpAv": "Sahip değiştirilemez", - "lr1CUA": "Senaryolara göz at", - "Ul0aFX": "İçe senaryo aktar", - "LfhTNW": "Senaryo ve oyunlara göz atın ya da oluşturun", - "GVpA4Q": "Yeni senaryo oluştur", - "CFysvS": "Oyun açılan kutusu oluştur", - "/qDObA": "Oyunlara göz at", - "/+8SGX": "{filteredNum} / {totalNum} etkinlik görüntüleniyor", - "Jli9m7": "Oyun kanalına, bir güncelleme göndermelerini isteyen bir ileti gönderilecek.", - "9xs0pp": "Değer ekle...", - "jboo9u": "Güncelleme iste", - "Xx0WZV": "İleti gönder", - "UePrSL": "{num} {num, plural, one {katılımcı} other {katılımcı}}", - "UMFnWV": "Geçmiş değerlendirmesini görüntüle", - "P9PKvb": "Oyun kanalına bir ileti gönderildi.", - "NGqzDU": "Güncelleme isteğini onayla", - "JvEwg/": "Bir güncelleme istenemedi", - "RCT0Px": "{displayName} katılımcısını kanala ekle", - "KeO51o": "Kanal", - "VpQKQE": "{displayName} oyunun bir katılımcısı değil. Katılımcı yapmak ister misiniz? Oyun kanalındaki tüm ileti geçmişine erişebilecek.", - "zW/5AB": "Professional tarife özelliği Bu ücretli bir özelliktir ve 30 günlük ücretsiz deneme süresi boyunca kullanabilirsiniz", - "vDvWJ6": "Ücretsiz deneme ile güncelleme isteğini deneyin", - "pzTOmv": "Takipçiler", - "pFK6bJ": "Tümünü görüntüle", - "lKeJ+i": "Herhangi bir özet yok", - "hIWK05": "Oyun kanalına sizi katılımcı olarak eklemelerini isteyen bir ileti gönderilecek.", - "ch4Vs1": "Senaryo oyunlarındaki güncellemeleri tek bir tıklamayla isteyin ve bir güncelleme yayınlandığında doğrudan bilgilendirme alın. Denemek için 30 günlük ücretsiz deneme sürümünü başlatın.", - "U8u4uF": "Katkıda bulun", - "PdRg+3": "Tümünü görüntüle...", - "P6NEL/": "Komut...", - "MD6oav": "Katılma isteği yapılamadı", - "J2NmIY": "Katılmak istediğinizi onaylayın", - "3O8M5M": "İstek oyun kanalına gönderildi.", - "1fXVVz": "Bitiş tarihi...", - "1GOpgL": "Atanan...", - "u6Fyic": "İsteğiniz oyun kanalına gönderildi.", - "pXWclp": "Katılım isteğiniz oyun kanalına gönderilecek.", - "Nf9oAA": "Bu oyuna katılmak üzeresiniz.", - "5PpBsd": "İsteğiniz yapılamadı.", - "4Iqlfe": "Bu oyuna katıldınız.", - "mNgqXf": "Bu özelliği kullanabilmek için:", - "j2VYGA": "Tüm senaryoları görüntüle", - "SMrXWc": "Sık kullanılanlar", - "PWmZrW": "Tüm oyunları görüntüle", - "PW+sL4": "Kullanılamaz", - "KzHQCQ": "Bu süzgeçlere uygun tamamlanmış bir oyun yok.", - "CUhlqp": "eğitim turu ipucu ürün görseli", - "5HXkY/": "Tür: {typeTitle}", - "3zF589": "Tüm {filterName} sıfırla", - "wGp7l3": "{icon} Dolar", - "s+rSpl": "{icon} Tamsayı", - "qp5G0Z": "Geçmiş değerlendirmesi özellikleri için üst tarifeye geçilmesi gerekir.", - "ojQue/": "{icon} Süre (gg:ss:dd)", - "xfnuXm": "Katılın", - "wRM2AO": "Güncelleme isteği yapılamadı.", - "ePhhuK": "İsteğiniz oyun kanalına gönderildi.", - "b+DwLA": "Bu oyuna katılma isteği gönderin.", - "PoX2HN": "İsteği gönder", - "CV1ddt": "Oyuna katılın", - "Gwmqz5": "Bir güncelleme isteyin", - "OfN7IN": "Oyun kanalına bir durum güncellemesi isteği yapılacak.", - "B9z0uZ": "Oyuna katılma isteğiniz yapılamadı.", - "AH+V3r": "Oyuna katılın.", - "+6DCr9": "Bir katılımcı olarak durum güncellemeleri gönderebilir, görevler atayıp tamamlayabilir ve geriye dönük incelemeler gerçekleştirebilirsiniz.", - "wBZz47": "Oyundan ayrıldınız.", - "gfUBRi": "Oyundan ayrılmadan önce yeni bir sahip belirleyin.", - "fnihsY": "Ayrıl", - "a1vQ5Q": "Ayrılmayı onayla", - "SK5APX": "Oyundan ayrılınamadı.", - "N9CTUJ": "Oyundan ayrıl", - "F/HKIy": "Oyundan ayrılmak istediğinize emin misiniz?", - "Mjq//Y": "Sık kullanılanlardan kaldır", - "5Hzwqs": "Sık kullanılanlara ekle", - "XS4umx": "{name} bir durum güncellemesini susturdu", - "mttASm": "Oyundan ayrıl ve izlemeyi bırak", - "lpWBJE": "Ayrılıp izlemeyi bırakmayı onayla", - "hnYSP3": "Bir oyundan ayrılıp izlemeyi bıraktığınızda, sol yan çubuğunuzdan kaldırılır. Tüm oyunları görüntüleyerek yeniden erişebilirsiniz.", - "AhY0vJ": "Ayrıl ve izlemeyi bırak", - "egUE/K": "Seçilmiş kanallarda yayınlansın", - "Xm0L7N": "Bir durum güncellemesi gönderildiğinde ya da bir geçmiş değerlendirmesi yayınlandığında", - "iEtImk": "Bir oyundan ayrılıp{isFollowing, select, true { izlemeyi bıraktığınızda} other { }}, oyun sağ yan çubuktan kaldırılır. Oyuna tüm oyunları görüntüleyerek yeniden erişebilirsiniz.", - "cnfVhV": "Oyundan ayrıl{isFollowing, select, true { ve izlemeyi bırak } other {}}", - "Suyx6A": "Senaryo içe aktarılamadı. Lütfen JSOn dosyasının geçerli olduğunu denetleyip yeniden deneyin.", - "Q4sutg": "Ayrılmayı onayla{isFollowing, select, true { ve izlemeyi bırak} other {}}", - "QegBKq": "Senaryoya katıl", - "FgydNe": "Görüntüle", - "P6PLpi": "Katıl", - "qGlwfc": "Oyunu başlat", - "j2FnDV": "Bu adı taşıyan bir kanal eklenecek", - "iQhFxR": "Son kullanılma", - "03oqA2": "Süren oyunlar", - "KjNfA8": "Süre geçersiz", - "vqmRBs": "Oyunu yeniden başlatmayı onayla", - "k5EChD": "Oyunu yeniden başlatmak istediğinize emin misiniz?", - "Zg0obP": "Oyunu yeniden başlat", - "izWS4J": "İzlemeyi bırak", - "XnICdK": "Oyuna katılınamadı", - "unwVil": "Kanala katılma isteği gönderilemedi.", - "ZRv7Dm": "Katılma isteği", - "M9tXoZ": "Oyun kanalına bir katılma isteği gönderilecek.", - "0QD99o": "Kanala katılma isteğinde bulun", - "q48ca7": "Senaryolar hakkında geri bildirimde bulunun.", - "fVMECF": "Katılımcı", - "bCmvTY": "Geri bildirimde bulunun", - "FLG4Iu": "Oyun sahibi olarak ata", - "6rygzu": "Oyundan çıkar", - "0Azlrb": "Yönetim", - "/GCoTA": "Temizle", - "wCDmf3": "Güncellemeleri etkinleştir", - "w4Nhhb": "Katılımcı ekle", - "utHl3F": "{runName} oyununa kişiler ekleyin", - "qDxsQH": "Bu oyun ile etkileşime geçmek için bir katılımcı olun", - "nsd54s": "Durum güncellemelerini devre dışı bırakmayı onayla", - "lqzBNa": "Kişileri oyun kanalından çıkar", - "l/W5n7": "Katılımcılar ayrıca bu oyuna bağlı kanala da eklenir", - "jrOlPO": "Oyun durum güncellemesi bildirimlerini alın", - "jAo8dd": "{name} tarafından devre dışı bırakılmış durum güncellemelerini oynat", - "ieL3dC": "Kanal işlemlerini ayarla", - "ha1TB3": "Bir katılımcı oyuna katıldığında", - "cpGAhx": "Bu oyun için durum güncellemelerini devre dışı bırakmak istediğinize emin misiniz?", - "cUCiWw": "Katılımcı olun", - "b8Gps8": "{name} tarafından etkinleştirilmiş durum güncellemelerini oynat", - "Z18I+c": "Kanal işlemleri ile bu kanaldaki işlemleri otomatikleştirebilirsiniz", - "Y1EoT/": "Bir katılımcı oyundan ayrıldığında", - "WFA0Cg": "Bu oyun için durum güncellemelerini etkinleştirmek istediğinize emin misiniz?", - "WC+NOj": "Kişiler bu oyuna bağlı kanala da eklensin", - "H7IzRB": "Durum güncellemelerini devre dışı bırak", - "9qqGGd": "Katılımcılar çağır", - "5b1zuB": "Bu kişileri oyun kanalına ekle", - "1prgB2": "Kişi arama", - "1OluNs": "Durum güncellemelerini etkinleştirmeyi onaylayın", - "1OVPiC": "Oyuna katılın. Katılımcı olarak durum güncellemeleri gönderebilir, görevler atayıp tamamlayabilir ve geriye dönük incelemeler yapabilirsiniz.", - "//o1Nu": "Güncellemeleri devre dışı bırak", - "zSOvI0": "Süzgeçler", - "u/yGzS": "{name}, @{user} kullanıcısını oyuna ekledi", - "t6lwwM": "{requester}, {users} kullanıcılarını oyundan çıkardı", - "feNxoJ": "{requester}, {users} kullanıcılarını oyuna ekledi", - "qxYWTy": "Kendi oyunlarımdaki tüm görevleri görüntüle", - "jfpnye": "@{user} oyundan ayrıldı", - "grv9Fm": "Bir görev listesini açıp kapatmak için seçin.", - "ecS/qx": "{name}, {num} katılımcıyı oyuna ekledi", - "YBvwXR": "Henüz atanmış bir görev yok", - "WFd88+": "İşaretlenmiş görevleri görüntüle", - "VM75su": "{name}, {num} katılımcıyı oyundan çıkardı", - "TnUG7m": "Atanmış herhangi bir bekleyen göreviniz yok.", - "SwlL5j": "@{user} oyuna katıldı", - "SRqpbI": "{assignedNum, plural, =0 {Atanmış bir görev yok} other {görev atanmış}}", - "RXjd3Q": "{name}, @{user} kullanıcısını oyundan çıkardı", - "I0NIMp": "Görevleriniz", - "DUU48k": "Size açıkça atanmış bir görev yok. Süzgeçleri kullanarak aramanızı genişletebilirsiniz.", - "CgAtTJ": "{overdueNum, plural, =0 {} other {süresi geçmiş}}", - "meD+1Q": "OYUN KATILIMCILARI", - "Gg/nch": "KATILMIYOR", - "iH5e4J": "Bu oyun ile ilişkili kanala da ekleneceksiniz.", - "UAS7Bn": "Bu oyun ile ilişkili kanala erişme isteğinde bulun", - "NGKqOC": "Beni bu oyun ile ilişkili kanala da ekle", - "L6vn9U": "Oyun katılımcıları", - "BJNrYQ": "Bir katılımcı olarak, oyun özetini güncelleyebilir, görevleri tamamlayabilir, durum güncellemeleri ekleyebilir ve geçmiş değerlendirmesini düzenleyebilirsiniz.", - "36NwLv": "Oyun katılımcıları listesi yönetimi", - "fBG/Ge": "Maliyet", - "VjJYEV": "Satış etkisi, Satın almalar gibi", - "9X3jwi": "{icon} Maliyet", - "dK2JKl": "Var olan bir kanal ile ilişkilendir", - "IdTL+v": "Oyun kanalı ekle", - "2BCWLD": "Kanal yapılandırması", - "lqceIp": "ya da bir senaryoyu içe aktarın", - "zxj2Gh": "Son Güncelleme: {time}", - "yP3Ud4": "Bu kanal ile ilişkilendirilmiş süren herhangi bir oyun yok", - "tqAmbk": "Süren oyunlar", - "a2r7Vb": "Özel kanal", - "VA1Q/S": "Herkese açık kanal", - "RgQwWr": "Oyunları şuna göre sırala", - "Z1sgPO": "Tamamlanmış oyunları görüntüle", - "ORJ0Hb": "{outstanding, plural, =1 {görev tamamlanmamış} other {görev tamamlanmamış}}. Tüm katılımcılar için oyunu tamamlamak istediğinize emin misiniz?", - "AoNLta": "Bu kanal ile ilişkilendirilmiş tamamlanmış bir oyun yok", - "0boT49": "Oyunu tüm katılımcılar için tamamlamak istediğinizden emin misiniz?", - "RC6rA2": "Son eklenenler", - "Q/t0//": "Tamamlanmış oyunlar", - "NNksk4": "Alfabetik olarak", - "AG7PKJ": "Oyunu yeniden adlandır", - "2NDgJq": "Son durum güncellemesi", - "kQAf2d": "Seç", - "gS1i4/": "Görevi tamamlanmış olarak işaretle", - "gGtlrk": "Senaryolarınız", - "fvNMLo": "Görev işlemleri", - "cGCoJe": "Gönderen", - "W1EKh5": "Yeni senaryo ekle", - "SRbTcY": "Diğer senaryolar", - "L1tFef": "Lütfen yazımı denetleyin ya da başka bir arama yapmayı deneyin", - "KQunC7": "Bu kanalda kullanılan", - "HfjhwE": "Senaryo ara", - "GZoWl1": "Bu görev için işlemleri otomatikleştir", - "EVSn9A": "Bir oyuna başla", - "9AQ5FE": "Oyun özeti", - "95v+5O": "{actions, plural, =0 {Task Actions} one {işlem} other {işlem}}", - "7KMbBa": "Hiç kullanılmamış", - "3sXVwy": "Görev işlemleri...", - "3Yvt4d": "Senaryolar, takımların belirli ve öngörülebilir sonuçlara ulaşması için yinelenebilir bir süreç tanımlayan yapılandırılabilir kontrol listeleridir", - "0CeyUV": "\"{searchTerm}\" için bir sonuç bulunamadı", - "zscc/+": "{outstanding, plural, =1 {# gecikmiş görev} other {# gecikmiş görev}} var. Tüm katılımcılar için {runName} oyununu tamamlamak istediğinize emin misiniz?", - "uCS6py": "Bu senaryoyu görüntüleme izniniz yok", - "prs4kX": "Belirli anahtar sözcükler içeren bir ileti gönderildiğinde", - "mILd++": "Oyun adı {maxLength} karakterden uzun olmamalıdır", - "m8hzTK": "Son kullanılma: {time}", - "l3QwVw": "Kanal seçin", - "ksG35Q": "Bu çalışma alanına senaryo ekleme izniniz yok.", - "k7Nzfi": "Çağrıları devre dışı bırak", - "fwW0T1": "Önceden atanmış üyelerin kaldırılmasını onayla", - "ZSa3cf": "@{targetUsername}, lütfen [{runName}]({playbookURL}) için bir durum güncellemesi bildirin.", - "bEoDyV": "@{authorUsername}, [{runName}]({overviewURL}) için bir durum güncellemesi bildirdi", - "YKLHXL": "Süren oyunları görüntüle", - "Wy3sw+": "{count, plural, =1{1 oyun sürüyor} =0 {Süren bir oyun yok} other {# oyun sürüyor}}", - "TP/O/b": "Kullanıcıyı kaldır", - "QvEO6m": "Bu oyunu düzenleme izniniz yok", - "QJTSaI": "Oyunu başka bir kanal ile ilişkilendirme", - "LKu0ex": "Tüm katılımcılar için {runName} oyununu tamamlamak istediğinize emin misiniz?", - "IE2BzH": "Önceden bir ya da birkaç göreve atanmış kullanıcılar var. Çağrıları devre dışı bırakmak , önceki tüm atamaları kaldırır.{br}{br}Çağrıları devre dışı bırakmak istediğinize emin misiniz?", - "DQn9Uj": "{name} kullanıcısı önceden bir ya da birkaç göreve atanmış. Bu kullanıcıyı otomatik olarak çağırmamak, önceki atamaları kaldırır.{br}{br}Bu kullanıcının oyuna üye olarak çağrılmasını istemediğinizden emin misiniz?", - "Bgt0C8": "{runName} oyununun bu güncellemesi {hasChannels, select, true {{broadcastChannelCount, plural, =1 {bir kanalda} other {{broadcastChannelCount, number} kanalda}}} other {}}{hasFollowersAndChannels, select, true { ve } other {}}{hasFollowers, select, true {{followersChannelCount, plural, =1 {bir doğrudan iletide} other {{followersChannelCount, number} doğrudan iletide}}} other {}} yayınlanacak.", - "BiQjuS": "Oyun {channel} kanalına taşındı", - "9w0mDI": "Önceden atanmış üyenin kaldırılmasını onayla", - "uYrkxy": "Dosya geçerli bir JSON senaryo kalıbı olmalıdır.", - "m4vqJl": "Dosyalar", - "Zbk+OU": "Dosya boyutu 5MB sınırından büyük.", - "MieztS": "İçe aktarmak istediğiniz senaryo dosyasını sürükleyip buraya bırakın.", - "HGSVzc": "Aynı anda birden fazla dosya içe aktarılamaz.", - "LaseGE": "Bu kontrol listesini düzenleme izniniz yok", - "Edy3wX": "Kontrol listesi {channel} kanalına taşındı", - "8//+Yb": "Kontrol listesini başka bir kanala bağla", - "706Soh": "görevler tamamlandı", - "XHJUSG": "Oyunları otomatik izle", - "DqTQOp": "Bir kez", - "vjb+hS": "{user}, \"{name}\" kontrol listesi ögesini geri yükledi", - "OqWwvQ": "{user}, \"{name}\" kontrol listesi ögesinin tamamlandı işaretini kaldırdı", - "8FzC0B": "{user}, \"{name}\" kontrol listesi ögesini tamamlandı olarak işaretledi", - "DKiv0o": "{user}, \"{name}\" kontrol listesi ögesini atladı", - "9M92On": "Kanalları seçin", - "3qPQMX": "{name} bir durum güncellemesi istedi" -} diff --git a/webapp/playbooks/i18n/zh_Hans.json b/webapp/playbooks/i18n/zh_Hans.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/webapp/playbooks/i18n/zh_Hans.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/webapp/playbooks/i18n/zh_Hant.json b/webapp/playbooks/i18n/zh_Hant.json deleted file mode 100644 index 0967ef424bc..00000000000 --- a/webapp/playbooks/i18n/zh_Hant.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/webapp/playbooks/jest.config.js b/webapp/playbooks/jest.config.js deleted file mode 100644 index f33b9b00c3a..00000000000 --- a/webapp/playbooks/jest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/** @type {import('jest').Config} */ - -const config = { - moduleDirectories: ['src', 'node_modules'], - moduleNameMapper: { - '^@mattermost/(components)$': '/../platform/$1/src', - '^@mattermost/(client)$': '/../platform/$1/src', - '^@mattermost/(types)/(.*)$': '/../platform/$1/src/$2', - '^mattermost-redux/(.*)$': '/../channels/src/packages/mattermost-redux/src/$1', - '^src/(.*)$': '/src/$1', - }, - testEnvironment: 'jsdom', -}; - -module.exports = config; \ No newline at end of file diff --git a/webapp/playbooks/package.json b/webapp/playbooks/package.json deleted file mode 100644 index 227240fd4a8..00000000000 --- a/webapp/playbooks/package.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "name": "playbooks", - "private": true, - "version": "8.0.0", - "dependencies": { - "@apollo/client": "3.7.3", - "@floating-ui/react-dom-interactions": "0.6.3", - "@mattermost/client": "*", - "@mattermost/compass-icons": "0.1.32", - "@mattermost/types": "*", - "@mdi/js": "^6.5.95", - "@mdi/react": "1.5.0", - "@tippyjs/react": "4.2.6", - "chart.js": "3.8.2", - "chartjs-plugin-annotation": "2.1.2", - "chrono-node": "2.3.5", - "core-js": "3.20.2", - "debounce": "1.2.1", - "graphql": "16.3.0", - "js-trim-multiline-string": "^1.0.8", - "lodash": "4.17.21", - "luxon": "2.3.1", - "mattermost-webapp": "*", - "parse-duration": "1.0.2", - "qs": "6.10.2", - "react": "^17.0.2", - "react-chartjs-2": "4.3.1", - "react-dom": "^17.0.2", - "react-infinite-scroll-component": "^6.1.0", - "react-infinite-scroller": "1.2.6", - "react-intl": "*", - "react-redux": "7.2.6", - "react-router-dom": "5.3.4", - "react-router-hash-link": "2.4.3", - "react-select": "4.3.1", - "react-use": "17.3.2", - "redux": "4.1.2", - "reselect": "4.1.8", - "styled-components": "5.3.3", - "typescript": "4.7.4" - }, - "devDependencies": { - "@formatjs/cli": "4.7.0", - "@graphql-codegen/cli": "2.16.3", - "@graphql-codegen/client-preset": "1.2.5", - "@testing-library/react-hooks": "8.0.0", - "@types/debounce": "1.2.1", - "@types/history": "4.7.8", - "@types/jest": "27.4.0", - "@types/lodash": "4.14.178", - "@types/luxon": "2.0.9", - "@types/qs": "6.9.7", - "@types/react": "^17.0.2", - "@types/react-beautiful-dnd": "13.1.2", - "@types/react-bootstrap": "1.0.1", - "@types/react-custom-scrollbars": "4.0.10", - "@types/react-dom": "^17.0.2", - "@types/react-infinite-scroller": "1.2.3", - "@types/react-redux": "7.1.21", - "@types/react-router-dom": "5.3.3", - "@types/react-router-hash-link": "2.4.5", - "@types/react-select": "3.1.2", - "@types/react-test-renderer": "17.0.1", - "@types/redux-mock-store": "1.0.3", - "@types/shallow-equals": "1.0.0", - "@types/styled-components": "5.1.19", - "@typescript-eslint/eslint-plugin": "5.57.1", - "@typescript-eslint/parser": "5.57.1", - "babel-plugin-add-react-displayname": "0.0.5", - "classnames": "2.3.1", - "eslint-plugin-import": "2.25.4", - "eslint-plugin-import-newlines": "1.3.0", - "eslint-plugin-no-relative-import-paths": "1.5.0", - "eslint-plugin-react": "7.28.0", - "eslint-plugin-react-hooks": "4.3.0", - "eslint-plugin-unused-imports": "2.0.0", - "identity-obj-proxy": "3.0.0", - "jest": "27.4.7", - "jest-canvas-mock": "2.3.1", - "jest-junit": "13.0.0", - "react-beautiful-dnd": "13.1.0", - "react-bootstrap": "1.6.1", - "react-test-renderer": "17.0.2", - "redux-mock-store": "1.5.4", - "redux-thunk": "2.4.1", - "ts-prune": "0.10.3" - }, - "scripts": { - "build": "webpack --mode=production --stats-error-details", - "build:watch": "webpack --mode=production --watch", - "debug": "webpack --mode=development", - "debug:watch": "webpack --mode=development --watch", - "check": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --cache", - "fix": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx --ext tsx --ext ts . --quiet --fix --cache", - "test": "cross-env TZ=Etc/UTC jest", - "test:watch": "cross-env TZ=Etc/UTC jest --watch", - "test:updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot", - "test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles --verbose", - "test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100%", - "check-types": "tsc -b", - "i18n-extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.d.ts\" --id-interpolation-pattern '[sha512:contenthash:base64:6]' --format simple --out-file i18n/en.json", - "graphql": "graphql-codegen --config graphql_gen.ts", - "report-unused-exports": "ts-prune", - "start:product": "webpack --mode=development --watch", - "deploy:product": "node scripts/deploy.js", - "clean": "rm -rf dist node_modules .eslintcache" - } -} diff --git a/webapp/playbooks/rudder_transform.js b/webapp/playbooks/rudder_transform.js deleted file mode 100644 index 34d2885ed28..00000000000 --- a/webapp/playbooks/rudder_transform.js +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable no-unused-vars */ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -// Note that This file is not used from webapp's build -// -// It's the transformer function that ruddersack should have to transform the -// old event data into the new event data. - -// ts-prune-ignore-next -export function transformEvent(event, metadata) { - const action = event.properties.Action; - - if (action === undefined) { - return event; - } - - switch (event.event) { - // eslint-disable-next-line lines-around-comment - // Rename events - case 'playbookrun_get_involved_join': - event.event = 'playbookrun_participate'; - break; - case 'playbookrun_request_update_click': - event.event = 'playbookrun_request_update'; - break; - case 'playbookrun_action': - switch (action) { - case 'update_playbookrun_actions': - event.event = 'playbookrun_update_actions'; - delete event.properties.Action; - break; - } - break; - - // Convert old frontend events - case 'frontend': - switch (action) { - case 'view_run_details': - event.type = 'page'; - event.event = 'run_details'; - delete event.properties.Action; - break; - case 'view_run_channels_rhs_details': - event.event = 'channels_rhs_rundetails'; - event.type = 'page'; - delete event.properties.Action; - break; - - // ... other actions for frontend event - } - - // ... other events - break; - } - return event; -} diff --git a/webapp/playbooks/scripts/deploy.js b/webapp/playbooks/scripts/deploy.js deleted file mode 100644 index df61881515d..00000000000 --- a/webapp/playbooks/scripts/deploy.js +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/* eslint-disable no-console */ - -const fs = require('fs'); - -async function deploy() { - const target = '../channels/dist/products/playbooks'; - if (!fs.existsSync(target)) { - fs.mkdirSync(target, {recursive: true}); - } - fs.cpSync('dist', target, {recursive: true}); -} - -deploy(); diff --git a/webapp/playbooks/src/actions.ts b/webapp/playbooks/src/actions.ts deleted file mode 100644 index df0f4ef1392..00000000000 --- a/webapp/playbooks/src/actions.ts +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -import {AnyAction, Dispatch} from 'redux'; - -import {generateId} from 'mattermost-redux/utils/helpers'; -import {IntegrationTypes} from 'mattermost-redux/action_types'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; -import {GetStateFunc} from 'mattermost-redux/types/actions'; - -import {makeModalDefinition as makeUpdateRunNameModalDefinition} from 'src/components/modals/run_update_name'; -import {makeModalDefinition as makeUpdateRunChannelModalDefinition} from 'src/components/modals/run_update_channel'; -import {makeModalDefinition as makePlaybookRunModalDefinition} from 'src/components/modals/run_playbook_modal'; -import {PlaybookRun} from 'src/types/playbook_run'; -import {canIPostUpdateForRun, selectToggleRHS} from 'src/selectors'; -import {BackstageRHSSection, BackstageRHSViewMode} from 'src/types/backstage_rhs'; -import { - CLOSE_BACKSTAGE_RHS, - CloseBackstageRHS, - HIDE_CHANNEL_ACTIONS_MODAL, - HIDE_PLAYBOOK_ACTIONS_MODAL, - HIDE_POST_MENU_MODAL, - HIDE_RUN_ACTIONS_MODAL, - HideChannelActionsModal, - HidePlaybookActionsModal, - HidePostMenuModal, - HideRunActionsModal, - OPEN_BACKSTAGE_RHS, - OpenBackstageRHS, - PLAYBOOK_ARCHIVED, - PLAYBOOK_CREATED, - PLAYBOOK_RESTORED, - PLAYBOOK_RUN_CREATED, - PLAYBOOK_RUN_UPDATED, - PUBLISH_TEMPLATES, - PlaybookArchived, - PlaybookCreated, - PlaybookRestored, - PlaybookRunCreated, - PlaybookRunUpdated, - PublishTemplates, - RECEIVED_GLOBAL_SETTINGS, - RECEIVED_PLAYBOOK_RUNS, - RECEIVED_TEAM_PLAYBOOK_RUNS, - RECEIVED_TOGGLE_RHS_ACTION, - REMOVED_FROM_CHANNEL, - ReceivedGlobalSettings, - ReceivedPlaybookRuns, - ReceivedTeamPlaybookRuns, - ReceivedToggleRHSAction, - RemovedFromChannel, - SET_ALL_CHECKLISTS_COLLAPSED_STATE, - SET_CHECKLIST_COLLAPSED_STATE, - SET_CHECKLIST_ITEMS_FILTER, - SET_CLIENT_ID, - SET_EVERY_CHECKLIST_COLLAPSED_STATE, - SET_HAS_VIEWED_CHANNEL, - SET_RHS_ABOUT_COLLAPSED_STATE, - SET_RHS_OPEN, - SHOW_CHANNEL_ACTIONS_MODAL, - SHOW_PLAYBOOK_ACTIONS_MODAL, - SHOW_POST_MENU_MODAL, - SHOW_RUN_ACTIONS_MODAL, - SetAllChecklistsCollapsedState, - SetChecklistCollapsedState, - SetChecklistItemsFilter, - SetClientId, - SetEveryChecklistCollapsedState, - SetHasViewedChannel, - SetRHSAboutCollapsedState, - SetRHSOpen, - SetTriggerId, - ShowChannelActionsModal, - ShowPlaybookActionsModal, - ShowPostMenuModal, - ShowRunActionsModal, -} from 'src/types/actions'; -import {clientExecuteCommand} from 'src/client'; -import {GlobalSettings} from 'src/types/settings'; -import {ChecklistItemsFilter, TaskAction as TaskActionType} from 'src/types/playbook'; -import {modals} from 'src/webapp_globals'; -import {makeModalDefinition as makeUpdateRunStatusModalDefinition} from 'src/components/modals/update_run_status_modal'; -import {makePlaybookAccessModalDefinition} from 'src/components/backstage/playbook_access_modal'; - -import {PlaybookCreateModalProps, makePlaybookCreateModal} from 'src/components/create_playbook_modal'; -import {makeRhsRunDetailsTourDialog} from 'src/components/rhs/rhs_run_details_tour_dialog'; -import {PresetTemplate} from 'src/components/templates/template_data'; -import {makeTaskActionsModalDefinition} from 'src/components/checklist_item/task_actions_modal'; -import {PlaybookRunType} from 'src/graphql/generated/graphql'; - -export function startPlaybookRun(teamId: string, postId?: string) { - return async (dispatch: Dispatch, getState: GetStateFunc) => { - // Add unique id - const clientId = generateId(); - dispatch(setClientId(clientId)); - - let command = `/playbook run ${clientId}`; - if (postId) { - command = `${command} ${postId}`; - } - - await clientExecuteCommand(dispatch, getState, command, teamId); - }; -} - -export function openUpdateRunNameModal(playbookRunId: string, onSubmit: (newName: string) => void) { - return modals.openModal(makeUpdateRunNameModalDefinition({ - playbookRunId, - onSubmit, - })); -} - -export function openUpdateRunChannelModal(playbookRunId: string, teamId: string, type: PlaybookRunType, onSubmit: (newChannelId: string, newChannelName: string) => void) { - return modals.openModal(makeUpdateRunChannelModalDefinition({ - playbookRunId, - teamId, - onSubmit, - })); -} - -type newRunModalProps = { - playbookId?: string, - triggerChannelId?: string, - teamId: string, - onRunCreated: (runId: string, channelId: string, statsData: object) => void, -}; - -export function openPlaybookRunModal(dialogProps: newRunModalProps) { - return modals.openModal(makePlaybookRunModalDefinition( - dialogProps.playbookId, - dialogProps.triggerChannelId, - dialogProps.teamId, - dialogProps.onRunCreated, - )); -} - -export function promptUpdateStatus( - teamId: string, - playbookRunId: string, - channelId: string, -) { - return async (dispatch: Dispatch, getState: GetStateFunc) => { - const state = getState(); - const hasPermission = canIPostUpdateForRun(state, channelId, teamId); - dispatch(openUpdateRunStatusModal(playbookRunId, channelId, hasPermission)); - }; -} - -export function openUpdateRunStatusModal( - playbookRunId: string, - channelId: string, - hasPermission: boolean, - message?: string, - reminderInSeconds?: number, - finishRunChecked?: boolean -) { - return modals.openModal(makeUpdateRunStatusModalDefinition({ - playbookRunId, - channelId, - hasPermission, - message, - reminderInSeconds, - finishRunChecked, - })); -} - -export function displayEditPlaybookAccessModal( - playbookId: string, - refetch?: () => void, -) { - return async (dispatch: Dispatch) => { - dispatch(modals.openModal(makePlaybookAccessModalDefinition({playbookId, refetch}))); - }; -} - -export function displayPlaybookCreateModal(props: PlaybookCreateModalProps) { - return async (dispatch: Dispatch) => { - dispatch(modals.openModal(makePlaybookCreateModal(props))); - }; -} - -export function displayRhsRunDetailsTourDialog(props: Parameters[0]) { - return async (dispatch: Dispatch) => { - dispatch(modals.openModal(makeRhsRunDetailsTourDialog(props))); - }; -} - -export function finishRun(teamId: string, playbookRunId: string) { - return async (dispatch: Dispatch, getState: GetStateFunc) => { - await clientExecuteCommand(dispatch, getState, `/playbook finish-by-id ${playbookRunId}`, teamId); - }; -} - -export function addToTimeline(postId: string) { - return async (dispatch: Dispatch, getState: GetStateFunc) => { - const currentTeamId = getCurrentTeamId(getState()); - - await clientExecuteCommand(dispatch, getState, `/playbook add ${postId}`, currentTeamId); - }; -} - -export function setRHSOpen(open: boolean): SetRHSOpen { - return { - type: SET_RHS_OPEN, - open, - }; -} - -/** - * Stores`showRHSPlugin` action returned by - * registerRightHandSidebarComponent in plugin initialization. - */ -export function setToggleRHSAction(toggleRHSPluginAction: () => void): ReceivedToggleRHSAction { - return { - type: RECEIVED_TOGGLE_RHS_ACTION, - toggleRHSPluginAction, - }; -} - -export function toggleRHS() { - return (dispatch: Dispatch, getState: GetStateFunc) => { - selectToggleRHS(getState())(); - }; -} - -export function setTriggerId(triggerId: string): SetTriggerId { - return { - type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, - data: triggerId, - }; -} - -export function setClientId(clientId: string): SetClientId { - return { - type: SET_CLIENT_ID, - clientId, - }; -} - -export const playbookRunCreated = (playbookRun: PlaybookRun): PlaybookRunCreated => ({ - type: PLAYBOOK_RUN_CREATED, - playbookRun, -}); - -export const playbookRunUpdated = (playbookRun: PlaybookRun): PlaybookRunUpdated => ({ - type: PLAYBOOK_RUN_UPDATED, - playbookRun, -}); - -export const playbookCreated = (teamID: string): PlaybookCreated => ({ - type: PLAYBOOK_CREATED, - teamID, -}); - -export const playbookArchived = (teamID: string): PlaybookArchived => ({ - type: PLAYBOOK_ARCHIVED, - teamID, -}); - -export const playbookRestored = (teamID: string): PlaybookRestored => ({ - type: PLAYBOOK_RESTORED, - teamID, -}); - -export const receivedPlaybookRuns = (playbookRuns: PlaybookRun[]): ReceivedPlaybookRuns => ({ - type: RECEIVED_PLAYBOOK_RUNS, - playbookRuns, -}); - -export const receivedTeamPlaybookRuns = (playbookRuns: PlaybookRun[]): ReceivedTeamPlaybookRuns => ({ - type: RECEIVED_TEAM_PLAYBOOK_RUNS, - playbookRuns, -}); - -export const removedFromPlaybookRunChannel = (channelId: string): RemovedFromChannel => ({ - type: REMOVED_FROM_CHANNEL, - channelId, -}); - -export const actionSetGlobalSettings = (settings: GlobalSettings): ReceivedGlobalSettings => ({ - type: RECEIVED_GLOBAL_SETTINGS, - settings, -}); - -export const showPostMenuModal = (): ShowPostMenuModal => ({ - type: SHOW_POST_MENU_MODAL, -}); - -export const hidePostMenuModal = (): HidePostMenuModal => ({ - type: HIDE_POST_MENU_MODAL, -}); - -export const showChannelActionsModal = (): ShowChannelActionsModal => ({ - type: SHOW_CHANNEL_ACTIONS_MODAL, -}); - -export const hideChannelActionsModal = (): HideChannelActionsModal => ({ - type: HIDE_CHANNEL_ACTIONS_MODAL, -}); - -export const showRunActionsModal = (): ShowRunActionsModal => ({ - type: SHOW_RUN_ACTIONS_MODAL, -}); - -export const hideRunActionsModal = (): HideRunActionsModal => ({ - type: HIDE_RUN_ACTIONS_MODAL, -}); - -export const showPlaybookActionsModal = (): ShowPlaybookActionsModal => ({ - type: SHOW_PLAYBOOK_ACTIONS_MODAL, -}); - -export const hidePlaybookActionsModal = (): HidePlaybookActionsModal => ({ - type: HIDE_PLAYBOOK_ACTIONS_MODAL, -}); - -export const setHasViewedChannel = (channelId: string): SetHasViewedChannel => ({ - type: SET_HAS_VIEWED_CHANNEL, - channelId, - hasViewed: true, -}); - -export const setRHSAboutCollapsedState = (channelId: string, collapsed: boolean): SetRHSAboutCollapsedState => ({ - type: SET_RHS_ABOUT_COLLAPSED_STATE, - channelId, - collapsed, -}); - -export const setChecklistCollapsedState = (key: string, checklistIndex: number, collapsed: boolean): SetChecklistCollapsedState => ({ - type: SET_CHECKLIST_COLLAPSED_STATE, - key, - checklistIndex, - collapsed, -}); - -export const setEveryChecklistCollapsedStateChange = (key: string, state: Record): SetEveryChecklistCollapsedState => ({ - type: SET_EVERY_CHECKLIST_COLLAPSED_STATE, - key, - state, -}); - -export const setAllChecklistsCollapsedState = (key: string, collapsed: boolean, numOfChecklists: number): SetAllChecklistsCollapsedState => ({ - type: SET_ALL_CHECKLISTS_COLLAPSED_STATE, - key, - numOfChecklists, - collapsed, -}); - -export const setChecklistItemsFilter = (key: string, nextState: ChecklistItemsFilter): SetChecklistItemsFilter => ({ - type: SET_CHECKLIST_ITEMS_FILTER, - key, - nextState, -}); - -export function openTaskActionsModal(onTaskActionsChange: (newTaskActions: TaskActionType[]) => void, taskActions?: TaskActionType[] | null) { - return modals.openModal(makeTaskActionsModalDefinition(onTaskActionsChange, taskActions)); -} - -export const closeBackstageRHS = (): CloseBackstageRHS => ({ - type: CLOSE_BACKSTAGE_RHS, -}); - -export const openBackstageRHS = (section: BackstageRHSSection, viewMode: BackstageRHSViewMode): OpenBackstageRHS => ({ - type: OPEN_BACKSTAGE_RHS, - section, - viewMode, -}); - -export const publishTemplates = (templates: PresetTemplate[]): PublishTemplates => ({ - type: PUBLISH_TEMPLATES, - templates, -}); diff --git a/webapp/playbooks/src/browser_routing.ts b/webapp/playbooks/src/browser_routing.ts deleted file mode 100644 index 3bf2163133a..00000000000 --- a/webapp/playbooks/src/browser_routing.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -// @ts-ignore -const WebappUtils = window.WebappUtils; - -type PathLike = { - pathname: string; - search: string; -} - -export const navigateToUrl = (urlPath: string | PathLike) => { - WebappUtils.browserHistory.push(urlPath); -}; - -export const pluginUrl = (urlPath: string) => { - return '/playbooks' + urlPath; -}; - -export const navigateToPluginUrl = (urlPath: string) => { - WebappUtils.browserHistory.push(pluginUrl(urlPath)); -}; - -/** - * Navigate to channel given a channelId and teamName - */ -export const navigateToChannel = async (teamName: string, channelId: string) => { - navigateToUrl(`/${teamName}/channels/${channelId}`); -}; - -export const pluginErrorUrl = (type: string) => { - return pluginUrl(`/error?type=${type}`); -}; - -export const handleFormattedTextClick = (e: React.MouseEvent, currentRelativeTeamUrl: string) => { - // @ts-ignore - const channelMentionAttribute = e.target.getAttributeNode('data-channel-mention'); - - if (channelMentionAttribute) { - e.preventDefault(); - navigateToUrl(currentRelativeTeamUrl + '/channels/' + channelMentionAttribute.value); - } -}; diff --git a/webapp/playbooks/src/client.ts b/webapp/playbooks/src/client.ts deleted file mode 100644 index 2141753d88b..00000000000 --- a/webapp/playbooks/src/client.ts +++ /dev/null @@ -1,813 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {AnyAction, Dispatch} from 'redux'; -import qs from 'qs'; - -import {GetStateFunc} from 'mattermost-redux/types/actions'; -import {IntegrationTypes} from 'mattermost-redux/action_types'; -import {Client4} from 'mattermost-redux/client'; -import {ClientError} from '@mattermost/client'; -import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; - -import { - FetchPlaybookRunsParams, - FetchPlaybookRunsReturn, - Metadata, - PlaybookRun, - RunMetricData, - StatusPostComplete, -} from 'src/types/playbook_run'; - -import {setTriggerId} from 'src/actions'; -import {OwnerInfo} from 'src/types/backstage'; -import { - PlaybookRunEventTarget, - PlaybookRunViewTarget, - TelemetryEventTarget, - TelemetryViewTarget, -} from 'src/types/telemetry'; -import { - Checklist, - ChecklistItem, - ChecklistItemState, - DraftPlaybookWithChecklist, - FetchPlaybooksParams, - FetchPlaybooksReturn, - Playbook, - PlaybookWithChecklist, -} from 'src/types/playbook'; -import {AdminNotificationType} from 'src/constants'; -import {ChannelAction} from 'src/types/channel_actions'; -import {EmptyPlaybookStats, PlaybookStats, SiteStats} from 'src/types/stats'; - -import {pluginId} from './manifest'; -import {GlobalSettings, globalSettingsSetDefaults} from './types/settings'; -import {Category} from './types/category'; -import {InsightsResponse} from './types/insights'; - -let siteURL = ''; -let basePath = ''; -let apiUrl = `${basePath}/plugins/${pluginId}/api/v0`; - -export const setSiteUrl = (url?: string): void => { - if (url) { - basePath = new URL(url).pathname.replace(/\/+$/, ''); - siteURL = url; - } else { - basePath = ''; - siteURL = ''; - } - - apiUrl = `${basePath}/plugins/${pluginId}/api/v0`; -}; - -export const getSiteUrl = (): string => { - return siteURL; -}; - -export const getApiUrl = (): string => { - return apiUrl; -}; - -export async function fetchPlaybookRuns(params: FetchPlaybookRunsParams) { - const queryParams = qs.stringify(params, {addQueryPrefix: true, indices: false}); - - let data = await doGet(`${apiUrl}/runs${queryParams}`); - if (!data) { - data = {items: [], total_count: 0, page_count: 0, has_more: false} as FetchPlaybookRunsReturn; - } - - return data as FetchPlaybookRunsReturn; -} - -export async function fetchPlaybookRun(id: string) { - const data = await doGet(`${apiUrl}/runs/${id}`); - - return data as PlaybookRun; -} - -export async function fetchPlaybookRunStatusUpdates(id: string) { - return doGet(`${apiUrl}/runs/${id}/status-updates`); -} - -export async function createPlaybookRun( - playbook_id: string, - owner_user_id: string, - team_id: string, - name: string, - description: string, - channel_id?: string, - create_public_run?: boolean -) { - const run = await doPost(`${apiUrl}/runs`, JSON.stringify({ - owner_user_id, - team_id, - name, - description, - playbook_id, - channel_id, - create_public_run, - })); - return run as PlaybookRun; -} - -export async function postStatusUpdate( - playbookRunId: string, - payload: { - message: string, - reminder?: number, - finishRun: boolean, - }, - ids: { - user_id: string, - channel_id: string, - team_id: string, - }, -) { - const base = { - type: 'dialog_submission', - callback_id: '', - state: '', - cancelled: false, - }; - - const body = JSON.stringify({ - ...base, - ...ids, - submission: { - ...payload, - reminder: payload.reminder?.toFixed() ?? '', - finish_run: payload.finishRun, - }, - }); - - try { - const data = await doPost(`${apiUrl}/runs/${playbookRunId}/update-status-dialog`, body); - return data; - } catch (error) { - return {error}; - } -} - -export async function fetchPlaybookRunMetadata(id: string) { - const data = await doGet(`${apiUrl}/runs/${id}/metadata`); - - return data; -} - -export async function fetchPlaybookRunByChannel(channelId: string) { - const data = await doGet(`${apiUrl}/runs/channel/${channelId}`); - - return data as PlaybookRun; -} - -export async function fetchPlaybookRunsForChannelByUser(channelId: string) { - const data = await doGet(`${apiUrl}/runs/channel/${channelId}/runs`); - - return data as PlaybookRun[]; -} - -export async function fetchCheckAndSendMessageOnJoin(channelId: string) { - const data = await doGet(`${apiUrl}/actions/channels/${channelId}/check-and-send-message-on-join`); - return Boolean(data.viewed); -} - -export function fetchPlaybookRunChannels(teamID: string, userID: string) { - return doGet(`${apiUrl}/runs/channels?team_id=${teamID}&participant_id=${userID}`); -} - -export async function clientExecuteCommand(dispatch: Dispatch, getState: GetStateFunc, command: string, teamId: string) { - let currentChannel = getCurrentChannel(getState()); - - // Default to town square if there is no current channel (i.e., if Mattermost has not yet loaded) - // or in a different team. - if (!currentChannel || currentChannel.team_id !== teamId) { - currentChannel = await Client4.getChannelByName(teamId, 'town-square'); - } - - const args = { - channel_id: currentChannel?.id, - team_id: teamId, - }; - - try { - const data = await Client4.executeCommand(command, args); - dispatch(setTriggerId(data?.trigger_id)); - } catch (error) { - console.error(error); //eslint-disable-line no-console - } -} - -export async function clientRunChecklistItemSlashCommand(dispatch: Dispatch, playbookRunId: string, checklistNumber: number, itemNumber: number) { - try { - const data = await doPost(`${apiUrl}/runs/${playbookRunId}/checklists/${checklistNumber}/item/${itemNumber}/run`); - if (data.trigger_id) { - dispatch({type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID, data: data.trigger_id}); - } - } catch (error) { - console.error(error); //eslint-disable-line no-console - } -} - -export function clientFetchPlaybooks(teamID: string, params: FetchPlaybooksParams) { - const queryParams = qs.stringify({ - team_id: teamID, - ...params, - }, {addQueryPrefix: true}); - return doGet(`${apiUrl}/playbooks${queryParams}`); -} - -export const clientHasPlaybooks = async (teamID: string): Promise => { - const result = await clientFetchPlaybooks(teamID, { - page: 0, - per_page: 1, - }) as FetchPlaybooksReturn; - - return result.items?.length > 0; -}; - -export function clientFetchPlaybook(playbookID: string) { - return doGet(`${apiUrl}/playbooks/${playbookID}`); -} - -export async function savePlaybook(playbook: PlaybookWithChecklist | DraftPlaybookWithChecklist) { - if (!playbook.id) { - const data = await doPost(`${apiUrl}/playbooks`, JSON.stringify(playbook)); - return data; - } - - await doFetchWithoutResponse(`${apiUrl}/playbooks/${playbook.id}`, { - method: 'PUT', - body: JSON.stringify(playbook), - }); - return {id: playbook.id}; -} - -export async function archivePlaybook(playbookId: Playbook['id']) { - const {data} = await doFetchWithTextResponse(`${apiUrl}/playbooks/${playbookId}`, { - method: 'DELETE', - }); - return data; -} - -export async function restorePlaybook(playbookId: Playbook['id']) { - const {data} = await doFetchWithTextResponse(`${apiUrl}/playbooks/${playbookId}/restore`, { - method: 'PUT', - }); - return data; -} - -export async function importFile(file: any, teamId: string) { - const data = await doPost(`${apiUrl}/playbooks/import?team_id=${teamId}`, file); - return data; -} - -export async function duplicatePlaybook(playbookId: Playbook['id']) { - const {id} = await doPost(`${apiUrl}/playbooks/${playbookId}/duplicate`, ''); - return id; -} - -export async function fetchOwnersInTeam(teamId: string): Promise { - const queryParams = qs.stringify({team_id: teamId}, {addQueryPrefix: true}); - - let data = await doGet(`${apiUrl}/runs/owners${queryParams}`); - if (!data) { - data = []; - } - return data as OwnerInfo[]; -} - -export async function finishRun(playbookRunId: string) { - try { - return await doPut(`${apiUrl}/runs/${playbookRunId}/finish`); - } catch (error) { - return {error}; - } -} - -export async function restoreRun(playbookRunId: string) { - try { - return await doPut(`${apiUrl}/runs/${playbookRunId}/restore`); - } catch (error) { - return {error}; - } -} - -export async function toggleRunStatusUpdates(playbookRunId: string, status_enabled: boolean) { - try { - return await doPut(`${apiUrl}/runs/${playbookRunId}/status-update-enabled`, JSON.stringify({status_enabled})); - } catch (error) { - return {error}; - } -} - -export async function setOwner(playbookRunId: string, ownerId: string) { - const body = `{"owner_id": "${ownerId}"}`; - try { - const data = await doPost(`${apiUrl}/runs/${playbookRunId}/owner`, body); - return data; - } catch (error) { - return {error}; - } -} - -export async function setAssignee(playbookRunId: string, checklistNum: number, itemNum: number, assigneeId?: string) { - const body = JSON.stringify({assignee_id: assigneeId}); - try { - return await doPut(`${apiUrl}/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/assignee`, body); - } catch (error) { - return {error}; - } -} - -export async function setDueDate(playbookRunId: string, checklistNum: number, itemNum: number, date?: number) { - const body = JSON.stringify({due_date: date}); - try { - return await doPut(`${apiUrl}/runs/${playbookRunId}/checklists/${checklistNum}/item/${itemNum}/duedate`, body); - } catch (error) { - return {error}; - } -} - -export async function setChecklistItemState(playbookRunID: string, checklistNum: number, itemNum: number, newState: ChecklistItemState) { - const body = JSON.stringify({new_state: newState}); - try { - return await doPut(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/state`, body); - } catch (error) { - return {error: error as ClientError}; - } -} - -export async function clientDuplicateChecklistItem(playbookRunID: string, checklistNum: number, itemNum: number) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/duplicate`, { - method: 'post', - body: '', - }); -} - -export async function clientSkipChecklistItem(playbookRunID: string, checklistNum: number, itemNum: number) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/skip`, { - method: 'put', - body: '', - }); -} - -export async function clientSkipChecklist(playbookRunID: string, checklistNum: number) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/skip`, { - method: 'PUT', - body: '', - }); -} - -export async function clientRestoreChecklist(playbookRunID: string, checklistNum: number) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/restore`, { - method: 'PUT', - body: '', - }); -} - -export async function clientRestoreChecklistItem(playbookRunID: string, checklistNum: number, itemNum: number) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/restore`, { - method: 'put', - body: '', - }); -} - -interface ChecklistItemUpdate { - title?: string - command: string - description?: string -} - -export async function clientEditChecklistItem(playbookRunID: string, checklistNum: number, itemNum: number, itemUpdate: ChecklistItemUpdate) { - const data = await doPut(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}`, - JSON.stringify({ - title: itemUpdate.title, - command: itemUpdate.command, - description: itemUpdate.description, - })); - - return data; -} - -export async function clientAddChecklistItem(playbookRunID: string, checklistNum: number, item: ChecklistItem) { - const data = await doPost(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/add`, - JSON.stringify(item) - ); - - return data; -} - -export async function clientSetChecklistItemCommand(playbookRunID: string, checklistNum: number, itemNum: number, command: string) { - const data = await doPut(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/item/${itemNum}/command`, - JSON.stringify({ - command, - })); - - return data; -} - -export async function clientAddChecklist(playbookRunID: string, checklist: Checklist) { - const data = await doPost(`${apiUrl}/runs/${playbookRunID}/checklists`, - JSON.stringify(checklist), - ); - - return data; -} - -export async function clientDuplicateChecklist(playbookRunID: string, checklistNum: number): Promise { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/duplicate`, { - method: 'post', - body: '', - }); -} - -export async function clientRenameChecklist(playbookRunID: string, checklistNum: number, newTitle: string) { - const data = await doPut(`${apiUrl}/runs/${playbookRunID}/checklists/${checklistNum}/rename`, - JSON.stringify({ - title: newTitle, - }), - ); - - return data; -} - -export async function clientMoveChecklist(playbookRunID: string, sourceChecklistIdx: number, destChecklistIdx: number) { - const data = await doPost(`${apiUrl}/runs/${playbookRunID}/checklists/move`, - JSON.stringify({ - source_checklist_idx: sourceChecklistIdx, - dest_checklist_idx: destChecklistIdx, - }), - ); - - return data; -} - -export async function clientMoveChecklistItem(playbookRunID: string, sourceChecklistIdx: number, sourceItemIdx: number, destChecklistIdx: number, destItemIdx: number) { - const data = await doPost(`${apiUrl}/runs/${playbookRunID}/checklists/move-item`, - JSON.stringify({ - source_checklist_idx: sourceChecklistIdx, - source_item_idx: sourceItemIdx, - dest_checklist_idx: destChecklistIdx, - dest_item_idx: destItemIdx, - }), - ); - - return data; -} - -export async function clientRemoveTimelineEvent(playbookRunID: string, entryID: string) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/timeline/${entryID}`, { - method: 'delete', - body: '', - }); -} - -// fetchSiteStats collect the stats we want to expose in system console -export async function fetchSiteStats(): Promise { - const data = await doGet(`${apiUrl}/stats/site`); - if (!data) { - return null; - } - return data as SiteStats; -} - -export async function fetchPlaybookStats(playbookID: string): Promise { - const data = await doGet(`${apiUrl}/stats/playbook?playbook_id=${playbookID}`); - if (!data) { - return EmptyPlaybookStats; - } - - return data as PlaybookStats; -} - -// telemetryRunAction are the event types that can be reported to telemetry server re: PlaybookRun -// string is kept to do progressive migration to enum -type telemetryRunAction = PlaybookRunViewTarget | PlaybookRunEventTarget | string; - -export async function telemetryEventForPlaybookRun(playbookRunID: string, action: telemetryRunAction) { - await doFetchWithoutResponse(`${apiUrl}/telemetry/run/${playbookRunID}`, { - method: 'POST', - body: JSON.stringify({action}), - }); -} - -export async function telemetryEventForPlaybook(playbookID: string, action: string) { - await doFetchWithoutResponse(`${apiUrl}/telemetry/playbook/${playbookID}`, { - method: 'POST', - body: JSON.stringify({action}), - }); -} - -export async function telemetryEventForTemplate(templateName: string, action: string) { - await doFetchWithoutResponse(`${apiUrl}/telemetry/template`, { - method: 'POST', - body: JSON.stringify({template_name: templateName, action}), - }); -} - -export async function telemetryEvent(name: TelemetryEventTarget, properties: {[key: string]: string}) { - await doFetchWithoutResponse(`${apiUrl}/telemetry`, { - method: 'POST', - body: JSON.stringify( - {name, type: 'track', properties} - ), - }); -} - -export async function telemetryView(name: TelemetryViewTarget, properties: {[key: string]: string}) { - await doFetchWithoutResponse(`${apiUrl}/telemetry`, { - method: 'POST', - body: JSON.stringify( - {name, type: 'page', properties} - ), - }); -} - -export async function fetchGlobalSettings(): Promise { - const data = await doGet(`${apiUrl}/settings`); - if (!data) { - return globalSettingsSetDefaults({}); - } - - return globalSettingsSetDefaults(data); -} - -export async function updateRetrospective(playbookRunID: string, updatedText: string, metrics: RunMetricData[]) { - const data = await doPost(`${apiUrl}/runs/${playbookRunID}/retrospective`, - JSON.stringify({ - retrospective: updatedText, - metrics, - })); - return data; -} - -export async function publishRetrospective(playbookRunID: string, currentText: string, metrics: RunMetricData[]) { - const data = await doPost(`${apiUrl}/runs/${playbookRunID}/retrospective/publish`, - JSON.stringify({ - retrospective: currentText, - metrics, - })); - return data; -} - -export async function noRetrospective(playbookRunID: string) { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunID}/no-retrospective-button`, { - method: 'POST', - }); -} - -export function exportChannelUrl(channelId: string) { - const exportPluginUrl = '/plugins/com.mattermost.plugin-channel-export/api/v1'; - - const queryParams = qs.stringify({ - channel_id: channelId, - format: 'csv', - }, {addQueryPrefix: true}); - - return `${exportPluginUrl}/export${queryParams}`; -} - -export const postMessageToAdmins = async (messageType: AdminNotificationType) => { - const body = `{"message_type": "${messageType}"}`; - try { - const response = await doPost(`${apiUrl}/bot/notify-admins`, body); - return {data: response}; - } catch (e) { - return {error: e.message}; - } -}; - -export const notifyConnect = async () => { - await doFetchWithoutResponse(`${apiUrl}/bot/connect`, { - method: 'GET', - headers: { - 'X-Timezone-Offset': -new Date().getTimezoneOffset() / 60, - }, - }); -}; - -export const followPlaybookRun = async (playbookRunId: string) => { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunId}/followers`, { - method: 'PUT', - }); -}; - -export const unfollowPlaybookRun = async (playbookRunId: string) => { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunId}/followers`, { - method: 'DELETE', - }); -}; - -export const autoFollowPlaybook = async (playbookId: string, userId: string) => { - await doFetchWithoutResponse(`${apiUrl}/playbooks/${playbookId}/autofollows/${userId}`, { - method: 'PUT', - }); -}; - -export const autoUnfollowPlaybook = async (playbookId: string, userId: string) => { - await doFetchWithoutResponse(`${apiUrl}/playbooks/${playbookId}/autofollows/${userId}`, { - method: 'DELETE', - }); -}; - -export async function clientFetchPlaybookFollowers(playbookId: string): Promise { - const data = await doGet(`${apiUrl}/playbooks/${playbookId}/autofollows`); - - if (!data) { - return []; - } - - return data; -} - -export const resetReminder = async (playbookRunId: string, newReminderSeconds: number) => { - await doFetchWithoutResponse(`${apiUrl}/runs/${playbookRunId}/reminder`, { - method: 'POST', - body: JSON.stringify({ - new_reminder_seconds: newReminderSeconds, - }), - }); -}; - -export const fetchChannelActions = async (channelID: string, triggerType?: string): Promise => { - const queryParams = triggerType ? `?trigger_type=${triggerType}` : ''; - const data = await doGet(`${apiUrl}/actions/channels/${channelID}${queryParams}`); - if (!data) { - return []; - } - - return data; -}; - -export const saveChannelAction = async (action: ChannelAction): Promise => { - if (!action.id) { - const data = await doPost(`${apiUrl}/actions/channels/${action.channel_id}`, JSON.stringify(action)); - return data.id; - } - - await doFetchWithoutResponse(`${apiUrl}/actions/channels/${action.channel_id}/${action.id}`, { - method: 'PUT', - body: JSON.stringify(action), - }); - return action.id; -}; - -export const requestUpdate = async (playbookRunId: string) => { - try { - return await doPost(`${apiUrl}/runs/${playbookRunId}/request-update`); - } catch (error) { - return {error}; - } -}; - -export const requestJoinChannel = async (playbookRunId: string) => { - try { - return await doPost(`${apiUrl}/runs/${playbookRunId}/request-join-channel`); - } catch (error) { - return {error}; - } -}; - -export const isFavoriteItem = async (teamID: string, itemID: string, itemType: string) => { - const data = await doGet(`${apiUrl}/my_categories/favorites?team_id=${teamID}&item_id=${itemID}&type=${itemType}`); - return Boolean(data); -}; - -export const fetchMyCategories = async (teamID: string): Promise => { - const queryParams = `?team_id=${teamID}`; - const data = await doGet(`${apiUrl}/my_categories${queryParams}`); - if (!data) { - return []; - } - - return data; -}; - -export const setCategoryCollapsed = async (categoryID: string, collapsed: boolean) => { - try { - return await doPut(`${apiUrl}/my_categories/${categoryID}/collapse`, collapsed); - } catch (error) { - return {error}; - } -}; - -export const doGet = async (url: string) => { - const {data} = await doFetchWithResponse(url, {method: 'get'}); - - return data; -}; - -export const doPost = async (url: string, body = {}) => { - const {data} = await doFetchWithResponse(url, { - method: 'POST', - body, - }); - - return data; -}; - -export const doPut = async (url: string, body = {}) => { - const {data} = await doFetchWithResponse(url, { - method: 'PUT', - body, - }); - - return data; -}; - -export const doFetchWithResponse = async (url: string, options = {}) => { - const response = await fetch(url, Client4.getOptions(options)); - let data; - if (response.ok) { - const contentType = response.headers.get('content-type'); - if (contentType === 'application/json') { - data = await response.json() as TData; - } - - return { - response, - data, - }; - } - - data = await response.text(); - - throw new ClientError(Client4.url, { - message: data || '', - status_code: response.status, - url, - }); -}; - -export const doFetchWithTextResponse = async (url: string, options = {}) => { - const response = await fetch(url, Client4.getOptions(options)); - - let data; - if (response.ok) { - data = await response.text() as TData; - - return { - response, - data, - }; - } - - data = await response.text(); - - throw new ClientError(Client4.url, { - message: data || '', - status_code: response.status, - url, - }); -}; - -export const doFetchWithoutResponse = async (url: string, options = {}) => { - const response = await fetch(url, Client4.getOptions(options)); - - if (response.ok) { - return; - } - - throw new ClientError(Client4.url, { - message: '', - status_code: response.status, - url, - }); -}; - -export const playbookExportProps = (playbook: {id: string, title: string}) => { - const href = `${apiUrl}/playbooks/${playbook.id}/export`; - const filename = playbook.title.split(/\s+/).join('_').toLowerCase() + '_playbook.json'; - return [href, filename]; -}; - -export async function getMyTopPlaybooks(timeRange: string, page: number, perPage: number, teamId: string): Promise { - const queryParams = qs.stringify({ - time_range: timeRange, - page, - per_page: perPage, - team_id: teamId, - }, {addQueryPrefix: true}); - - const data = await doGet(`${apiUrl}/playbooks/insights/user/me${queryParams}`); - if (!data) { - return null; - } - return data as InsightsResponse; -} - -export async function getTeamTopPlaybooks(timeRange: string, page: number, perPage: number, teamId: string): Promise { - const queryParams = qs.stringify({ - time_range: timeRange, - page, - per_page: perPage, - }, {addQueryPrefix: true}); - - const data = await doGet(`${apiUrl}/playbooks/insights/teams/${teamId}${queryParams}`); - if (!data) { - return null; - } - return data as InsightsResponse; -} diff --git a/webapp/playbooks/src/components/__snapshots__/formatted_duration.test.tsx.snap b/webapp/playbooks/src/components/__snapshots__/formatted_duration.test.tsx.snap deleted file mode 100644 index bb769a555f9..00000000000 --- a/webapp/playbooks/src/components/__snapshots__/formatted_duration.test.tsx.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`FormattedDuration renders correctly 1`] = ` -
- 59s -
-`; - -exports[`FormattedDuration renders correctly 1.5 years 1`] = ` -
- 1y, 181d, 0m -
-`; - -exports[`FormattedDuration renders correctly slightly greater than 1 year 1`] = ` -
- 1y, 0m -
-`; - -exports[`FormattedDuration renders correctly when to is 0 or undefined 1`] = ` -
- 3y, 2h, 29m -
-`; diff --git a/webapp/playbooks/src/components/actions_modal.tsx b/webapp/playbooks/src/components/actions_modal.tsx deleted file mode 100644 index c83ac2a8bec..00000000000 --- a/webapp/playbooks/src/components/actions_modal.tsx +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useIntl} from 'react-intl'; -import {Modal} from 'react-bootstrap'; - -import styled from 'styled-components'; - -import {LightningBoltOutlineIcon} from '@mattermost/compass-icons/components'; - -import GenericModal, {DefaultFooterContainer, ModalSubheading} from 'src/components/widgets/generic_modal'; - -interface Props { - id: string; - title: React.ReactNode; - subtitle: React.ReactNode; - show: boolean; - onHide: () => void; - editable: boolean; - onSave: () => void; - children: React.ReactNode; - isValid: boolean; - autoCloseOnConfirmButton?: boolean; -} - -const ActionsModal = (props: Props) => { - const {formatMessage} = useIntl(); - - const header = ( -
- - - - - {props.title} - - {props.subtitle} - - -
- ); - - // We want to show the confirm button but disabled when is invalid - const onHandleConfirm = () => { - if (!props.editable) { - return null; - } - if (!props.isValid) { - return () => null; - } - return props.onSave; - }; - - return ( - {/* do nothing else after the modal has exited */}} - handleCancel={props.editable ? props.onHide : null} - handleConfirm={onHandleConfirm()} - confirmButtonText={formatMessage({defaultMessage: 'Save'})} - cancelButtonText={formatMessage({defaultMessage: 'Cancel'})} - isConfirmDisabled={!props.editable} - confirmButtonClassName={props.isValid ? '' : 'disabled'} - isConfirmDestructive={false} - autoCloseOnCancelButton={true} - autoCloseOnConfirmButton={props.autoCloseOnConfirmButton ?? false} - enforceFocus={true} - components={{ - Header: ModalHeader, - FooterContainer: ModalFooter, - }} - > - {props.children} - - ); -}; - -const ModalHeader = styled(Modal.Header)` - &&&& { - margin-bottom: 0; - } -`; - -const StyledModal = styled(GenericModal)` - .modal-body { - :before { - content: ''; - height: 1px; - width: 600px; - position: absolute; - left: -24px; - top: 0px; - background: rgba(var(--center-channel-color-rgb), 0.08); - } - } -`; - -const ModalTitle = styled.div` - font-weight: 600; - font-size: 20px; - line-height: 20px; -`; - -const ModalFooter = styled(DefaultFooterContainer)` - :after { - content: ''; - height: 1px; - width: 100%; - position: absolute; - left: 0px; - margin-top: -24px; - - background: rgba(var(--center-channel-color-rgb), 0.08); - } - - .disabled { - opacity: 0.4; - cursor: not-allowed; - } -`; - -const Header = styled.div` - display: flex; - flex-direction: row; -`; - -const IconWrapper = styled.div` - margin-right: 14px; - margin-top: 2px; -`; - -export const TriggersContainer = styled.div` - display: flex; - flex-direction: column; - row-gap: 16px; - @media screen and (max-height: 900px) { - max-height: 500px; - overflow-x: hidden; - overflow-y: scroll; - } -`; - -export const ActionsContainer = styled.div` - display: flex; - flex-direction: column; - row-gap: 20px; -`; - -export default ActionsModal; diff --git a/webapp/playbooks/src/components/actions_modal_action.tsx b/webapp/playbooks/src/components/actions_modal_action.tsx deleted file mode 100644 index 6dd4745885e..00000000000 --- a/webapp/playbooks/src/components/actions_modal_action.tsx +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import {Toggle as BasicToggle} from 'src/components/backstage/playbook_edit/automation/toggle'; - -interface Props { - enabled: boolean; - title: string; - onToggle: () => void; - editable: boolean; - children?: React.ReactNode; - id?: string; -} - -const Action = (props: Props) => { - const onChange = props.editable ? props.onToggle : () => {/* do nothing */}; - - return ( - - { - e.preventDefault(); - onChange(); - }} - clickable={props.editable} - > - {props.title} - {/* do nothing, clicking logic lives in Container's onClick */}} - /> - - {props.enabled && props.children && - {props.children} - } - - ); -}; - -const Wrapper = styled.div` - display: flex; - flex-direction: column; -`; - -const Container = styled.div<{clickable: boolean}>` - display: flex; - flex-direction: row; - justify-content: space-between; - cursor: ${({clickable}) => (clickable ? 'pointer' : 'default')}; -`; - -const Title = styled.label<{clickable: boolean}>` - font-weight: normal; - font-size: 14px; - cursor: ${({clickable}) => (clickable ? 'pointer' : 'default')}; -`; - -const Toggle = styled(BasicToggle)` - margin: 0; -`; - -const ChildrenContainer = styled.div` - margin-top: 8px; -`; - -export default Action; diff --git a/webapp/playbooks/src/components/actions_modal_action_children.tsx b/webapp/playbooks/src/components/actions_modal_action_children.tsx deleted file mode 100644 index ed4272b9afa..00000000000 --- a/webapp/playbooks/src/components/actions_modal_action_children.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useIntl} from 'react-intl'; - -import {usePlaybook, usePlaybooksCrud} from 'src/hooks'; - -import MarkdownTextbox from 'src/components/markdown_textbox'; -import {StyledSelect} from 'src/components/backstage/styles'; -import CategorySelector from 'src/components/backstage/category_selector'; -import ClearIndicator from 'src/components/backstage/playbook_edit/automation/clear_indicator'; - -interface WelcomeProps { - message: string; - onUpdate: (newMessage: string) => void; - editable: boolean; -} - -export const WelcomeActionChildren = ({message, onUpdate, editable}: WelcomeProps) => { - const {formatMessage} = useIntl(); - - return ( - - ); -}; - -interface RunPlaybookProps { - playbookId: string; - onUpdate: (newPlaybookId: string) => void; - editable: boolean; -} - -interface OptionType { - id: string; - value: string; - label: string; -} - -export const RunPlaybookChildren = ({playbookId, onUpdate, editable}: RunPlaybookProps) => { - const {formatMessage} = useIntl(); - const [playbook] = usePlaybook(playbookId); - const {playbooks, params, setSearchTerm} = usePlaybooksCrud({sort: 'title'}, {infinitePaging: false}); - - // Format the playbooks for use with StyledSelect. - const playbookOptions = playbooks?.map((p) => ({value: p.title, label: p.title, id: p.id})) || []; - - // Add the currently selected playbook, unless we're filtering. - const playbookOptionsWithSelected = playbookOptions; - if (playbook && params.search_term?.length === 0 && playbookOptions.findIndex((p) => p.id === playbook.id) === -1) { - playbookOptionsWithSelected.unshift({ - value: playbook.title, - label: playbook.title, - id: playbook.id, - }); - } - - return ( - true} - onChange={(option: OptionType) => onUpdate(option.id)} - options={playbookOptionsWithSelected} - value={playbookOptions?.find((p) => p.id === playbookId)} - isClearable={false} - maxMenuHeight={250} - styles={{indicatorSeparator: () => null}} - isDisabled={!editable} - captureMenuScroll={false} - menuPlacement={'auto'} - /> - ); -}; - -interface CategorizeChannelProps { - categoryName: string; - onUpdate: (newCategoryName: string) => void; - editable: boolean; -} - -export const CategorizeChannelChildren = ({categoryName, onUpdate, editable}: CategorizeChannelProps) => { - const {formatMessage} = useIntl(); - - return ( - null}} - isDisabled={!editable} - captureMenuScroll={false} - shouldRenderValue={true} - placeholder={formatMessage({defaultMessage: 'Enter category name'})} - /> - ); -}; diff --git a/webapp/playbooks/src/components/actions_modal_trigger.tsx b/webapp/playbooks/src/components/actions_modal_trigger.tsx deleted file mode 100644 index f55b25d9157..00000000000 --- a/webapp/playbooks/src/components/actions_modal_trigger.tsx +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useIntl} from 'react-intl'; - -import styled from 'styled-components'; - -import KeywordsSelector from 'src/components/keywords_selector'; - -interface Props { - title: string; - triggerModifier?: React.ReactNode; - children: React.ReactNode; -} - -const Trigger = (props: Props) => { - const {formatMessage} = useIntl(); - - return ( - -
- - - {props.title} - - {props.triggerModifier} -
- - {props.children} - -
- ); -}; - -interface TriggerKeywordsProps { - editable: boolean; - keywords: string[]; - onUpdate: (newKeywords: string[]) => void; - testId?: string; -} - -export const TriggerKeywords = ({editable, keywords, onUpdate, testId}: TriggerKeywordsProps) => { - return ( - - ); -}; - -const Container = styled.fieldset` - border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); - box-sizing: border-box; - - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.08); - border-radius: 4px; - - :first-child { - margin-top: 28px; - } - - :last-child { - margin-bottom: 28px; - } -`; - -const Header = styled.div` - background: rgba(var(--center-channel-color-rgb), 0.04); - - display: flex; - flex-direction: column; - justify-content: space-between; - - padding: 12px 20px; - padding-right: 27px; -`; - -const Legend = styled.legend` - display: flex; - flex-direction: column; - border: none; - margin: 0; -`; - -const Label = styled.div` - font-size: 11px; - color: rgba(var(--center-channel-color-rgb), 0.64); -`; - -const Title = styled.div` - font-size: 14px; - font-weight: 600; - color: var(--center-channel-color); - margin-top: 2px; -`; - -const Body = styled.div` - padding: 24px; -`; - -const StyledKeywordsSelector = styled(KeywordsSelector)` - margin-top: 8px; -`; - -export default Trigger; diff --git a/webapp/playbooks/src/components/assets/app-bar-icon.png b/webapp/playbooks/src/components/assets/app-bar-icon.png deleted file mode 100644 index 18eed43093cdf3635937d9570b3866f138af7540..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5005 zcmeI0do+}5+raNG_`NTi?6Z{^whJt-aTJ-#^|zW$>lI zuIqQ*7Z1BQD#~fg0RR*aIN2Ws0E50_0B#NX;KcXx(T8k=lV=nF@~V;_1afa{qDm<0 zsG}Vyf4y!76|lj35A6lu@fCUD_tF5UZa84S_gFkMH}Y-f@%}sQ3jsQ({co?w-^x;> z4o2QTqhNZ~E?{@{=sT?s9a)=i8~yV@_2!5s=f`JlvX9ooMwVT^5xL(YBn8g$hNMG`yaD}i%!=9F znVPdJ9z&$%!Ll*aZFX+5AcXvkUC~l^0|y7G0y^=GzF2Al$aMz9)~=SFBNrKj%JY{Q z^_U%(0Gxh{u*aw2!QFsLIq+%&An$#kY%|!gk5srblA|LXq67iOZMNKFGbM&mt{UWB ze{P!-@9~(%e(K}RuSFjkr(OJ~swT!}% zvF@?H0vngNZm!FutbO5aVHw|RdjG%2t4RjF+8fLW4qnb~jq2v)U(CNV==Okemx)Xt zdeW3wEo?V?CvQ`pPqp0sPGhg_qZ+RFR0n@f%;I{82FC`P-262m+Iiii?@0+Wg6`~q z3|_bo;N6H#xJPq&*oq7=q>2t@dl1Y_a>BDO>66^E-Ou7M^9KOiBDH|D_#!nRDNLub zI<&IiZ>e2*8KI`rCW^wmTl~z-4ZtVs$mo(L@nW4<97ReYZsJq5$S>xSKO?m?aKTDP z8cTed*xte}oNk)c!}NA1h??@njP>hb3OL#)|151g^&F$jvsM4Of}Q`&^jH`Na*WLZ zxMMkdGjadzsT7>YzHtv$7dJoJ;0nNPdpb)>nr)qj_01zfXCIKu*KmS9%Nc(wZPy#v z{GyD(D%XFSlbBi=A5@DBm}y~&@Tvx-ZeBRYl~S&yez$XI%;>)05ifc;uVNiDe??mJ zI4O@!yEa8tu2662lp|(niD`u;{Yoa#h!NObvQ?lXrcFLHftK1S@*^9;?uYY}RMC5a z8r*)oV$-M^{Sby6Q0uz5Z1Yj(^Dl|&CPJ#`EQb8TmoKiC_CM;oo2lI!gvv}gK)Ww21tAv7 zQMJc>Qmhw+%{}%y{jBR#eR)$8?fEyx%1>v9<@sH&;SNc^|MX$7X4~1Jd{%G=qyGHV zZ|{CR!#tWa^3x;IyR28%Y0G$E+V@+<^hxiQHJksulhGgjJVKM zkXq|NR2^GcO>LFwsHpoPUoz)7A^Ok}*2%M$v7o(ke%`E!}aWCvv+m1o60#zz3k;KTb~ePh<@}?yhfZ)?r^ip z;8;dv3B#5YTZYHe^YGxzCg2s?Q^9!TEatjkZ)WZm=BYr{&6*EuuteuqE|m?lY`>N9 zP{^osGoENI3uouv&F%8@@k!hvEZAkF3ynIw_r<_h`{cW3c*qGjXi$7wTuf`NOlWiX2PSEmKu%#+sBk79Bh0(6js`wyxEGRPq5L(SEvkpngG43Uw;IH!JMxqu!->7unEk~geKg3n;! zwnhP->X-#u^~YIQkQLi9y%dJJJiU7E)cma@;$Q_5+=v6(VT`P4y_Mt?Ta|c5*}Y2oVkY7z9#s;wMu039P0V@Qgsn?IWn4q75{*%u@nHEVTK|)_-b^LCwf484LXDSRi^a!VdVIr^3w(4I_n*1013ZbTbOC(t5L&rbMK2T6W>7pe#H z23eO*pr6OU4!5Gw4-w@c;^e_Cs8s`W3$CugSJtMKW~MTf@8iJ^_bhm|I;>F-HS?sMAx>i(e)qHCj$DmD>@RtEFt2oO{gSRk}(J=%J{Nyv*=D*d{H9k2SZ!G1aF0o z95IswE>H#0&qpLnW^)sex39rREKJU#wp2#3PLy8UPVPMN70~NKVe?O-$iD&J-n)_0 zE>kO8GCLQUuc{s=P(U=g@;A0NS$MZieHb(e{Bg#uIt#ZdZ+eSG{r(3lslB+)pSVtEx7o5q!2pNfdo6T_^dc(au(*jnKcE zj+a`%>&Jy22d$i}**H03Kbo9oI0DiZV6*!VC+Tla{n1^|q_)l}OjCX?2nt?SC)+-d z28L|6=ZC4`@*w2e%;ucf^d(e8u~v<2tJFPbVxEcCGgU{599^$niDmXhH0)+oQ*fql z@&+5#fQ+2NWXWrk>Jgf>v=?JQeqOAV1^hQse8oXfE{mhAtfrZU$}tDtG;i23B!RYY zi1sxW7<$J~?h1y;1)gEY*qwnMr0wIW^8Ge@P*Augq@zH0P|_B_zn8>`HEQRD#;XHkZCo?CxsAY! z*}?bg+}O7SxHP=E7WXpp+?#rlutW+J#TMK_k)h0De4mHD)qCH>VK;c8xSQW^ix?`# zPqEq_sKK>sfoTm&=R2NVEbPv{#Igyu0Bz*0tH|jv`T6JG)maI{;j8DiqpsN0oi(ko zWSGJgB^pHy+7XK@(Vkv|-%JzuQ2`j~2J+}a3r7^(`MZX)<@|jloK)u-LNQLPNLdUl zh+)!w_8?|O#qZvQa|8SnxD}!kw|d>==?*b3c|mtay(UNvE$w3;as=xVL@lK*YA7k^Xo=@g#JXfEnsE+0HTXcJ zVyG2F|H0YB>T{Wt>f`gT+qay`LkY>6_l6?vKdio|$twgMaBjK|L)!w(&n_cFuHeZ@ z+Y4c8K$G&qxnFz1@~GYw@ClRx+D=a~YYjw)+#xtJAv|oQQ6LfVF>2(y9nu4)Tx*S)3G8w-@f%!RWSKhLA~JP0)7lm_NOV7|7MkO6S!J5>!ZuuO6P zFRBFU>6Hu>u%55~ndE1BDaTTdHp1L4RuTp#{BL7;?{?y9-qb6PW}gQXj((+R%K~2s zUxkAYQ*TkH=K6y=(9@xA;O3+CsDj5$Z#jrD7#1$foa5ivCo%xrdQ(%TN!;Q(L7FPy z$U>i*pN$iEovs2u0|!fp*59jgleFN!D>j}4H56=J@Dr|7ONjef_ZSY$7nftvN{P0% zQv>n>UCp1>i{tp*Bh9-xW+_4qV?n(!L~}x!*-eSi!VR0{8Ab^@I2VjNpA}PT?Y?(~ zi~`IDW-D8&#QowHBsMT=Hb1=nZSdEU3y?>%f4u; - -export const UpgradeTertiaryButton = (props: UpgradeButtonProps & {className?: string}) => { - const {children, ...rest} = props; - return ( - - {children} - - - ); -}; - -const PositionedKeyVariantCircleIcon = styled(KeyVariantCircleIcon)` - position: absolute; - top: -4px; - right: -6px; - color: var(--online-indicator); -`; - -export const ButtonIcon = styled.button` - width: 28px; - height: 28px; - padding: 0; - border: none; - background: transparent; - border-radius: 4px; - color: rgba(var(--center-channel-color-rgb), 0.56); - fill: rgba(var(--center-channel-color-rgb), 0.56); - font-size: 1.6rem; - - &:hover { - background: rgba(var(--center-channel-color-rgb), 0.08); - color: rgba(var(--center-channel-color-rgb), 0.72); - fill: rgba(var(--center-channel-color-rgb), 0.72); - } - - &:active, - &--active, - &--active:hover { - background: rgba(var(--button-bg-rgb), 0.08); - color: var(--button-bg); - fill: var(--button-bg); - } - - display: flex; - align-items: center; - justify-content: center; -`; diff --git a/webapp/playbooks/src/components/assets/error_svg.tsx b/webapp/playbooks/src/components/assets/error_svg.tsx deleted file mode 100644 index 3b66e8c3bd7..00000000000 --- a/webapp/playbooks/src/components/assets/error_svg.tsx +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 192px; - height: 158px; - margin-right: 54px; - margin-left: 54px; -`; - -const ErrorSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default ErrorSvg; diff --git a/webapp/playbooks/src/components/assets/files_overlay.png b/webapp/playbooks/src/components/assets/files_overlay.png deleted file mode 100644 index b74e4f4519a9c8150c61e97d9071d6328bdd208c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4059 zcmX|Ec{r4B7oK;FW*A}^k_s6iB@|h*3?WO&lq4!5{bVakC0jF08l{lkSjJ8fW9&-} zS+lQMvV0|Lwyas2@A)p@^?R;sp7*@>Ip;q2IcNTPgH25?aB~WC!eB6NLj&C_;K>9( zClnjF1qc0vU@-Wq=|wX=@Va+cELMAaJFp=L>gecbXlMX-YildG7Z(?|wzj^1|GpQw zy}b>B|Ni~Ev$M0lzTVc>wuggzcXt=y;D8w5N={DRbLr^l?BR2Bb1N$=0Bmk<2AqHa zq=0*4V`J|X*wfR~U%q@PD=Qlr8QCiYyx^>@tpPnPEiIXunE(g;00f1)y1D?qCm5sv zW)J{M0Pp<#Jix1~tMl^mHa9m*N=kZrdqE>hOG|qS0mV1Pl5j*c=I48TLBQfV|Ar~f z$f&EUd;k7D0737AgM$eP2|yLtCa4GoGBq^?sspLP!NDmhDNRjHOePad4=4mJfKE$G zOOG5m(%sz+^n{0p4-XFm5|9Wafl+|!U{GM8U>aj%W1s}c0>c5pds_zSAQub-tNY;A4L&d&ZX2n;ZJ5KktPm6VkJ z#ee?%37P`?0EUoVApZXS`ynAA?(XjIRxC$A=8oqTldCWY z&LXn0vjR~pgn$+X$#sbldRLK?gLj`_-izx}2s9tp`|Lwv*o2}Y+Cy!9k4#3W#1K7N z)tSL~c%b0;NlQkK1vjHePw+gWxGaQG4i-|xDt>&-oKeisVw9HbPby=SF&L#)<+4SF zdLoRJNE-)MK98o)K^E&wFzcp?JeE;(*AM)kON)y@GUsJr76XH^UxLK~bo}vqz(WSB zyj+*TDtcq5B3Mo?tGr$=<9NH+`$|Q0VR0z&t$Ot*CbJ;S#ZW^@K~9z)?RiB-MvCfn z?VNxzb@_p$yAhit2MG4KZn)>nYXKIT!uKU@&yNp{}->7u4Gu=zmh-aQ(8z zOycqW9G77i$KjGZr394=r~7^1?!i*=*|M_LBRmnXu)(f_!}z*q7L$G~WkCB7Ql?8L+2Nevv!nvd-`6MEi`@ z;>+Ub%znVE5AFj|*b9{n?kSX#Ih zNff>A|J-YpFYgA@Vl2!rz}CFTaA|Y;5Va&bU)BmaFe;tt&V8oe?votswC!Au(Ya{o zamE9OQ2K)TIRu3NonyJZdk*;`8uq4tN&>nSLKjS=z-%v%rW(Bt?Z^{l7nE=JYW&WR z?@6O(TONau3G%mJe+VW{y`45B4ciJ4gnXN z35+6!=4b4;u>0y)v2CQiTGwG+izujb-I7r!X;f@jy~`jjY9%dJaxl3n=cY#Z9b7N3 z%NawTa7l}$98ASZ!yO~-5X=q-nu+D#a*|Sp+aXxB@Q2IEstn(&heA`AD&}rIiD|8P zV38*;I-rTQupf{?KkBGhRC(C=0xx@!TTOptWZT+>yshYQUPm*Xr|QunTJ=+cYB2e{ zrmfnM3~$t5AI3*G!)kak7p{yV)hQ;MR3_O~@Oyfl>Kt6NDzzfj=Xmb(@&3`m6sReE z>$WECccFtW67Vs3OXGl0m_*Ksx9!6wLmP9)GSw3}+f$-0ka8~HlTqEk)n3XG1!Syc%RIJVa|xO6{8lFS@mU~CoQ zt8!U&d3z|{~IEt4;gKGRJ=Sq1T*~tFTBg5|4K;|ZS2FrQz(7d)MCOGl3 zR;q|@ucTy(BvhAljfOeVXqmuH$@d?A=U;97Hr*v~Kb&E}wCOvXj0qm0qXteJ!u6?} z2*<8h6c7AQS+j%wEw7oe5-!#LY3cMnZJTfzCQ35WT!YV^YT)~OPUAYF_beUh$g3Z9 z&lKZhdLEM?cXaL8*r^=PCx*h$R8c33P^J>@CE)`rT-9v)sC6A~&g?62ZiNyn{kmWK zrATAQeMn_PNi`RC$-@hEr2Ewx6pMZBqc4*E$wOqp0Kf5zTuOJn&()u{&72Th>N|Vi zCg$$s+3jr@zm2bI)`1n~;6{WuXPnHM|I{rL>C`u(6zO~2j9=`o5h_$?_WiL4aS!Gf zX?@c2&fmCF;SI=hj*m;Z)@;JO# zQbZHJZt9A$f&wVgjgXB>qMDO<-3rIseWp`%4G$_ zlDXQWCKDTcVWCSw2_Av5*Ai7AK<)H3Pj&)I-x;ZU+z+it>;P zUTO>SGcDwGFlBrJ4`{-7GKlvcU;k?qqSNEL$gWQ~ZxXv0bj0eXYJ`9T=i%9mfX$b0tH%g zu5}_4q9#$MREr-*Vjo50o4_b>ckkEd zk4xdNwtJ9ntIqknS`!I3DhPhWL&c3Nc1DHc(6B=P@Squ?H4{9Rsl#`Hw8H4e88F>w z#;MQcnMM6Vu)94ucjVZeo9s|Rp}UTnx<`;|7}+91B~8XHiv6V!)R63mXuZzM?J4!H z#K9!kRW)wa@Ik}K;H$|&G@<~N@ae?0cp7Pery!N~+~y~Q_x zDQhM8FV=%|ozJ^;3V*MBmQCVFKSQbaHOsZk*kyXHXo)~CNG4%~3++b<`?#N(EzNSY zbscN5BxGGPM387c9=t+XnX4#|3{7tH?-UDDzJT$xVc%sLbz37WJs@E%?n$?*1>A&n z*(`&jk#uVOfOeKWmRno|ozP{Hf;Sio3>$gnH^7y^-HStgWxG`-prj))K6q@R;?{p-b?jpaBQWmaY*N`z%U?9^p;q*kBteC+H?O z!uelxAFOz+1MT9nQ_Fo6%wxR~S_aqs;Yx84OX_v#juXcuUj9r+@unEmk(N2uQ2V(( z*PeE2tl7(%O7OBV%kbCYwb^b}BNW9I)PiNgc@?Hqnla9__pe~f=Drq^!AbB*(hBmD zJD2eEtTZ0=pR+G@qYOV7h1#&)y2VbRjZw&`kXL=)2+?U_Jd;wlP_`m*VTtrX8zJQT z)~*x3VPuCsNfWtqDBvo$EK^D~fXDOoiT^)kmPC>#a$RgEoN?%zg>x8Rnv)r%*?jGN z@0fBS)1E5RhOE#R@OmRM@3x7bGQm6i`Ogw7WVjfDD}XNv0SnU8kC}CkIPXg5UfNg? zF4cJW^+bf^0^PaM@8=(Fp?bskdp`H$>j*-d<=a7aR>Lgo{QSA%4%z9)_>Cv~hBV~q zzohgD<^*hwN*k}H&JE$uex}z1gLYfviMO}MxLQBv;%@Uy`wxk7aWfyg|Dz}UgSXbp zdJBcliR?Bq4DR|U_}vfjPh_s-Mu|@DU#vNH07S^5a;&H!nVPUA`d9~J4<;nS-%d+dF zcj&Q=Pm4ama(tzkDilFa$K21TiWBdh_zx#1Bpnf{E&p7@wqa|kKhH@}l`c%f>O%~o zXhPEA{N09GqZke1RIoTaopyEnQRj`@vqlLWHxmp$r!&=Kdecf4*FSec>}3asV;8j1Y6AU7_i1;Q@6ew{i5#(iTduxV$3AP4Z$DI$ zZ8Ll?e~QSZoRdkIP#ut|9@=?2ID>!|cj#|%#%9+#k4v=ri@s^NVa`m6`Q7;=zMMBG zW3D2Is3prrj+O1iRDO(v)WnXaCFWg!$g5tp>2s{{)HF)d_uI$za3Aq7Vkhg09MD$wx2cPsFkxssC2`r_xzxG<5rIl`NN0(o1==tlp}f zRt;g|>+q`(xu+k?zM#f&#i=x(ib@Zji8&=4V|Fd9O27FkM)>9Q(^yyD@B@RNcRt$g zRuj}O6mV9=AU{7p6xXk9<|EQXt;LV2-+@NR;@_KJ%KTQ)4Nz|iyY7S730RmHA6~`I z8W>5%hkH6U$yvURV|4e zsG9i`&RZgp<`&%BO;y&=;!}Hj$C|u3Hqqzl58cgK73jjAH2L3mj-j53?gt&4fd2yd CUh;zg diff --git a/webapp/playbooks/src/components/assets/icons/clear_icon.tsx b/webapp/playbooks/src/components/assets/icons/clear_icon.tsx deleted file mode 100644 index c97e42b0973..00000000000 --- a/webapp/playbooks/src/components/assets/icons/clear_icon.tsx +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import styled from 'styled-components'; - -export default styled.i.attrs(() => ({className: 'icon icon-close-circle'}))` - color: rgba(var(--center-channel-color-rgb), 0.56); -`; diff --git a/webapp/playbooks/src/components/assets/icons/clipboards_checkmark.tsx b/webapp/playbooks/src/components/assets/icons/clipboards_checkmark.tsx deleted file mode 100644 index fe13dedf13a..00000000000 --- a/webapp/playbooks/src/components/assets/icons/clipboards_checkmark.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 26px; - height: 30px; - color: var(--button-bg) -`; - -const ClipboardsCheckmark = (props: {className?: string}) => ( - - - -); - -export default ClipboardsCheckmark; diff --git a/webapp/playbooks/src/components/assets/icons/clipboards_play.tsx b/webapp/playbooks/src/components/assets/icons/clipboards_play.tsx deleted file mode 100644 index ad3ae64377e..00000000000 --- a/webapp/playbooks/src/components/assets/icons/clipboards_play.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 26px; - height: 30px; - color: var(--button-bg) -`; - -const ClipboardsPlay = (props: {className?: string}) => ( - - - -); - -export default ClipboardsPlay; diff --git a/webapp/playbooks/src/components/assets/icons/clock.tsx b/webapp/playbooks/src/components/assets/icons/clock.tsx deleted file mode 100644 index a6babd5bbb7..00000000000 --- a/webapp/playbooks/src/components/assets/icons/clock.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 34px; - height: 34px; - - color: var(--center-channel-color); -`; - -const Clock = (props : {className?: string}) => ( - - - -); - -export default Clock; diff --git a/webapp/playbooks/src/components/assets/icons/exclamation.tsx b/webapp/playbooks/src/components/assets/icons/exclamation.tsx deleted file mode 100644 index 1bb97e02ea2..00000000000 --- a/webapp/playbooks/src/components/assets/icons/exclamation.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 34px; - height: 34px; - - color: var(--dnd-indicator); -`; - -const Exclamation = (props : {className?: string}) => ( - - - - - -); - -export default Exclamation; diff --git a/webapp/playbooks/src/components/assets/icons/external_link.tsx b/webapp/playbooks/src/components/assets/icons/external_link.tsx deleted file mode 100644 index e56cb5d754e..00000000000 --- a/webapp/playbooks/src/components/assets/icons/external_link.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 14px; - height: 14px; -`; - -const ExternalLink = (props : {className?: string}) => ( - - - -); - -export default ExternalLink; - diff --git a/webapp/playbooks/src/components/assets/icons/left_chevron.tsx b/webapp/playbooks/src/components/assets/icons/left_chevron.tsx deleted file mode 100644 index a316b0a1f7b..00000000000 --- a/webapp/playbooks/src/components/assets/icons/left_chevron.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -const LeftChevron = () => ( - - - -); - -export default LeftChevron; diff --git a/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx b/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx deleted file mode 100644 index c5866d61d66..00000000000 --- a/webapp/playbooks/src/components/assets/icons/playbooks_product_icon.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; -import styled from 'styled-components'; - -interface Props { - id?: string; -} - -const Icon = styled.i` - font-size: 22px; -`; - -const PlaybooksProductIcon = React.forwardRef((props: Props, forwardedRef) => ( - -)); - -export default PlaybooksProductIcon; diff --git a/webapp/playbooks/src/components/assets/icons/post_menu_icon.tsx b/webapp/playbooks/src/components/assets/icons/post_menu_icon.tsx deleted file mode 100644 index 2f0f3ac3bb9..00000000000 --- a/webapp/playbooks/src/components/assets/icons/post_menu_icon.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; - -import PlaybooksProductIcon from './playbooks_product_icon'; - -const PlaybookRunPostMenuIcon = () => ( - - - -); - -export default PlaybookRunPostMenuIcon; diff --git a/webapp/playbooks/src/components/assets/icons/profiles.tsx b/webapp/playbooks/src/components/assets/icons/profiles.tsx deleted file mode 100644 index 7a60ad6f9f1..00000000000 --- a/webapp/playbooks/src/components/assets/icons/profiles.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 28px; - height: 38px; - color: var(--button-bg) -`; - -const Profiles = (props: {className?: string}) => ( - - - -); - -export default Profiles; diff --git a/webapp/playbooks/src/components/assets/icons/three_dots_icon.tsx b/webapp/playbooks/src/components/assets/icons/three_dots_icon.tsx deleted file mode 100644 index 0ce050a1305..00000000000 --- a/webapp/playbooks/src/components/assets/icons/three_dots_icon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -const ThreeDotsIcon = (props: React.PropsWithoutRef): JSX.Element => ( - -); - -export const HamburgerButton = styled(ThreeDotsIcon)` - font-size: 24px; - position: relative; -`; diff --git a/webapp/playbooks/src/components/assets/icons/warning_icon.tsx b/webapp/playbooks/src/components/assets/icons/warning_icon.tsx deleted file mode 100644 index 58e1fb1f2c9..00000000000 --- a/webapp/playbooks/src/components/assets/icons/warning_icon.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -export default function WarningIcon() { - return ( - - ); -} diff --git a/webapp/playbooks/src/components/assets/illustrations/bug_search_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/bug_search_svg.tsx deleted file mode 100644 index b173c4c8a8a..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/bug_search_svg.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const BugSearch = () => ( - - - - - - - - - -); - -export default BugSearch; diff --git a/webapp/playbooks/src/components/assets/illustrations/clipboard_checklist_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/clipboard_checklist_svg.tsx deleted file mode 100644 index 478a661ae58..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/clipboard_checklist_svg.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const ClipboardChecklist = (props: {className?: string}) => ( - - - - - - - - - - - - - - - -); - -export default ClipboardChecklist; diff --git a/webapp/playbooks/src/components/assets/illustrations/dumpster_fire_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/dumpster_fire_svg.tsx deleted file mode 100644 index ea4c5dee9a1..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/dumpster_fire_svg.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const DumpsterFire = () => ( - - - - - - - - - - - - - - - - - -); - -export default DumpsterFire; diff --git a/webapp/playbooks/src/components/assets/illustrations/gears_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/gears_svg.tsx deleted file mode 100644 index be738108765..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/gears_svg.tsx +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const Gears = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default Gears; diff --git a/webapp/playbooks/src/components/assets/illustrations/handshake_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/handshake_svg.tsx deleted file mode 100644 index ded7bb8ce86..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/handshake_svg.tsx +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const Handshake = () => ( - - - - - - - - - - - - - - - -); - -export default Handshake; diff --git a/webapp/playbooks/src/components/assets/illustrations/light_bulb_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/light_bulb_svg.tsx deleted file mode 100644 index dff13566fe2..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/light_bulb_svg.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const LightBulb = () => ( - - - - - - - - - - - - - - -); - -export default LightBulb; diff --git a/webapp/playbooks/src/components/assets/illustrations/rocket_man_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/rocket_man_svg.tsx deleted file mode 100644 index c6f0c98b47f..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/rocket_man_svg.tsx +++ /dev/null @@ -1,440 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const RocketMan = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default RocketMan; diff --git a/webapp/playbooks/src/components/assets/illustrations/rocket_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/rocket_svg.tsx deleted file mode 100644 index 81b1d2866df..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/rocket_svg.tsx +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const Rocket = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default Rocket; diff --git a/webapp/playbooks/src/components/assets/illustrations/search_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/search_svg.tsx deleted file mode 100644 index 87b723887aa..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/search_svg.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const Search = () => ( - - - - - - - - -); - -export default Search; diff --git a/webapp/playbooks/src/components/assets/illustrations/smiley_sunglasses_svg.tsx b/webapp/playbooks/src/components/assets/illustrations/smiley_sunglasses_svg.tsx deleted file mode 100644 index 66418d0636e..00000000000 --- a/webapp/playbooks/src/components/assets/illustrations/smiley_sunglasses_svg.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from 'src/components/assets/svg'; - -const SmileySunglasses = () => ( - - - - - - - - - - - - - - - - - - - - - -); - -export default SmileySunglasses; diff --git a/webapp/playbooks/src/components/assets/inputs.tsx b/webapp/playbooks/src/components/assets/inputs.tsx deleted file mode 100644 index 44b4267ca13..00000000000 --- a/webapp/playbooks/src/components/assets/inputs.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import styled from 'styled-components'; - -export const BaseInput = styled.input<{invalid?: boolean}>` - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - background-color: rgb(var(--center-channel-bg-rgb)); - border: none; - box-shadow: ${(props) => (props.invalid ? 'inset 0 0 0 2px var(--error-text)' : 'inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16)')}; - border-radius: 4px; - height: 40px; - line-height: 40px; - padding: 0 16px; - font-size: 14px; - - &:focus { - box-shadow: ${(props) => (props.invalid ? 'inset 0 0 0 2px var(--error-text)' : 'inset 0 0 0 2px var(--button-bg)')}; - } -`; - -export const BaseTextArea = styled.textarea` - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - background-color: rgb(var(--center-channel-bg-rgb)); - border: none; - box-shadow: inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16); - border-radius: 4px; - font-size: 14px; - line-height: 20px; - padding: 8px 16px; - - &:focus { - box-shadow: inset 0 0 0 2px var(--button-bg); - } -`; - -interface InputTrashIconProps { - show: boolean; -} - -export const InputTrashIcon = styled.span` - visibility: ${(props) => (props.show ? 'visible' : 'hidden')}; - cursor: pointer; - position: absolute; - top: 0px; - right: 5px; - color: rgba(var(--center-channel-color-rgb), 0.56); - - &:hover { - color: var(--center-channel-color); - } -`; diff --git a/webapp/playbooks/src/components/assets/loading_spinner.tsx b/webapp/playbooks/src/components/assets/loading_spinner.tsx deleted file mode 100644 index 422e2643b21..00000000000 --- a/webapp/playbooks/src/components/assets/loading_spinner.tsx +++ /dev/null @@ -1,79 +0,0 @@ - -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -const LoadingSpinner = (props: {className?: string}) => ( - - - - - - - - - - - - - - - - - - - - -); - -export default LoadingSpinner; diff --git a/webapp/playbooks/src/components/assets/mattermost_logo_svg.tsx b/webapp/playbooks/src/components/assets/mattermost_logo_svg.tsx deleted file mode 100644 index f416eb49ae9..00000000000 --- a/webapp/playbooks/src/components/assets/mattermost_logo_svg.tsx +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import Svg from './svg'; - -const defaultFill = '#818698'; - -const MattermostLogo = (props: {className?: string, fill?: string; width?: string | number; height?: string | number;}) => ( - - - - - - -); - -export default MattermostLogo; diff --git a/webapp/playbooks/src/components/assets/no_content_playbook_runs_svg.tsx b/webapp/playbooks/src/components/assets/no_content_playbook_runs_svg.tsx deleted file mode 100644 index dcf99ae3b71..00000000000 --- a/webapp/playbooks/src/components/assets/no_content_playbook_runs_svg.tsx +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; - -const NoContentPlaybookRunsSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default NoContentPlaybookRunsSvg; diff --git a/webapp/playbooks/src/components/assets/no_metrics_svg.tsx b/webapp/playbooks/src/components/assets/no_metrics_svg.tsx deleted file mode 100644 index 50b7855797a..00000000000 --- a/webapp/playbooks/src/components/assets/no_metrics_svg.tsx +++ /dev/null @@ -1,309 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 229px; - height: 220px; -`; - -const NoMetricsSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default NoMetricsSvg; diff --git a/webapp/playbooks/src/components/assets/page_run_collaboration_svg.tsx b/webapp/playbooks/src/components/assets/page_run_collaboration_svg.tsx deleted file mode 100644 index 35b8ed15448..00000000000 --- a/webapp/playbooks/src/components/assets/page_run_collaboration_svg.tsx +++ /dev/null @@ -1,321 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; -import styled from 'styled-components'; - -const Svg = styled.svg` - flex-shrink: 0; -`; - -const PageRunCollaborationSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default PageRunCollaborationSvg; diff --git a/webapp/playbooks/src/components/assets/success_svg.tsx b/webapp/playbooks/src/components/assets/success_svg.tsx deleted file mode 100644 index e48b3a7d5b8..00000000000 --- a/webapp/playbooks/src/components/assets/success_svg.tsx +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 256px; - height: 156px; - margin-right: 54px; - margin-left: 54px; -`; - -const SuccessSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default SuccessSvg; diff --git a/webapp/playbooks/src/components/assets/svg.tsx b/webapp/playbooks/src/components/assets/svg.tsx deleted file mode 100644 index 8430fb0b3ee..00000000000 --- a/webapp/playbooks/src/components/assets/svg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import styled from 'styled-components'; - -// Hat-tip: https://www.pinkdroids.com/blog/svg-react-styled-components/ -const Svg = styled.svg.attrs({ - version: '1.1', - xmlns: 'http://www.w3.org/2000/svg', - xmlnsXlink: 'http://www.w3.org/1999/xlink', -})``; - -export default Svg; diff --git a/webapp/playbooks/src/components/assets/upgrade_error_illustration_svg.tsx b/webapp/playbooks/src/components/assets/upgrade_error_illustration_svg.tsx deleted file mode 100644 index 745ffa18efe..00000000000 --- a/webapp/playbooks/src/components/assets/upgrade_error_illustration_svg.tsx +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; - -const UpgradeErrorIllustrationSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default UpgradeErrorIllustrationSvg; diff --git a/webapp/playbooks/src/components/assets/upgrade_illustration_svg.tsx b/webapp/playbooks/src/components/assets/upgrade_illustration_svg.tsx deleted file mode 100644 index 22bf4d939ce..00000000000 --- a/webapp/playbooks/src/components/assets/upgrade_illustration_svg.tsx +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 223px; - height: 178px; -`; - -const UpgradeIllustrationSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default UpgradeIllustrationSvg; diff --git a/webapp/playbooks/src/components/assets/upgrade_key_metrics_background_svg.tsx b/webapp/playbooks/src/components/assets/upgrade_key_metrics_background_svg.tsx deleted file mode 100644 index 9aa788c9681..00000000000 --- a/webapp/playbooks/src/components/assets/upgrade_key_metrics_background_svg.tsx +++ /dev/null @@ -1,1240 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -const UpgradeKeyMetricsBackgroundSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default UpgradeKeyMetricsBackgroundSvg; diff --git a/webapp/playbooks/src/components/assets/upgrade_playbook_background_svg.tsx b/webapp/playbooks/src/components/assets/upgrade_playbook_background_svg.tsx deleted file mode 100644 index cba2b20f7b6..00000000000 --- a/webapp/playbooks/src/components/assets/upgrade_playbook_background_svg.tsx +++ /dev/null @@ -1,862 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import Icon from 'src/components/assets/svg'; - -const Svg = styled(Icon)` - width: 1086px; - height: 261px; -`; - -const UpgradePlaybookBackgroundSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default UpgradePlaybookBackgroundSvg; diff --git a/webapp/playbooks/src/components/assets/upgrade_success_illustration_svg.tsx b/webapp/playbooks/src/components/assets/upgrade_success_illustration_svg.tsx deleted file mode 100644 index 34c083810c5..00000000000 --- a/webapp/playbooks/src/components/assets/upgrade_success_illustration_svg.tsx +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License for license information. - -import React from 'react'; - -const UpgradeSuccessIllustrationSvg = () => ( - - - - - - - - - - - - - - - - - - - - - - - - -); - -export default UpgradeSuccessIllustrationSvg; diff --git a/webapp/playbooks/src/components/backstage/archive_playbook_modal.tsx b/webapp/playbooks/src/components/backstage/archive_playbook_modal.tsx deleted file mode 100644 index 68df51eeacd..00000000000 --- a/webapp/playbooks/src/components/backstage/archive_playbook_modal.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, {useState} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; - -import {Banner} from 'src/components/backstage/styles'; -import {Playbook} from 'src/types/playbook'; - -import ConfirmModal from 'src/components/widgets/confirmation_modal'; - -import {useLHSRefresh} from './lhs_navigation'; - -const ArchiveBannerTimeout = 5000; - -interface ArchiveModalParams { - id: string - title: string -} - -const useConfirmPlaybookArchiveModal = (archivePlaybook: (id: Playbook['id']) => void): [React.ReactNode, (pb: ArchiveModalParams) => void] => { - const {formatMessage} = useIntl(); - const [open, setOpen] = useState(false); - const [showBanner, setShowBanner] = useState(false); - const [playbook, setPlaybook] = useState(null); - const refreshLHS = useLHSRefresh(); - - const openModal = (playbookToOpenWith: ArchiveModalParams) => { - setPlaybook(playbookToOpenWith); - setOpen(true); - }; - - const onArchive = async () => { - if (playbook) { - await archivePlaybook(playbook.id); - refreshLHS(); - - setOpen(false); - setShowBanner(true); - - window.setTimeout(() => { - setShowBanner(false); - }, ArchiveBannerTimeout); - } - }; - - const modal = ( - <> - setOpen(false)} - /> - {showBanner && - - - - - } - - ); - - return [modal, openModal]; -}; - -export default useConfirmPlaybookArchiveModal; diff --git a/webapp/playbooks/src/components/backstage/backstage.tsx b/webapp/playbooks/src/components/backstage/backstage.tsx deleted file mode 100644 index 347b2161b56..00000000000 --- a/webapp/playbooks/src/components/backstage/backstage.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useEffect} from 'react'; -import {matchPath, useLocation, useRouteMatch} from 'react-router-dom'; -import {useSelector} from 'react-redux'; -import styled, {css} from 'styled-components'; -import {GlobalState} from '@mattermost/types/store'; -import {Theme, getTheme} from 'mattermost-redux/selectors/entities/preferences'; - -import {useForceDocumentTitle} from 'src/hooks'; -import CloudModal from 'src/components/cloud_modal'; -import {applyTheme} from 'src/components/backstage/css_utils'; - -import BackstageRHS from 'src/components/backstage/rhs/rhs'; - -import {ToastProvider} from './toast_banner'; -import LHSNavigation from './lhs_navigation'; -import MainBody from './main_body'; - -const BackstageContainer = styled.div` - background: var(--center-channel-bg); - overflow-y: auto; - height: 100%; -`; - -const Backstage = () => { - const {pathname} = useLocation(); - - const {url} = useRouteMatch(); - const noContainerScroll = matchPath<{playbookRunId?: string; playbookId?: string;}>(pathname, { - path: [`${url}/runs/:playbookRunId`, `${url}/playbooks`], - }); - - const currentTheme = useSelector(getTheme); - useEffect(() => { - // This class, critical for all the styling to work, is added by ChannelController, - // which is not loaded when rendering this root component. - document.body.classList.add('app__body'); - const root = document.getElementById('root'); - if (root) { - root.className += ' channel-view'; - } - - applyTheme(currentTheme); - return function cleanUp() { - document.body.classList.remove('app__body'); - }; - }, [currentTheme]); - - useForceDocumentTitle('Playbooks'); - - return ( - - - - - - - - - - - ); -}; - -const MainContainer = styled.div<{noContainerScroll: boolean}>` - display: grid; - grid-auto-flow: column; - grid-template-columns: max-content auto; - ${({noContainerScroll}) => (noContainerScroll ? css` - height: 100%; - ` : css` - min-height: 100%; - `)} -`; - -export const BackstageID = 'playbooks-backstageRoot'; - -export default Backstage; - diff --git a/webapp/playbooks/src/components/backstage/backstage_list_header.tsx b/webapp/playbooks/src/components/backstage/backstage_list_header.tsx deleted file mode 100644 index 001b2e851eb..00000000000 --- a/webapp/playbooks/src/components/backstage/backstage_list_header.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import styled, {css} from 'styled-components'; - -const BackstageListHeader = styled.div<{$edgeless?: boolean}>` - font-weight: 600; - padding: 0 1.6rem; - font-size: 14px; - line-height: 4rem; - color: var(--center-channel-color); - border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); - border-top-color: rgba(var(--center-channel-color-rgb), 0.16); - background-color: rgba(var(--center-channel-color-rgb), 0.04); - border-radius: 4px; - ${({$edgeless}) => $edgeless && css` - border-width: 1px 0; - border-radius: 0; - `} -`; - -export default BackstageListHeader; diff --git a/webapp/playbooks/src/components/backstage/bar_graph.tsx b/webapp/playbooks/src/components/backstage/bar_graph.tsx deleted file mode 100644 index f59db48956e..00000000000 --- a/webapp/playbooks/src/components/backstage/bar_graph.tsx +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Bar} from 'react-chartjs-2'; -import 'chartjs-plugin-annotation'; -import styled from 'styled-components'; - -import {NullNumber} from 'src/types/stats'; - -const GraphBoxContainer = styled.div` - padding: 10px; -`; - -interface BarGraphProps { - title: string; - xlabel?: string; - data?: NullNumber[]; - labels?: string[]; - className?: string; - color?: string; - tooltipTitleCallback?: (xLabel: string) => string; - tooltipLabelCallback?: (yLabel: number) => string; - onClick?: (index: number) => void; - yAxesTicksCallback?: (val: number, index: number) => string; - xAxesTicksCallback?: (val: number, index: number) => string; - options?: any; -} - -const BarGraph = (props: BarGraphProps) => { - const style = getComputedStyle(document.body); - const centerChannelFontColor = style.getPropertyValue('--center-channel-color'); - const colorName = props.color ? props.color : '--button-bg'; - const color = style.getPropertyValue(colorName); - - return ( - - { - return (val % 1 === 0) ? val : null; - }, - beginAtZero: true, - color: centerChannelFontColor, - }, - }, - x: { - scaleLabel: { - display: Boolean(props.xlabel), - text: props.xlabel, - color: centerChannelFontColor, - }, - ticks: { - callback: props.xAxesTicksCallback ? props.xAxesTicksCallback : (val: any, index: number) => { - return (index % 2) === 0 ? val : ''; - }, - color: centerChannelFontColor, - maxRotation: 0, - }, - }, - }, - onClick(event: any, element: any) { - if (!props.onClick) { - return; - } else if (element.length === 0) { - props.onClick(-1); - return; - } - // eslint-disable-next-line no-underscore-dangle - props.onClick(element[0]._index); - }, - onHover(event: any) { - if (props.onClick) { - event.native.target.style.cursor = 'pointer'; - } - }, - maintainAspectRatio: false, - responsive: true, - ...props.options, - }} - data={{ - labels: props.labels, - datasets: [{ - backgroundColor: color, - borderColor: color, - - // pointBackgroundColor: color, - // pointBorderColor: '#fff', - // pointHoverBackgroundColor: '#fff', - // pointHoverBorderColor: color, - - // This is okay, it can take nulls and numbers - data: props.data as number[], - }], - }} - /> - - ); -}; - -export default BarGraph; diff --git a/webapp/playbooks/src/components/backstage/category_selector.tsx b/webapp/playbooks/src/components/backstage/category_selector.tsx deleted file mode 100644 index c40a5bb7c94..00000000000 --- a/webapp/playbooks/src/components/backstage/category_selector.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import {SelectComponentsConfig, components as defaultComponents} from 'react-select'; -import {useSelector} from 'react-redux'; -import {makeGetCategoriesForTeam} from 'mattermost-redux/selectors/entities/channel_categories'; - -import {ChannelCategory} from '@mattermost/types/channel_categories'; -import {GlobalState} from '@mattermost/types/store'; - -import {StyledCreatable} from './styles'; - -export interface Props { - id?: string; - onCategorySelected: (categoryName: string) => void; - categoryName?: string; - isClearable?: boolean; - selectComponents?: SelectComponentsConfig; - isDisabled: boolean; - captureMenuScroll: boolean; - shouldRenderValue: boolean; - placeholder: string; - menuPlacement?: string; -} - -const getCategoriesForTeam = makeGetCategoriesForTeam(); -const getMyCategories = (state: GlobalState) => getCategoriesForTeam(state, state.entities.teams.currentTeamId); - -const CategorySelector = (props: Props & { className?: string }) => { - const selectableCategories = useSelector(getMyCategories); - - const options = React.useMemo(() => { - return selectableCategories - .filter((category) => category.type !== 'direct_messages' && category.type !== 'channels') - .map((category) => ({value: category.display_name, label: category.display_name})); - }, [selectableCategories]); - - const onChange = (option: {label: string; value: string}, {action}: {action: string}) => { - if (action === 'clear') { - props.onCategorySelected(''); - } else { - props.onCategorySelected(option.value); - } - }; - - const components = props.selectComponents || defaultComponents; - - return ( - - ); -}; - -export default CategorySelector; diff --git a/webapp/playbooks/src/components/backstage/channel_selector.tsx b/webapp/playbooks/src/components/backstage/channel_selector.tsx deleted file mode 100644 index f223f9eeba3..00000000000 --- a/webapp/playbooks/src/components/backstage/channel_selector.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import React, {useEffect} from 'react'; -import {SelectComponentsConfig, components as defaultComponents} from 'react-select'; -import {useDispatch, useSelector} from 'react-redux'; -import {createSelector} from 'reselect'; -import styled from 'styled-components'; - -import {getAllChannels, getChannelsInTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels'; -import {IDMappedObjects, RelationOneToMany, RelationOneToOne} from '@mattermost/types/utilities'; -import {GlobeIcon, LockIcon} from '@mattermost/compass-icons/components'; -import General from 'mattermost-redux/constants/general'; -import {Channel, ChannelMembership} from '@mattermost/types/channels'; -import {Team} from '@mattermost/types/teams'; -import {fetchMyChannelsAndMembersREST, getChannel} from 'mattermost-redux/actions/channels'; - -import {useIntl} from 'react-intl'; - -import {StyledSelect} from './styles'; - -export interface Props { - id?: string; - onChannelsSelected?: (channelIds: string[]) => void; // if isMulti=true - onChannelSelected?: (channelId: string, channelName: string) => void; // if isMulti=false - channelIds: string[]; - isClearable?: boolean; - selectComponents?: SelectComponentsConfig; - isDisabled: boolean; - captureMenuScroll: boolean; - shouldRenderValue: boolean; - placeholder?: string; - teamId: string; - isMulti: boolean; -} - -const getAllPublicChannelsInTeam = (teamId: string) => createSelector( - getAllChannels, - getChannelsInTeam, - (allChannels: IDMappedObjects, channelsByTeam: RelationOneToMany): Channel[] => { - const publicChannels : Channel[] = []; - (channelsByTeam[teamId] || []).forEach((channelId: string) => { - const channel = allChannels[channelId]; - if (channel.type === General.OPEN_CHANNEL && channel.delete_at === 0) { - publicChannels.push(channel); - } - }); - return publicChannels; - }, -); - -const getMyPublicAndPrivateChannelsInTeam = (teamId: string) => createSelector( - getAllChannels, - getChannelsInTeam, - getMyChannelMemberships, - (allChannels: IDMappedObjects, channelsByTeam: RelationOneToMany, myMembers: RelationOneToOne): Channel[] => { - const myChannels : Channel[] = []; - (channelsByTeam[teamId] || []).forEach((channelId: string) => { - if (Object.prototype.hasOwnProperty.call(myMembers, channelId)) { - const channel = allChannels[channelId]; - if (channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL && channel.delete_at === 0) { - myChannels.push(channel); - } - } - }); - return myChannels; - }, -); - -const filterChannels = (channelIDs: string[], channels: Channel[]): Channel[] => { - if (!channelIDs || !channels) { - return []; - } - - const channelsMap = new Map(); - channels.forEach((channel: Channel) => channelsMap.set(channel.id, channel)); - - const result: Channel[] = []; - channelIDs.forEach((id: string) => { - let filteredChannel: Channel; - const channel = channelsMap.get(id); - if (channel && channel.delete_at === 0) { - filteredChannel = channel; - } else { - filteredChannel = {display_name: '', id} as Channel; - } - result.push(filteredChannel); - }); - return result; -}; - -const ChannelSelector = (props: Props & {className?: string}) => { - const dispatch = useDispatch(); - const {formatMessage} = useIntl(); - const selectableChannels = useSelector(getMyPublicAndPrivateChannelsInTeam(props.teamId)); - const allPublicChannels = useSelector(getAllPublicChannelsInTeam(props.teamId)); - - useEffect(() => { - if (props.teamId !== '' && selectableChannels.length === 0) { - dispatch(fetchMyChannelsAndMembersREST(props.teamId)); - } - }, [props.teamId]); - - useEffect(() => { - // Create a map with all channels in the store, keyed by channel ID - const channelsMap = new Map(); - [...allPublicChannels, ...selectableChannels].forEach((channel: Channel) => channelsMap.set(channel.id, channel)); - - // For all channels not in the store initially, fetch them and add them to the store - props.channelIds.forEach((channelID) => { - if (!channelsMap.has(channelID)) { - dispatch(getChannel(channelID)); - } - }); - }, []); - - const onChangeMulti = (channels: Channel[], {action}: {action: string}) => { - props.onChannelsSelected?.(action === 'clear' ? [] : channels.map((c) => c.id)); - }; - const onChange = (channel: Channel | Channel, {action}: {action: string}) => { - props.onChannelSelected?.(action === 'clear' ? '' : channel.id, action === 'clear' ? '' : channel.display_name); - }; - - const getOptionValue = (channel: Channel) => { - return channel.id; - }; - - const formatOptionLabel = (channel: Channel) => { - return ( - - - {channel.type === 'O' ? : } - - {channel.display_name || formatMessage({defaultMessage: 'Unknown Channel'})} - - ); - }; - - const filterOption = (option: {label: string, value: string, data: Channel}, term: string): boolean => { - const channel = option.data as Channel; - - if (term.trim().length === 0) { - return true; - } - - return channel.name.toLowerCase().includes(term.toLowerCase()) || - channel.display_name.toLowerCase().includes(term.toLowerCase()) || - channel.id.toLowerCase() === term.toLowerCase(); - }; - - const values = filterChannels(props.channelIds, [...allPublicChannels, ...selectableChannels]); - - const components = props.selectComponents || defaultComponents; - - return ( - - ); -}; - -export default ChannelSelector; - -const ChannelContainer = styled.div` - display: flex; - flex-direction: row; - -`; -const ChanneIcon = styled.div` - display: flex; - align-self: center; - color: rgba(var(--center-channel-color-rgb), 0.56); -`; -const ChannelDisplay = styled.div` - margin-left: 4px; - font-size: 12px; - color: var(--center-channel-color); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; diff --git a/webapp/playbooks/src/components/backstage/convert_enterprise_notice.tsx b/webapp/playbooks/src/components/backstage/convert_enterprise_notice.tsx deleted file mode 100644 index f112daeec41..00000000000 --- a/webapp/playbooks/src/components/backstage/convert_enterprise_notice.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import styled from 'styled-components'; - -const ConvertEnterpriseNotice = () => ( - <> - - - -

- -

  • - - - -
  • -
  • - -
  • - -

    - -); - -export default ConvertEnterpriseNotice; - -const StyledOl = styled.ol` - list-style-position: inside; - margin-left: -16px; -`; - -const Subheader = styled.p` - font-weight: 600; - font-size: 14px; -`; diff --git a/webapp/playbooks/src/components/backstage/convert_private_playbook_modal.tsx b/webapp/playbooks/src/components/backstage/convert_private_playbook_modal.tsx deleted file mode 100644 index 46f132fbccf..00000000000 --- a/webapp/playbooks/src/components/backstage/convert_private_playbook_modal.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React, {useState} from 'react'; -import {useIntl} from 'react-intl'; - -import ConfirmModal from 'src/components/widgets/confirmation_modal'; -import {useEditPlaybook} from 'src/hooks'; -import {PlaybookWithChecklist} from 'src/types/playbook'; - -type ConfirmPlaybookConvertPrivateReturn = [React.ReactNode, (show: boolean) => void]; -type Props = { - playbookId: string, - refetch?: () => void | undefined, - updater?: (update: Partial) => void, -} - -const useConfirmPlaybookConvertPrivateModal = ({playbookId, refetch, updater}: Props): ConfirmPlaybookConvertPrivateReturn => { - const {formatMessage} = useIntl(); - const [showMakePrivateConfirm, setShowMakePrivateConfirm] = useState(false); - const [playbook, updatePlaybook] = useEditPlaybook(playbookId, refetch); - - const modal = ( - { - if (playbookId && updater) { - updater({public: false}); - } else if (playbookId) { - updatePlaybook({public: false}); - } - setShowMakePrivateConfirm(false); - }} - onCancel={() => setShowMakePrivateConfirm(false)} - /> - ); - return [modal, setShowMakePrivateConfirm]; -}; - -export default useConfirmPlaybookConvertPrivateModal; diff --git a/webapp/playbooks/src/components/backstage/css_utils.tsx b/webapp/playbooks/src/components/backstage/css_utils.tsx deleted file mode 100644 index ca26a45af52..00000000000 --- a/webapp/playbooks/src/components/backstage/css_utils.tsx +++ /dev/null @@ -1,221 +0,0 @@ -import {changeOpacity} from 'mattermost-redux/utils/theme_utils'; -import cssVars from 'css-vars-ponyfill'; - -export function applyTheme(theme: any) { - cssVars({ - variables: { - - // RGB values derived from theme hex values i.e. '255, 255, 255' - // (do not apply opacity mutations here) - 'away-indicator-rgb': toRgbValues(theme.awayIndicator), - 'button-bg-rgb': toRgbValues(theme.buttonBg), - 'button-color-rgb': toRgbValues(theme.buttonColor), - 'center-channel-bg-rgb': toRgbValues(theme.centerChannelBg), - 'center-channel-color-rgb': toRgbValues(theme.centerChannelColor), - 'dnd-indicator-rgb': toRgbValues(theme.dndIndicator), - 'error-text-color-rgb': toRgbValues(theme.errorTextColor), - 'link-color-rgb': toRgbValues(theme.linkColor), - 'mention-bg-rgb': toRgbValues(theme.mentionBg), - 'mention-color-rgb': toRgbValues(theme.mentionColor), - 'mention-highlight-bg-rgb': toRgbValues(theme.mentionHighlightBg), - 'mention-highlight-link-rgb': toRgbValues(theme.mentionHighlightLink), - 'mention-highlight-bg-mixed-rgb': dropAlpha(blendColors(theme.centerChannelBg, theme.mentionHighlightBg, 0.5)), - 'pinned-highlight-bg-mixed-rgb': dropAlpha(blendColors(theme.centerChannelBg, theme.mentionHighlightBg, 0.24)), - 'own-highlight-bg-rgb': dropAlpha(blendColors(theme.mentionHighlightBg, theme.centerChannelColor, 0.05)), - 'new-message-separator-rgb': toRgbValues(theme.newMessageSeparator), - 'online-indicator-rgb': toRgbValues(theme.onlineIndicator), - 'sidebar-bg-rgb': toRgbValues(theme.sidebarBg), - 'sidebar-header-bg-rgb': toRgbValues(theme.sidebarHeaderBg), - 'sidebar-teambar-bg-rgb': toRgbValues(theme.sidebarTeamBarBg), - 'sidebar-header-text-color-rgb': toRgbValues(theme.sidebarHeaderTextColor), - 'sidebar-text-rgb': toRgbValues(theme.sidebarText), - 'sidebar-text-active-border-rgb': toRgbValues(theme.sidebarTextActiveBorder), - 'sidebar-text-active-color-rgb': toRgbValues(theme.sidebarTextActiveColor), - 'sidebar-text-hover-bg-rgb': toRgbValues(theme.sidebarTextHoverBg), - 'sidebar-unread-text-rgb': toRgbValues(theme.sidebarUnreadText), - - // Hex CSS variables - 'sidebar-bg': theme.sidebarBg, - 'sidebar-text': theme.sidebarText, - 'sidebar-unread-text': theme.sidebarUnreadText, - 'sidebar-text-hover-bg': theme.sidebarTextHoverBg, - 'sidebar-text-active-border': theme.sidebarTextActiveBorder, - 'sidebar-text-active-color': theme.sidebarTextActiveColor, - 'sidebar-header-bg': theme.sidebarHeaderBg, - 'sidebar-teambar-bg': theme.sidebarTeamBarBg, - 'sidebar-header-text-color': theme.sidebarHeaderTextColor, - 'online-indicator': theme.onlineIndicator, - 'away-indicator': theme.awayIndicator, - 'dnd-indicator': theme.dndIndicator, - 'mention-bg': theme.mentionBg, - 'mention-color': theme.mentionColor, - 'center-channel-bg': theme.centerChannelBg, - 'center-channel-color': theme.centerChannelColor, - 'new-message-separator': theme.newMessageSeparator, - 'link-color': theme.linkColor, - 'button-bg': theme.buttonBg, - 'button-color': theme.buttonColor, - 'error-text': theme.errorTextColor, - 'mention-highlight-bg': theme.mentionHighlightBg, - 'mention-highlight-link': theme.mentionHighlightLink, - - // Legacy variables with baked in opacity, do not use! - 'sidebar-text-08': changeOpacity(theme.sidebarText, 0.08), - 'sidebar-text-16': changeOpacity(theme.sidebarText, 0.16), - 'sidebar-text-30': changeOpacity(theme.sidebarText, 0.3), - 'sidebar-text-40': changeOpacity(theme.sidebarText, 0.4), - 'sidebar-text-50': changeOpacity(theme.sidebarText, 0.5), - 'sidebar-text-60': changeOpacity(theme.sidebarText, 0.6), - 'sidebar-text-72': changeOpacity(theme.sidebarText, 0.72), - 'sidebar-text-80': changeOpacity(theme.sidebarText, 0.8), - 'sidebar-header-text-color-80': changeOpacity(theme.sidebarHeaderTextColor, 0.8), - 'center-channel-bg-88': changeOpacity(theme.centerChannelBg, 0.88), - 'center-channel-color-88': changeOpacity(theme.centerChannelColor, 0.88), - 'center-channel-bg-80': changeOpacity(theme.centerChannelBg, 0.8), - 'center-channel-color-80': changeOpacity(theme.centerChannelColor, 0.8), - 'center-channel-color-72': changeOpacity(theme.centerChannelColor, 0.72), - 'center-channel-bg-64': changeOpacity(theme.centerChannelBg, 0.64), - 'center-channel-color-64': changeOpacity(theme.centerChannelColor, 0.64), - 'center-channel-bg-56': changeOpacity(theme.centerChannelBg, 0.56), - 'center-channel-color-56': changeOpacity(theme.centerChannelColor, 0.56), - 'center-channel-color-48': changeOpacity(theme.centerChannelColor, 0.48), - 'center-channel-bg-40': changeOpacity(theme.centerChannelBg, 0.4), - 'center-channel-color-40': changeOpacity(theme.centerChannelColor, 0.4), - 'center-channel-bg-30': changeOpacity(theme.centerChannelBg, 0.3), - 'center-channel-color-32': changeOpacity(theme.centerChannelColor, 0.32), - 'center-channel-bg-20': changeOpacity(theme.centerChannelBg, 0.2), - 'center-channel-color-20': changeOpacity(theme.centerChannelColor, 0.2), - 'center-channel-bg-16': changeOpacity(theme.centerChannelBg, 0.16), - 'center-channel-color-24': changeOpacity(theme.centerChannelColor, 0.24), - 'center-channel-color-16': changeOpacity(theme.centerChannelColor, 0.16), - 'center-channel-bg-08': changeOpacity(theme.centerChannelBg, 0.08), - 'center-channel-color-08': changeOpacity(theme.centerChannelColor, 0.08), - 'center-channel-color-04': changeOpacity(theme.centerChannelColor, 0.04), - 'link-color-08': changeOpacity(theme.linkColor, 0.08), - 'button-bg-88': changeOpacity(theme.buttonBg, 0.88), - 'button-color-88': changeOpacity(theme.buttonColor, 0.88), - 'button-bg-80': changeOpacity(theme.buttonBg, 0.8), - 'button-color-80': changeOpacity(theme.buttonColor, 0.8), - 'button-bg-72': changeOpacity(theme.buttonBg, 0.72), - 'button-color-72': changeOpacity(theme.buttonColor, 0.72), - 'button-bg-64': changeOpacity(theme.buttonBg, 0.64), - 'button-color-64': changeOpacity(theme.buttonColor, 0.64), - 'button-bg-56': changeOpacity(theme.buttonBg, 0.56), - 'button-color-56': changeOpacity(theme.buttonColor, 0.56), - 'button-bg-48': changeOpacity(theme.buttonBg, 0.48), - 'button-color-48': changeOpacity(theme.buttonColor, 0.48), - 'button-bg-40': changeOpacity(theme.buttonBg, 0.4), - 'button-color-40': changeOpacity(theme.buttonColor, 0.4), - 'button-bg-30': changeOpacity(theme.buttonBg, 0.32), - 'button-color-32': changeOpacity(theme.buttonColor, 0.32), - 'button-bg-24': changeOpacity(theme.buttonBg, 0.24), - 'button-color-24': changeOpacity(theme.buttonColor, 0.24), - 'button-bg-16': changeOpacity(theme.buttonBg, 0.16), - 'button-color-16': changeOpacity(theme.buttonColor, 0.16), - 'button-bg-08': changeOpacity(theme.buttonBg, 0.08), - 'button-color-08': changeOpacity(theme.buttonColor, 0.08), - 'button-bg-04': changeOpacity(theme.buttonBg, 0.04), - 'button-color-04': changeOpacity(theme.buttonColor, 0.04), - 'error-text-08': changeOpacity(theme.errorTextColor, 0.08), - 'error-text-12': changeOpacity(theme.errorTextColor, 0.12), - }, - }); -} - -// given '#fffff', returns '255, 255, 255' (no trailing comma) -function toRgbValues(hexStr: string) { - const rgbaStr = `${parseInt(hexStr.substr(1, 2), 16)}, ${parseInt(hexStr.substr(3, 2), 16)}, ${parseInt(hexStr.substr(5, 2), 16)}`; - return rgbaStr; -} - -function dropAlpha(value: string) { - return value.substr(value.indexOf('(') + 1).split(',', 3).join(','); -} - -function blendComponent(background: number, foreground: number, opacity: number): number { - return ((1 - opacity) * background) + (opacity * foreground); -} - -export const blendColors = (background: string, foreground: string, opacity: number, hex = false): string => { - const backgroundComponents = getComponents(background); - const foregroundComponents = getComponents(foreground); - - const red = Math.floor(blendComponent( - backgroundComponents.red, - foregroundComponents.red, - opacity, - )); - const green = Math.floor(blendComponent( - backgroundComponents.green, - foregroundComponents.green, - opacity, - )); - const blue = Math.floor(blendComponent( - backgroundComponents.blue, - foregroundComponents.blue, - opacity, - )); - const alpha = blendComponent( - backgroundComponents.alpha, - foregroundComponents.alpha, - opacity, - ); - - if (hex) { - let r = red.toString(16); - let g = green.toString(16); - let b = blue.toString(16); - - if (r.length === 1) { - r = '0' + r; - } - if (g.length === 1) { - g = '0' + g; - } - if (b.length === 1) { - b = '0' + b; - } - - return `#${r + g + b}`; - } - - return `rgba(${red},${green},${blue},${alpha})`; -}; - -function getComponents(inColor: string): {red: number; green: number; blue: number; alpha: number} { - let color = inColor; - - // RGB color - const match = rgbPattern.exec(color); - if (match) { - return { - red: parseInt(match[1], 10), - green: parseInt(match[2], 10), - blue: parseInt(match[3], 10), - alpha: match[4] ? parseFloat(match[4]) : 1, - }; - } - - // Hex color - if (color[0] === '#') { - color = color.slice(1); - } - - if (color.length === 3) { - const tempColor = color; - color = ''; - - color += tempColor[0] + tempColor[0]; - color += tempColor[1] + tempColor[1]; - color += tempColor[2] + tempColor[2]; - } - - return { - red: parseInt(color.substring(0, 2), 16), - green: parseInt(color.substring(2, 4), 16), - blue: parseInt(color.substring(4, 6), 16), - alpha: 1, - }; -} - -const rgbPattern = /^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/; diff --git a/webapp/playbooks/src/components/backstage/file_drag_detection.tsx b/webapp/playbooks/src/components/backstage/file_drag_detection.tsx deleted file mode 100644 index 9dd63cd8153..00000000000 --- a/webapp/playbooks/src/components/backstage/file_drag_detection.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {useEffect, useState} from 'react'; - -// Could be in state instead but that would cause more render-calls. -let dragDepth = 0; -export const useFileDragDetection = () => { - const [isDraggingFile, setIsDraggingFile] = useState(false); - - useEffect(() => { - dragDepth = 0; - - const updateIsDraggingFile = () => { - setIsDraggingFile(dragDepth > 0); - }; - - const handleDragEnter = () => { - dragDepth++; - updateIsDraggingFile(); - }; - - const handleDragLeave = () => { - dragDepth--; - updateIsDraggingFile(); - }; - - const handleDrop = () => { - dragDepth = 0; - updateIsDraggingFile(); - }; - - document.addEventListener('dragenter', handleDragEnter); - document.addEventListener('dragleave', handleDragLeave); - document.addEventListener('drop', handleDrop); - return () => { - document.removeEventListener('dragenter', handleDragEnter); - document.removeEventListener('dragleave', handleDragLeave); - document.removeEventListener('drop', handleDrop); - }; - }, []); - - return isDraggingFile; -}; diff --git a/webapp/playbooks/src/components/backstage/file_upload_overlay.tsx b/webapp/playbooks/src/components/backstage/file_upload_overlay.tsx deleted file mode 100644 index dd294e5614b..00000000000 --- a/webapp/playbooks/src/components/backstage/file_upload_overlay.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {useIntl} from 'react-intl'; - -import MattermostLogo from 'src/components/assets/mattermost_logo_svg'; -import filesOverlay from 'src/components/assets/files_overlay.png'; - -export interface FileUploadOverlayProps { - message: string; - show: boolean; - overlayType: string; -} - -export const FileUploadOverlay = (props: FileUploadOverlayProps) => { - const {formatMessage} = useIntl(); - - let overlayClass = 'file-overlay'; - if (!props.show) { - overlayClass += ' hidden'; - } - if (props.overlayType === 'right') { - overlayClass += ' right-file-overlay'; - } else if (props.overlayType === 'center') { - overlayClass += ' center-file-overlay'; - } - - return ( -
    -
    -
    - {formatMessage({defaultMessage: - - - {props.message} - - -
    -
    -
    - ); -}; diff --git a/webapp/playbooks/src/components/backstage/follow_button.tsx b/webapp/playbooks/src/components/backstage/follow_button.tsx deleted file mode 100644 index db5436c02b5..00000000000 --- a/webapp/playbooks/src/components/backstage/follow_button.tsx +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useIntl} from 'react-intl'; -import {useSelector} from 'react-redux'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import styled from 'styled-components'; - -import {SecondaryButton, TertiaryButton} from 'src/components/assets/buttons'; -import {followPlaybookRun, telemetryEvent, unfollowPlaybookRun} from 'src/client'; -import {PlaybookRunEventTarget} from 'src/types/telemetry'; -import {useToaster} from 'src/components/backstage/toast_banner'; -import {ToastStyle} from 'src/components/backstage/toast'; -import Tooltip from 'src/components/widgets/tooltip'; -import {useLHSRefresh} from 'src/components/backstage/lhs_navigation'; - -interface FollowState { - isFollowing: boolean; - followers: string[]; - setFollowers: (followers: string[]) => void; -} - -interface Props { - runID: string; - followState?: FollowState; - trigger: 'run_details'|'playbooks_lhs'|'channel_rhs' -} - -const FollowButton = styled(TertiaryButton)` - font-family: 'Open Sans'; - font-size: 12px; - height: 24px; - padding: 0 10px; -`; - -const UnfollowButton = styled(SecondaryButton)` - font-family: 'Open Sans'; - font-size: 12px; - height: 24px; - padding: 0 10px; -`; - -export const FollowUnfollowButton = ({runID, followState, trigger}: Props) => { - const {formatMessage} = useIntl(); - const addToast = useToaster().add; - const currentUserId = useSelector(getCurrentUserId); - const refreshLHS = useLHSRefresh(); - - if (followState === undefined) { - return null; - } - const {isFollowing, followers, setFollowers} = followState; - - const toggleFollow = () => { - const action = isFollowing ? unfollowPlaybookRun : followPlaybookRun; - const eventTarget = isFollowing ? PlaybookRunEventTarget.Unfollow : PlaybookRunEventTarget.Follow; - action(runID) - .then(() => { - const newFollowers = isFollowing ? followers.filter((userId: string) => userId !== currentUserId) : [...followers, currentUserId]; - setFollowers(newFollowers); - refreshLHS(); - telemetryEvent(eventTarget, { - playbookrun_id: runID, - from: trigger, - }); - }) - .catch(() => { - addToast({ - content: formatMessage({defaultMessage: 'It was not possible to {isFollowing, select, true {unfollow} other {follow}} the run'}, {isFollowing}), - toastStyle: ToastStyle.Failure, - }); - }); - }; - - if (isFollowing) { - return ( - - {formatMessage({defaultMessage: 'Following'})} - - ); - } - - return ( - - - {formatMessage({defaultMessage: 'Follow'})} - - - ); -}; - -export default FollowUnfollowButton; diff --git a/webapp/playbooks/src/components/backstage/import_playbook.tsx b/webapp/playbooks/src/components/backstage/import_playbook.tsx deleted file mode 100644 index 418a93e64ca..00000000000 --- a/webapp/playbooks/src/components/backstage/import_playbook.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useRef} from 'react'; -import {useIntl} from 'react-intl'; - -import {importFile} from 'src/client'; -import {useToaster} from 'src/components/backstage/toast_banner'; -import {ToastStyle} from 'src/components/backstage/toast'; - -type FileData = string | ArrayBuffer | null | undefined; - -// 5MB in bytes -const fileSizeLimit = 5242880; - -export const useImportPlaybook = (teamId: string, cb: (id: string) => void) => { - const fileInputRef = useRef(null); - const {formatMessage} = useIntl(); - const addToast = useToaster().add; - - const genericErrorHandler = () => addToast({ - content: formatMessage({defaultMessage: 'The playbook import has failed. Please check that JSON is valid and try again.'}), - toastStyle: ToastStyle.Failure, - }); - - const readFile = (file: File) => new Promise((resolve, reject) => { - if (file.size > fileSizeLimit) { - addToast({ - content: formatMessage({defaultMessage: 'The file size exceeds the limit of 5MB.'}), - toastStyle: ToastStyle.Failure, - }); - reject(new Error('File size limit exceeded')); - return; - } - if (file.type !== 'application/json') { - addToast({ - content: formatMessage({defaultMessage: 'The file must be a valid JSON playbook template.'}), - toastStyle: ToastStyle.Failure, - }); - reject(new Error('File must be a JSON file')); - return; - } - const reader = new FileReader(); - reader.onload = (e) => { - return resolve(e.target?.result); - }; - reader.onerror = genericErrorHandler; - reader.readAsArrayBuffer(file); - }); - - const uploadFile = (data: FileData) => { - importFile(data, teamId) - .then(({id}) => cb(id)) - .catch(genericErrorHandler); - }; - - const onChange = (e: React.ChangeEvent) => { - if (!e.target.files?.[0]) { - return; - } - if (e.target.files.length > 1) { - addToast({ - content: formatMessage({defaultMessage: 'Can not import multiple files at once.'}), - toastStyle: ToastStyle.Failure, - }); - return; - } - readFile(e.target.files[0]) - .then((data) => { - uploadFile(data); - e.target.value = ''; - }); - }; - - const importPlaybookFile = (file: File) => { - readFile(file) - .then(uploadFile); - }; - - const input = ( - - ); - return [fileInputRef, input, importPlaybookFile] as const; -}; diff --git a/webapp/playbooks/src/components/backstage/lhs_navigation.tsx b/webapp/playbooks/src/components/backstage/lhs_navigation.tsx deleted file mode 100644 index e7fec432303..00000000000 --- a/webapp/playbooks/src/components/backstage/lhs_navigation.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import {useApolloClient} from '@apollo/client'; -import React from 'react'; -import styled from 'styled-components'; - -import PlaybooksSidebar, {playbookLHSQueryDocument} from 'src/components/sidebar/playbooks_sidebar'; - -const LHSContainer = styled.div` - width: 240px; - background-color: var(--sidebar-bg); - - display: flex; - flex-direction: column; -`; - -const LHSNavigation = () => { - return ( - - - - ); -}; - -export const useLHSRefresh = () => { - const apolloClient = useApolloClient(); - - const refreshLists = () => { - apolloClient.refetchQueries({ - include: [playbookLHSQueryDocument], - }); - }; - - return refreshLists; -}; - -export default LHSNavigation; diff --git a/webapp/playbooks/src/components/backstage/lhs_playbook_dot_menu.tsx b/webapp/playbooks/src/components/backstage/lhs_playbook_dot_menu.tsx deleted file mode 100644 index 6d57a825b49..00000000000 --- a/webapp/playbooks/src/components/backstage/lhs_playbook_dot_menu.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {DotsVerticalIcon} from '@mattermost/compass-icons/components'; - -import DotMenu from 'src/components/dot_menu'; -import {Separator} from 'src/components/backstage/playbook_runs/shared'; - -import {DotMenuButtonStyled} from './shared'; -import {CopyPlaybookLinkMenuItem, FavoritePlaybookMenuItem, LeavePlaybookMenuItem} from './playbook_editor/controls'; - -interface Props { - playbookId: string; - isFavorite: boolean; -} - -export const LHSPlaybookDotMenu = ({playbookId, isFavorite}: Props) => { - return ( - <> - - )} - dotMenuButton={DotMenuButtonStyled} - > - - - - - - - ); -}; diff --git a/webapp/playbooks/src/components/backstage/lhs_run_dot_menu.tsx b/webapp/playbooks/src/components/backstage/lhs_run_dot_menu.tsx deleted file mode 100644 index 5d070eb2383..00000000000 --- a/webapp/playbooks/src/components/backstage/lhs_run_dot_menu.tsx +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {useIntl} from 'react-intl'; -import {DotsVerticalIcon} from '@mattermost/compass-icons/components'; -import {useSelector} from 'react-redux'; -import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; - -import {followPlaybookRun, telemetryEvent, unfollowPlaybookRun} from 'src/client'; -import DotMenu from 'src/components/dot_menu'; -import {useToaster} from 'src/components/backstage/toast_banner'; -import {ToastStyle} from 'src/components/backstage/toast'; -import {Role, Separator} from 'src/components/backstage/playbook_runs/shared'; -import {useSetRunFavorite} from 'src/graphql/hooks'; -import {useRunFollowers} from 'src/hooks'; -import {PlaybookRunEventTarget} from 'src/types/telemetry'; - -import {useLeaveRun} from './playbook_runs/playbook_run/context_menu'; -import { - CopyRunLinkMenuItem, - FavoriteRunMenuItem, - FollowRunMenuItem, - LeaveRunMenuItem, -} from './playbook_runs/playbook_run/controls'; -import {DotMenuButtonStyled} from './shared'; -import {useLHSRefresh} from './lhs_navigation'; - -interface Props { - playbookRunId: string; - isFavorite: boolean; - ownerUserId: string; - participantIDs: string[]; - followerIDs: string[]; - hasPermanentViewerAccess: boolean; -} - -export const LHSRunDotMenu = ({playbookRunId, isFavorite, ownerUserId, participantIDs, followerIDs, hasPermanentViewerAccess}: Props) => { - const {formatMessage} = useIntl(); - const {add: addToast} = useToaster(); - const setRunFavorite = useSetRunFavorite(playbookRunId); - const currentUser = useSelector(getCurrentUser); - const refreshLHS = useLHSRefresh(); - - const followState = useRunFollowers(followerIDs); - const {isFollowing, followers, setFollowers} = followState; - - const {leaveRunConfirmModal, showLeaveRunConfirm} = useLeaveRun(hasPermanentViewerAccess, playbookRunId, ownerUserId, isFollowing, 'playbooks_lhs'); - const role = participantIDs.includes(currentUser.id) ? Role.Participant : Role.Viewer; - - const toggleFavorite = () => { - setRunFavorite(!isFavorite); - }; - - // TODO: converge with src/hooks/run useFollowRun - const toggleFollow = () => { - const action = isFollowing ? unfollowPlaybookRun : followPlaybookRun; - const eventTarget = isFollowing ? PlaybookRunEventTarget.Unfollow : PlaybookRunEventTarget.Follow; - action(playbookRunId) - .then(() => { - const newFollowers = isFollowing ? followers.filter((userId) => userId !== currentUser.id) : [...followers, currentUser.id]; - setFollowers(newFollowers); - refreshLHS(); - telemetryEvent(eventTarget, { - playbookrun_id: playbookRunId, - from: 'playbooks_lhs', - }); - }) - .catch(() => { - addToast({ - content: formatMessage({defaultMessage: 'It was not possible to {isFollowing, select, true {unfollow} other {follow}} the run'}, {isFollowing}), - toastStyle: ToastStyle.Failure, - }); - }); - }; - - return ( - <> - - )} - dotMenuButton={DotMenuButtonStyled} - > - - - - - - - - {leaveRunConfirmModal} - - ); -}; diff --git a/webapp/playbooks/src/components/backstage/line_graph.tsx b/webapp/playbooks/src/components/backstage/line_graph.tsx deleted file mode 100644 index a1b04f1b4c0..00000000000 --- a/webapp/playbooks/src/components/backstage/line_graph.tsx +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Line} from 'react-chartjs-2'; -import {Chart, registerables} from 'chart.js'; -Chart.register(...registerables); -import styled from 'styled-components'; - -const GraphBoxContainer = styled.div` - padding: 10px; -`; - -interface LineGraphProps { - title: string - xlabel?: string - data?: number[] - labels?: string[] - className?: string - tooltipTitleCallback?: (xLabel: string) => string - tooltipLabelCallback?: (yLabel: number) => string - onClick?: (index: number) => void -} - -const LineGraph = (props: LineGraphProps) => { - const style = getComputedStyle(document.body); - const centerChannelFontColor = style.getPropertyValue('--center-channel-color'); - const buttonBgColor = style.getPropertyValue('--button-bg'); - return ( - - {/*@ts-ignore*/} - { - return (val % 1 === 0) ? val : null; - }, - color: centerChannelFontColor, - }, - }, - x: { - title: { - display: Boolean(props.xlabel), - text: props.xlabel, - color: centerChannelFontColor, - }, - ticks: { - callback: (val: any, index: number) => { - return (index % 2) === 0 ? val : ''; - }, - color: centerChannelFontColor, - maxRotation: 0, - }, - }, - }, - onClick(event: any, element: any) { - if (!props.onClick) { - return; - } else if (element.length === 0) { - props.onClick(-1); - return; - } - // eslint-disable-next-line no-underscore-dangle - props.onClick(element[0]._index); - }, - onHover(event: any) { - if (props.onClick) { - event.native.target.style.cursor = 'pointer'; - } - }, - maintainAspectRatio: false, - responsive: true, - }} - data={{ - labels: props.labels, - datasets: [{ - tension: 0, - fill: false, - backgroundColor: buttonBgColor, - borderColor: buttonBgColor, - pointBackgroundColor: buttonBgColor, - pointBorderColor: '#fff', - pointHoverBackgroundColor: '#fff', - pointHoverBorderColor: buttonBgColor, - data: props.data, - }], - }} - /> - - ); -}; - -export default LineGraph; diff --git a/webapp/playbooks/src/components/backstage/main_body.tsx b/webapp/playbooks/src/components/backstage/main_body.tsx deleted file mode 100644 index 1aa36c6ce3b..00000000000 --- a/webapp/playbooks/src/components/backstage/main_body.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import { - Redirect, - Route, - Switch, - matchPath, - useHistory, - useLocation, - useRouteMatch, -} from 'react-router-dom'; - -import {useDispatch, useSelector} from 'react-redux'; - -import {getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams'; - -import {useEffectOnce, useLocalStorage, useUpdateEffect} from 'react-use'; - -import {selectTeam} from 'mattermost-redux/actions/teams'; - -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; - -import PlaybookRun from 'src/components/backstage/playbook_runs/playbook_run/playbook_run'; - -import PlaybookList from 'src/components/backstage/playbook_list'; -import PlaybookEditor from 'src/components/backstage/playbook_editor/playbook_editor'; -import {ErrorPageTypes} from 'src/constants'; -import {pluginErrorUrl, pluginUrl} from 'src/browser_routing'; -import ErrorPage from 'src/components/error_page'; -import RunsPage from 'src/components/backstage/runs_page'; - -const useInitTeamRoutingLogic = () => { - const dispatch = useDispatch(); - const location = useLocation(); - const {url} = useRouteMatch(); - const teams = useSelector(getMyTeams); - const currentTeamId = useSelector(getCurrentTeamId); - const currentUserId = useSelector(getCurrentUserId); - - // ? consider moving to multi-product or plugin infrastructure - // see https://github.com/mattermost/mattermost-webapp/blob/25043262dbab1fc2f9ac6972b1f1b0b1f9c20ae0/stores/local_storage_store.jsx#L9 - const [prevTeamId, setPrevTeamId] = useLocalStorage(`user_prev_team:${currentUserId}`, teams[0].id, {raw: true}); - - /** - * * These routes will select the proper team they belong too. - * ! Don't restore prev team on these routes or those routes will redirect back to default route. - * @see {useDefaultRedirectOnTeamChange} - */ - const negateTeamRestore = matchPath<{playbookRunId?: string; playbookId?: string;}>(location.pathname, { - path: [ - `${url}/runs/:playbookRunId`, - `${url}/playbooks/:playbookId`, - ], - }); - - useEffectOnce(() => { - if (prevTeamId && !negateTeamRestore) { - // restore prev team - dispatch(selectTeam(prevTeamId)); - } - }); - - useUpdateEffect(() => { - setPrevTeamId(currentTeamId); - }, [currentTeamId]); -}; - -/** - * Use this hook to redirect back to the default route when a different team is selected while on a team-scoped page. - * ! This is to ensure that a team mismatch doesn't occur when viewing a Playbook or Run and then selecting another team. - * @param teamScopedModelTeamId team id from team-scoped entity (e.g. a Playbook or PlaybookRun) - */ -export const useDefaultRedirectOnTeamChange = (teamScopedModelTeamId: string | undefined) => { - const history = useHistory(); - const currentTeamId = useSelector(getCurrentTeamId); - useUpdateEffect(() => { - if ( - currentTeamId && - teamScopedModelTeamId && - currentTeamId !== teamScopedModelTeamId - ) { - // team mismatch, go back to start - history.push(pluginUrl('')); - } - }, [currentTeamId]); -}; - -const MainBody = () => { - const match = useRouteMatch(); - useInitTeamRoutingLogic(); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default MainBody; diff --git a/webapp/playbooks/src/components/backstage/metrics/metrics_card.tsx b/webapp/playbooks/src/components/backstage/metrics/metrics_card.tsx deleted file mode 100644 index a5c304bdf4c..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/metrics_card.tsx +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; -import {Duration} from 'luxon'; -import {FormattedNumber, useIntl} from 'react-intl'; - -import BarGraph from 'src/components/backstage/bar_graph'; - -import {Metric, MetricType} from 'src/types/playbook'; -import {HorizontalSpacer} from 'src/components/backstage/styles'; -import {NullNumber, PlaybookStats} from 'src/types/stats'; -import {formatDuration} from 'src/components/formatted_duration'; - -interface Props { - playbookMetrics: Metric[]; - playbookStats: PlaybookStats; - index: number; -} - -const MetricsCard = ({playbookMetrics, playbookStats, index}: Props) => { - const {formatMessage} = useIntl(); - const stats = makeCardStats(playbookMetrics, playbookStats, index); - const transformFn = playbookMetrics[index].type === MetricType.MetricDuration ? (val: number) => formatDuration(Duration.fromMillis(val)) : (val: number) => val; - const valueTransformFn = playbookMetrics[index].type === MetricType.MetricDuration ? (val: number) => formatDuration(Duration.fromMillis(val), 'narrow', 'truncate') : (val: number) => val; - - const style = getComputedStyle(document.body); - const buttonBg = style.getPropertyValue('--button-bg'); - const annotation = { - type: 'line', - mode: 'horizontal', - borderColor: buttonBg, - borderWidth: 1, - scaleID: 'y-axis-0', - value: stats.target, - enabled: Boolean(stats.target), - label: { - backgroundColor: 'transparent', - fontColor: buttonBg, - fontStyle: 'normal', - content: transformFn(stats.target || 0), - position: 'left', - yAdjust: -6, - enabled: Boolean(stats.target), - }, - }; - - const titleEllipsis = ellipsize(playbookMetrics[index].title, 32); - const chartTitle = titleEllipsis + ' ' + formatMessage({defaultMessage: 'per run over the last 10 runs'}); - - return ( - - - - - {formatMessage({defaultMessage: 'Average value'})} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {stats.average === null ? '-' : transformFn(stats.average)} - - - {formatMessage({defaultMessage: '10-run average value'})} - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {stats.rolling_average === null ? '-' : transformFn(stats.rolling_average)} - {percentageChange(stats.rolling_average_change)} - - - - {formatMessage({defaultMessage: 'Value range'})} - - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {stats.value_range[0] === null ? '-' : valueTransformFn(stats.value_range[0])} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {' ' + formatMessage({defaultMessage: 'to'}) + ' '} - {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} - {stats.value_range[1] === null ? '-' : valueTransformFn(stats.value_range[1])} - - - - { - stats.target && - <> - {formatMessage({defaultMessage: 'Target value'})} - {transformFn(stats.target)} - - } - - - - - - (idx % 2 === 0 ? '' : valueTransformFn(val).toString())} - xAxesTicksCallback={(val) => val.toString()} - tooltipTitleCallback={(label) => { - const runName = stats.last_x_run_names[parseInt(label, 10) - 1]; - return ellipsize(runName, 24); - }} - tooltipLabelCallback={(val) => transformFn(val).toString()} - options={{ - annotation: { - annotations: [ - annotation, - ], - }, - }} - /> - - - ); -}; - -interface MetricsCardStats { - average: NullNumber; - rolling_average: NullNumber; - rolling_average_change: NullNumber; - value_range: NullNumber[]; - rolling_values: NullNumber[]; - target: NullNumber; - last_x_run_names: string[]; -} - -const makeCardStats = (playbookMetrics: Metric[], stats: PlaybookStats, idx: number) => { - return { - average: stats.metric_overall_average[idx] || null, - rolling_average: stats.metric_rolling_average[idx] || null, - rolling_average_change: stats.metric_rolling_average_change[idx] || null, - value_range: stats.metric_value_range[idx] || [null, null], - rolling_values: stats.metric_rolling_values[idx] || [null, null, null, null, null, null, null, null, null, null], - target: playbookMetrics[idx].target || null, - last_x_run_names: stats.last_x_run_names || ['', '', '', '', '', '', '', '', '', ''], - } as MetricsCardStats; -}; - -const ellipsize = (original: string, charLimit: number) => - original.substring(0, charLimit) + (original.length > charLimit ? '...' : ''); - -const Container = styled.div` - display: flex; -`; - -const Card = styled.div` - border: 1px solid rgba(var(--center-channel-color-rgb), 0.04); - box-shadow: 0 2px 3px rgba(var(--center-channel-color-rgb), 0.08); - border-radius: 4px; - background: var(--center-channel-bg); - width: 533px; -`; - -const SummaryCardInner = styled.div` - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr 1fr; - grid-gap: 24px; - padding: 24px; -`; - -const Cell = styled.div` - display: flex; - flex-direction: column; -`; - -const Title = styled.div` - margin-bottom: 4px; - font-weight: 600; - font-size: 14px; - line-height: 20px; - color: rgba(var(--center-channel-color-rgb), 0.72); -`; - -const Value = styled.div` - font-size: 20px; - line-height: 24px; - font-weight: 600; -`; - -const ValueTo = styled.span` - font-weight: 400; -`; - -const Row = styled.div` - display: flex; - align-items: center; -`; - -const percentageChange = (change: NullNumber) => { - if (!change || change >= 99999999) { - return null; - } - const changeSymbol = (change > 0) ? 'icon-arrow-up' : 'icon-arrow-down'; - - return ( - - - - - ); -}; - -const PercentageChange = styled.div` - margin-left: 12px; - padding-right: 4px; - display: flex; - flex-direction: row; - border-radius: 10px; - background-color: rgba(var(--online-indicator-rgb), 0.08); - color: var(--online-indicator); - font-size: 10px; - line-height: 15px; - - > i { - font-size: 12px; - } -`; - -export default MetricsCard; diff --git a/webapp/playbooks/src/components/backstage/metrics/metrics_row.tsx b/webapp/playbooks/src/components/backstage/metrics/metrics_row.tsx deleted file mode 100644 index ed26cfcb1e3..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/metrics_row.tsx +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {Duration} from 'luxon'; -import styled from 'styled-components'; - -import {FormattedMessage} from 'react-intl'; - -import {MetricsInfo} from 'src/components/backstage/metrics/metrics_run_list'; - -import {PlaybookRun} from 'src/types/playbook_run'; -import {formatDuration} from 'src/components/formatted_duration'; -import {navigateToPluginUrl} from 'src/browser_routing'; -import {MetricType} from 'src/types/playbook'; - -interface Props { - metricsInfo: MetricsInfo[]; - playbookRun: PlaybookRun; -} - -const MetricsRow = ({metricsInfo, playbookRun}: Props) => { - function openPlaybookRunDetails() { - navigateToPluginUrl(`/runs/${playbookRun.id}`); - } - - // If there is a metricsInfo, but this run doesn't have a value for it: - const metrics = metricsInfo.map((m, idx) => playbookRun.metrics_data[idx] || {value: null}); - - return ( - -
    - {playbookRun.name} -
    - {metrics.map((m, idx) => ( - - ))} -
    - ); -}; - -interface CellProps { - type: MetricType; - value: number | null; - target: number; -} - -const Cell = ({type, value, target}: CellProps) => { - if (!value) { - return ( -
    - -
    - ); - } - - const valueAsDuration = Duration.fromMillis(value); - let val = <>{value}; - const prefix = value < target ? '- ' : '+ '; - let diff = prefix + Math.abs(value - target); - if (type === MetricType.MetricDuration) { - val =
    {formatDuration(valueAsDuration)}
    ; - diff = prefix + formatDuration(Duration.fromMillis(target).minus(valueAsDuration)); - } - - return ( -
    - {val} - {diff} -
    - ); -}; - -const NAValue = styled.div` - font-weight: 400; - line-height: 16px; - color: var(--error-text); -`; - -const NormalText = styled.div` - font-weight: 400; - line-height: 16px; -`; - -const SmallText = styled.div` - font-weight: 400; - font-size: 11px; - line-height: 16px; - color: rgba(var(--center-channel-color-rgb), 0.64); - margin: 5px 0; -`; - -const RunName = styled.div` - font-weight: 600; - font-size: 14px; - line-height: 16px; -`; - -const PlaybookRunItem = styled.div` - display: flex; - padding-top: 8px; - padding-bottom: 8px; - align-items: center; - margin: 0; - border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.16); - cursor: pointer; - background: var(--center-channel-bg); - - &:hover { - background: rgba(var(--center-channel-color-rgb), 0.04); - } -`; - -export default MetricsRow; diff --git a/webapp/playbooks/src/components/backstage/metrics/metrics_run_list.tsx b/webapp/playbooks/src/components/backstage/metrics/metrics_run_list.tsx deleted file mode 100644 index 7b1ed769bd0..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/metrics_run_list.tsx +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; -import {FormattedMessage} from 'react-intl'; -import InfiniteScroll from 'react-infinite-scroll-component'; - -import {FetchPlaybookRunsParams, PlaybookRun} from 'src/types/playbook_run'; -import Filters from 'src/components/backstage/runs_list/filters'; -import {Metric, MetricType} from 'src/types/playbook'; - -import LoadingSpinner from 'src/components/assets/loading_spinner'; - -import MetricsRunListHeader from './metrics_run_list_header'; -import MetricsRow from './metrics_row'; - -export interface MetricsInfo { - type: MetricType; - title: string; - target: number; -} - -interface Props { - playbookMetrics: Metric[]; - playbookRuns: PlaybookRun[] - totalCount: number - fetchParams: FetchPlaybookRunsParams - setFetchParams: React.Dispatch> -} - -const MetricsRunList = ({ - playbookMetrics, - playbookRuns, - totalCount, - fetchParams, - setFetchParams, -}: Props) => { - const metricsInfo = playbookMetrics.map((m) => ({type: m.type, title: m.title, target: m.target} as MetricsInfo)); - - const isFiltering = ( - (fetchParams?.search_term?.length ?? 0) > 0 || - (fetchParams?.statuses?.length ?? 0) > 1 || - (fetchParams?.owner_user_id?.length ?? 0) > 0 || - (fetchParams?.participant_id?.length ?? 0) > 0 || - (fetchParams?.participant_or_follower_id?.length ?? 0) > 0 - ); - - const nextPage = () => { - setFetchParams((oldParam) => ({...oldParam, page: oldParam.page + 1})); - }; - - return ( - - - - {playbookRuns.length === 0 && !isFiltering && -
    - -
    - } - {playbookRuns.length === 0 && isFiltering && -
    - -
    - } - } - scrollableTarget={'playbooks-backstageRoot'} - > - {playbookRuns.map((playbookRun) => ( - - ))} - -
    - - - -
    -
    - ); -}; - -const PlaybookRunList = styled.div` - font-family: 'Open Sans', sans-serif; - color: rgba(var(--center-channel-color-rgb), 0.90); -`; - -const Footer = styled.div` - margin: 10px 0 20px; - font-size: 14px; -`; - -const Count = styled.div` - padding-top: 8px; - width: 100%; - text-align: center; - color: rgba(var(--center-channel-color-rgb), 0.56); -`; - -const SpinnerContainer = styled.div` - width: 100%; - height: 16px; - text-align: center; - margin-top: 10px; - overflow: visible; -`; - -const StyledSpinner = styled(LoadingSpinner)` - width: auto; - height: 100%; -`; - -export default MetricsRunList; diff --git a/webapp/playbooks/src/components/backstage/metrics/metrics_run_list_header.tsx b/webapp/playbooks/src/components/backstage/metrics/metrics_run_list_header.tsx deleted file mode 100644 index 325db8bb060..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/metrics_run_list_header.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; -import {useIntl} from 'react-intl'; - -import {MetricsInfo} from 'src/components/backstage/metrics/metrics_run_list'; - -import {SortableColHeader} from 'src/components/sortable_col_header'; -import {MetricType} from 'src/types/playbook'; -import {FetchPlaybookRunsParams} from 'src/types/playbook_run'; - -interface Props { - metricsInfo: MetricsInfo[]; - fetchParams: FetchPlaybookRunsParams - setFetchParams: React.Dispatch> -} - -const MetricsRunListHeader = ({metricsInfo, fetchParams, setFetchParams}: Props) => { - const {formatMessage} = useIntl(); - - function colHeaderClicked(index: number) { - // convert index to the col name we use on the backend - const colName = index === -1 ? 'name' : `metric${index}`; - if (fetchParams.sort === colName) { - // we're already sorting on this column; reverse the direction - const direction = fetchParams.direction === 'asc' ? 'desc' : 'asc'; - - setFetchParams((oldParams) => ({...oldParams, direction})); - return; - } - - let direction = 'asc'; - if (index > -1) { - // change to a new column; default to descending for time-based columns, ascending otherwise - direction = (metricsInfo[index].type === MetricType.MetricDuration) ? 'desc' : 'asc'; - } - - setFetchParams((oldParams) => ({...oldParams, sort: colName, direction})); - } - - return ( - -
    -
    - colHeaderClicked(-1)} - /> -
    - {metricsInfo.map((m, idx) => ( -
    - colHeaderClicked(idx)} - /> -
    - ))} -
    -
    - ); -}; - -const PlaybookRunListHeader = styled.div` - font-weight: 600; - font-size: 11px; - line-height: 36px; - color: rgba(var(--center-channel-color-rgb), 0.72); - background-color: rgba(var(--center-channel-color-rgb), 0.04); - border-radius: 4px; - padding: 0 1.6rem; -`; - -export default MetricsRunListHeader; diff --git a/webapp/playbooks/src/components/backstage/metrics/metrics_stats_view.tsx b/webapp/playbooks/src/components/backstage/metrics/metrics_stats_view.tsx deleted file mode 100644 index ed607bf78b1..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/metrics_stats_view.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; - -import {PlaybookStats} from 'src/types/stats'; -import {Metric, MetricType} from 'src/types/playbook'; -import {ClockOutline, DollarSign, PoundSign} from 'src/components/backstage/playbook_edit/styles'; - -import MetricsCard from './metrics_card'; - -interface Props { - playbookMetrics: Metric[]; - stats: PlaybookStats; -} - -const MetricsStatsView = ({playbookMetrics, stats}: Props) => { - return ( - <> - { - playbookMetrics.map((metric, idx) => ( - <> - - - - )) - } - - ); -}; - -const MetricHeader = ({metric}: { metric: Metric }) => { - let icon = ; - if (metric.type === MetricType.MetricInteger) { - icon = ; - } else if (metric.type === MetricType.MetricDuration) { - icon = ; - } - - return ( -
    - {icon} - {metric.title} - -
    - ); -}; - -const Header = styled.div` - display: flex; - align-items: center; - font-size: 16px; - font-weight: 600; - line-height: 24px; - color: var(--center-channel-color); - margin: 24px 0 8px 0; - - svg { - color: rgba(var(--center-channel-color-rgb), 0.56); - margin-right: 7px; - } -`; - -const Icon = styled.div` - margin-bottom: -6px; -`; - -const Title = styled.div` - white-space: nowrap; -`; - -const HorizontalLine = styled.div` - height: 0; - width: 100%; - margin: 0 0 0 16px; - border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08); -`; - -export default MetricsStatsView; diff --git a/webapp/playbooks/src/components/backstage/metrics/no_metrics_placeholder.tsx b/webapp/playbooks/src/components/backstage/metrics/no_metrics_placeholder.tsx deleted file mode 100644 index 55f667bf9fd..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/no_metrics_placeholder.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; -import {useIntl} from 'react-intl'; -import {useRouteMatch} from 'react-router-dom'; - -import NoMetricsSvg from 'src/components/assets/no_metrics_svg'; -import {SecondaryButton} from 'src/components/assets/buttons'; -import {navigateToUrl} from 'src/browser_routing'; - -const NoMetricsPlaceholder = () => { - const match = useRouteMatch(); - const {formatMessage} = useIntl(); - - return ( - - - - {formatMessage({defaultMessage: 'Track key metrics and measure value'})} - {formatMessage({defaultMessage: 'Use metrics to understand patterns and progress across runs, and track performance.'})} - { - navigateToUrl(match.url.replace('/reports', '/outline#retrospective')); - }} - > - {formatMessage({defaultMessage: 'Configure metrics in Retrospective'})} - - - - ); -}; - -const Container = styled.div` - display: flex; - flex-direction: column; - align-items: center; - margin-top: 96px; -`; - -const InnerContainer = styled.div` - max-width: 523px; - text-align: center; -`; - -const Title = styled.div` - font-size: 22px; - font-weight: 600; - line-height: 28px; - margin-top: 24px; -`; - -const Text = styled.div` - font-size: 14px; - font-weight: 400; - line-height: 20px; - margin: 8px 0 24px 0; -`; - -const StyledButton = styled(SecondaryButton)` - font-weight: 600; - font-size: 14px; - padding: 0 20px; - height: 40px; -`; - -export default NoMetricsPlaceholder; diff --git a/webapp/playbooks/src/components/backstage/metrics/playbook_key_metrics.tsx b/webapp/playbooks/src/components/backstage/metrics/playbook_key_metrics.tsx deleted file mode 100644 index e1af6b98657..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/playbook_key_metrics.tsx +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {HTMLAttributes, useEffect} from 'react'; -import styled from 'styled-components'; - -import {PlaybookStats} from 'src/types/stats'; -import {useAllowPlaybookAndRunMetrics, useRunsList} from 'src/hooks'; - -import {BACKSTAGE_LIST_PER_PAGE} from 'src/constants'; -import {PlaybookRunStatus} from 'src/types/playbook_run'; - -import {Metric} from 'src/types/playbook'; -import {usePlaybookViewTelemetry} from 'src/hooks/telemetry'; -import {PlaybookViewTarget} from 'src/types/telemetry'; - -import NoMetricsPlaceholder from './no_metrics_placeholder'; -import MetricsRunList from './metrics_run_list'; -import MetricsStatsView from './metrics_stats_view'; -import UpgradeKeyMetricsPlaceholder from './upgrade_key_metrics_placeholder'; - -const defaultPlaybookFetchParams = { - page: 0, - per_page: BACKSTAGE_LIST_PER_PAGE, - sort: 'last_status_update_at', - direction: 'desc', - statuses: [PlaybookRunStatus.Finished], -}; - -interface Props { - playbookID: string - playbookMetrics: Metric[] - stats: PlaybookStats; -} - -type Attrs = HTMLAttributes; - -const PlaybookKeyMetrics = ({ - playbookID, - playbookMetrics, - stats, - ...attrs -}: Props & Attrs) => { - usePlaybookViewTelemetry(PlaybookViewTarget.Reports, playbookID); - const allowStatsView = useAllowPlaybookAndRunMetrics(); - const [playbookRuns, totalCount, fetchParams, setFetchParams] = useRunsList(defaultPlaybookFetchParams); - - useEffect(() => { - setFetchParams((oldParams) => { - return {...oldParams, playbook_id: playbookID}; - }); - }, [playbookID, setFetchParams]); - - let content; - - if (!allowStatsView) { - content = ( - - - - ); - } else if (playbookMetrics.length === 0) { - content = ; - } else { - content = ( - <> - - - - - - ); - } - - return ( - - - {content} - - - ); -}; - -const PlaceholderRow = styled.div` - height: 260px; - margin: 32px 0; -`; - -const OuterContainer = styled.div` - height: 100%; -`; - -const InnerContainer = styled.div` - display: flex; - flex-direction: column; - padding: 0 20px 20px; - max-width: 1120px; - margin: 0 auto; - font-family: 'Open Sans', sans-serif; - font-style: normal; - font-weight: 600; -`; - -const RunListContainer = styled.div` - && { - margin-top: 36px; - } -`; - -export default styled(PlaybookKeyMetrics)``; diff --git a/webapp/playbooks/src/components/backstage/metrics/upgrade_key_metrics_placeholder.tsx b/webapp/playbooks/src/components/backstage/metrics/upgrade_key_metrics_placeholder.tsx deleted file mode 100644 index ca79bf594ef..00000000000 --- a/webapp/playbooks/src/components/backstage/metrics/upgrade_key_metrics_placeholder.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {useIntl} from 'react-intl'; - -import UpgradeBanner from 'src/components/upgrade_banner'; -import {AdminNotificationType} from 'src/constants'; -import UpgradeKeyMetricsBackgroundSvg from 'src/components/assets/upgrade_key_metrics_background_svg'; - -const UpgradeKeyMetricsPlaceholder = () => { - const {formatMessage} = useIntl(); - return ( - } - titleText={formatMessage({defaultMessage: 'Track key metrics and measure value'})} - helpText={formatMessage({defaultMessage: 'Use metrics to understand patterns and progress across runs, and track performance.'})} - notificationType={AdminNotificationType.PLAYBOOK_METRICS} - verticalAdjustment={412} - svgVerticalAdjustment={90} - vertical={true} - /> - ); -}; - -export default UpgradeKeyMetricsPlaceholder; diff --git a/webapp/playbooks/src/components/backstage/playbook_access_modal.tsx b/webapp/playbooks/src/components/backstage/playbook_access_modal.tsx deleted file mode 100644 index 208cd2ecd90..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_access_modal.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import {getTeam} from 'mattermost-redux/selectors/entities/teams'; -import {getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users'; -import {GlobalState} from '@mattermost/types/store'; -import {Team} from '@mattermost/types/teams'; -import React, {ComponentProps, useState} from 'react'; -import {useIntl} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; - -import styled from 'styled-components'; - -import GenericModal from 'src/components/widgets/generic_modal'; -import {AdminNotificationType, PROFILE_CHUNK_SIZE} from 'src/constants'; -import {useAllowMakePlaybookPrivate, useEditPlaybook, useHasPlaybookPermission} from 'src/hooks'; -import {Playbook, PlaybookMember, PlaybookWithChecklist} from 'src/types/playbook'; - -import {PlaybookPermissionGeneral, PlaybookRole} from 'src/types/permissions'; - -import SelectUsersBelow from './select_users_below'; -import UpgradeModal from './upgrade_modal'; -import useConfirmPlaybookConvertPrivateModal from './convert_private_playbook_modal'; - -const ID = 'playbooks_access'; - -type Props = { - playbookId: string - refetch?: () => void -} & Partial>; - -export const makePlaybookAccessModalDefinition = (props: Props) => ({ - modalId: ID, - dialogType: PlaybookAccessModal, - dialogProps: props, -}); - -const SizedGenericModal = styled(GenericModal)` - width: 800px; -`; - -const HorizontalBlock = styled.div` - display: flex; - flex-direction: row; - color: rgba(var(--center-channel-color-rgb), 0.64); - - > i { - font-size: 12px; - margin-left: -3px; - } -`; - -const SubTitle = styled.div` - font-size: 12px; - line-height: 16px; -`; - -const PrivateLink = styled.a` - font-size: 12px; - line-height: 16px; - color: var(--link-color); - margin-left: 4px; - margin-right: 3px; -`; - -const BlueArrow = styled.i` - color: var(--link-color); -`; - -const PlaybookAccessModal = ({ - playbookId, - refetch, - ...modalProps -}: Props) => { - const {formatMessage} = useIntl(); - const dispatch = useDispatch(); - const [playbook, updatePlaybook] = useEditPlaybook(playbookId, refetch); - const team = useSelector((state) => getTeam(state, playbook?.team_id || '')); - const permissionToMakePrivate = useHasPlaybookPermission(PlaybookPermissionGeneral.Convert, playbook); - const licenseToMakePrivate = useAllowMakePlaybookPrivate(); - - const [showUpgradeModal, setShowUpgradeModal] = useState(false); - const [confirmConvertPrivateModal, setShowMakePrivateConfirm] = useConfirmPlaybookConvertPrivateModal({playbookId, refetch, updater: updatePlaybook}); - - const onChange = (update: Partial) => { - if (playbook) { - const updatedPlaybook: PlaybookWithChecklist = {...playbook, ...update}; - updatePlaybook(updatedPlaybook); - } - }; - - const onAddMember = (member: PlaybookMember) => { - if (!playbook) { - return; - } - if (!playbook.members.find((elem: PlaybookMember) => elem.user_id === member.user_id)) { - onChange({ - members: [...playbook.members, member], - }); - } - }; - - const onRemoveUser = (userId: string) => { - if (!playbook) { - return; - } - const idx = playbook.members.findIndex((elem: PlaybookMember) => elem.user_id === userId); - onChange({ - members: [...playbook.members.slice(0, idx), ...playbook.members.slice(idx + 1)], - }); - }; - - const modifyRoles = (userId: string, roles: string[]) => { - if (!playbook) { - return; - } - const idx = playbook.members.findIndex((elem: PlaybookMember) => elem.user_id === userId); - const member = {...playbook.members[idx]}; - member.roles = roles; - onChange({ - members: [...playbook.members.slice(0, idx), ...playbook.members.slice(idx + 1), member], - }); - }; - - const onMakeAdmin = (userId: string) => { - modifyRoles(userId, [PlaybookRole.Admin, PlaybookRole.Member]); - }; - - const onMakeMember = (userId: string) => { - modifyRoles(userId, [PlaybookRole.Member]); - }; - - const searchUsers = (term: string) => { - return dispatch(searchProfiles(term, {team_id: playbook?.team_id})); - }; - - const getUsers = () => { - return dispatch(getProfilesInTeam(playbook?.team_id || '', 0, PROFILE_CHUNK_SIZE, '', {active: true})); - }; - - const getSubtitle = (pb: Playbook) => { - if (pb.public) { - if (team) { - return formatMessage({defaultMessage: 'Everyone in {team} can view this playbook.'}, {team: team.display_name}); - } - return formatMessage({defaultMessage: 'Everyone in this team can view this playbook.'}); - } - return formatMessage({defaultMessage: '{members, plural, =0 {No one} =1 {One person} other {# people}} can access this playbook.'}, {members: pb.members.length}); - }; - - return ( - <> - - {playbook && - <> - - - {getSubtitle(playbook)} - {(playbook.public && permissionToMakePrivate && licenseToMakePrivate) && - <> - setShowMakePrivateConfirm(true)} - > - {formatMessage({defaultMessage: 'Convert to private playbook'})} - - - - } - - - - } - - {confirmConvertPrivateModal} - setShowUpgradeModal(false)} - /> - - ); -}; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/assign_owner_selector.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/assign_owner_selector.tsx deleted file mode 100644 index 38644630f8a..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/assign_owner_selector.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import {useSelector} from 'react-redux'; - -import ReactSelect, {ControlProps} from 'react-select'; - -import styled from 'styled-components'; -import {ActionFunc} from 'mattermost-redux/types/actions'; -import {GlobalState} from '@mattermost/types/store'; -import {UserProfile} from '@mattermost/types/users'; -import {getUser} from 'mattermost-redux/selectors/entities/users'; - -import {useIntl} from 'react-intl'; - -import Profile from 'src/components/profile/profile'; -import ClearIndicator from 'src/components/backstage/playbook_edit/automation/clear_indicator'; -import MenuList from 'src/components/backstage/playbook_edit/automation/menu_list'; - -interface Props { - ownerID: string; - onAddUser: (userid: string) => void; - searchProfiles: (term: string) => ActionFunc; - getProfiles: () => ActionFunc; - isDisabled: boolean; -} - -const AssignOwnerSelector = (props: Props) => { - const {formatMessage} = useIntl(); - const [options, setOptions] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); - const ownerUser = useSelector((state: GlobalState) => getUser(state, props.ownerID)); - - // Update the options whenever the owner ID or the search term are updated - useEffect(() => { - const updateOptions = async (term: string) => { - let profiles; - if (term.trim().length === 0) { - profiles = props.getProfiles(); - } else { - profiles = props.searchProfiles(term); - } - - //@ts-ignore - profiles.then(({data}: { data: UserProfile[] }) => { - setOptions(data.filter((user: UserProfile) => user.id !== props.ownerID)); - }).catch(() => { - // eslint-disable-next-line no-console - console.error('Error searching user profiles in custom attribute settings dropdown.'); - }); - }; - - updateOptions(searchTerm); - }, [props.ownerID, searchTerm]); - - const handleSelectionChange = (userAdded: UserProfile | null, {action}: {action: string}) => { - if (action === 'clear') { - props.onAddUser(''); - } else if (userAdded) { - props.onAddUser(userAdded.id); - } - }; - - return ( - true} - isDisabled={props.isDisabled} - isMulti={false} - value={ownerUser} - controlShouldRenderValue={!props.isDisabled} - onChange={handleSelectionChange} - getOptionValue={(user: UserProfile) => user.id} - formatOptionLabel={(user: UserProfile) => ( - - )} - defaultMenuIsOpen={false} - openMenuOnClick={true} - isClearable={true} - placeholder={formatMessage({defaultMessage: 'Search for people'})} - components={{ClearIndicator, DropdownIndicator: () => null, IndicatorSeparator: () => null, MenuList}} - styles={{ - control: (provided: ControlProps) => ({ - ...provided, - minHeight: 34, - }), - }} - classNamePrefix='assign-owner-selector' - captureMenuScroll={false} - /> - ); -}; - -export default AssignOwnerSelector; - -const StyledProfile = styled(Profile)` - color: var(--center-channel-color); - - && .image { - width: 24px; - height: 24px; - } -`; - -const StyledReactSelect = styled(ReactSelect)` - flex-grow: 1; - background-color: ${(props) => (props.isDisabled ? 'rgba(var(--center-channel-bg-rgb), 0.16)' : 'var(--center-channel-bg)')}; - - .assign-owner-selector__input { - color: var(--center-channel-color); - } - - .assign-owner-selector__menu { - background-color: transparent; - box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12); - } - - - .assign-owner-selector__option { - height: 36px; - padding: 6px 21px 6px 12px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - } - - .assign-owner-selector__option--is-selected { - background-color: var(--center-channel-bg); - color: var(--center-channel-color); - } - - .assign-owner-selector__option--is-focused { - background-color: rgba(var(--button-bg-rgb), 0.04); - } - - .assign-owner-selector__control { - -webkit-transition: all 0.15s ease; - -webkit-transition-delay: 0s; - -moz-transition: all 0.15s ease; - -o-transition: all 0.15s ease; - transition: all 0.15s ease; - transition-delay: 0s; - background-color: transparent; - border-radius: 4px; - border: none; - box-shadow: inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16); - width: 100%; - height: 4rem; - font-size: 14px; - padding-left: 3.2rem; - padding-right: 16px; - - &--is-focused { - box-shadow: inset 0 0 0px 2px var(--button-bg); - } - - &:before { - left: 16px; - top: 8px; - position: absolute; - color: rgba(var(--center-channel-color-rgb), 0.56); - content: '\f0349'; - font-size: 18px; - font-family: 'compass-icons', mattermosticons; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - } - - .assign-owner-selector__option { - &:active { - background-color: rgba(var(--center-channel-color-rgb), 0.08); - } - } - - .assign-owner-selector__group-heading { - height: 32px; - padding: 8px 12px 8px; - font-size: 12px; - font-weight: 600; - line-height: 16px; - color: rgba(var(--center-channel-color-rgb), 0.56); - } -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/auto_assign_owner.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/auto_assign_owner.tsx deleted file mode 100644 index 7f519bb8937..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/auto_assign_owner.tsx +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {ActionFunc} from 'mattermost-redux/types/actions'; - -import {FormattedMessage} from 'react-intl'; - -import {AutomationHeader, AutomationTitle, SelectorWrapper} from 'src/components/backstage/playbook_edit/automation/styles'; -import AssignOwnerSelector from 'src/components/backstage/playbook_edit/automation/assign_owner_selector'; -import {Toggle} from 'src/components/backstage/playbook_edit/automation/toggle'; - -interface Props { - enabled: boolean; - disabled?: boolean; - onToggle: () => void; - searchProfiles: (term: string) => ActionFunc; - getProfiles: () => ActionFunc; - ownerID: string; - onAssignOwner: (userId: string | undefined) => void; -} - -export const AutoAssignOwner = (props: Props) => { - return ( - - - - - - - - - - - ); -}; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/channel_access.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/channel_access.tsx deleted file mode 100644 index 4ef8d7336ad..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/channel_access.tsx +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; -import {FormattedMessage, useIntl} from 'react-intl'; -import {useDispatch, useSelector} from 'react-redux'; -import {SettingsOutlineIcon} from '@mattermost/compass-icons/components'; -import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; - -import {PlaybookWithChecklist} from 'src/types/playbook'; -import {PatternedInput} from 'src/components/backstage/playbook_edit/automation/patterned_input'; -import { - AutomationHeader, - AutomationLabel, - AutomationTitle, - SelectorWrapper, -} from 'src/components/backstage/playbook_edit/automation/styles'; -import {HorizontalSpacer, RadioInput} from 'src/components/backstage/styles'; -import {showPlaybookActionsModal} from 'src/actions'; -import {SecondaryButtonLarger} from 'src/components/backstage/playbook_editor/controls'; -import ChannelSelector from 'src/components/backstage/channel_selector'; -import ClearIndicator from 'src/components/backstage/playbook_edit/automation/clear_indicator'; -import MenuList from 'src/components/backstage/playbook_edit/automation/menu_list'; - -type PlaybookSubset = Pick; - -interface Props { - playbook: PlaybookSubset; - setPlaybook: React.Dispatch>; - setChangesMade?: (b: boolean) => void; -} - -export const CreateAChannel = ({playbook, setPlaybook, setChangesMade}: Props) => { - const {formatMessage} = useIntl(); - const dispatch = useDispatch(); - const teamId = useSelector(getCurrentTeamId); - const archived = playbook.delete_at !== 0; - - const handlePublicChange = (isPublic: boolean) => { - setPlaybook({ - ...playbook, - create_public_playbook_run: isPublic, - }); - setChangesMade?.(true); - }; - - const handleChannelNameTemplateChange = (channelNameTemplate: string) => { - setPlaybook({ - ...playbook, - channel_name_template: channelNameTemplate, - }); - setChangesMade?.(true); - }; - - const handleChannelModeChange = (mode: 'create_new_channel' | 'link_existing_channel') => { - setPlaybook({ - ...playbook, - channel_mode: mode, - }); - setChangesMade?.(true); - }; - const handleChannelIdChange = (channel_id: string) => { - setPlaybook({ - ...playbook, - channel_id, - }); - setChangesMade?.(true); - }; - - return ( - - - - - handleChannelModeChange('link_existing_channel')} - /> - - - - - handleChannelIdChange(channel_id)} - channelIds={playbook.channel_id === '' ? [] : [playbook.channel_id]} - isClearable={true} - selectComponents={{ClearIndicator, DropdownIndicator: () => null, IndicatorSeparator: () => null, MenuList}} - isDisabled={archived || playbook.channel_mode === 'create_new_channel'} - captureMenuScroll={false} - shouldRenderValue={true} - teamId={teamId} - isMulti={false} - /> - - - - - - handleChannelModeChange('create_new_channel')} - /> - - - - - - - handlePublicChange(true)} - /> - - {formatMessage({defaultMessage: 'Public'})} - - - - handlePublicChange(false)} - /> - - {formatMessage({defaultMessage: 'Private'})} - - - - dispatch(showPlaybookActionsModal())} - > - - {formatMessage({defaultMessage: 'Configure channel'})} - - - - - ); -}; - -const Container = styled.div` - display: flex; - flex-direction: column; - gap: 16px; -`; - -export const VerticalSplit = styled.div` - display: flex; -`; - -const HorizontalSplit = styled.div` - display: block; - text-align: left; -`; - -export const ButtonLabel = styled.label<{disabled: boolean}>` - padding: 10px 16px; - border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); - background: ${({disabled}) => (disabled ? 'rgba(var(--center-channel-color-rgb), 0.04)' : 'var(--center-channel-bg)')}; - border-radius: 4px; - flex-grow: 1; - flex-basis: 0; - margin: 0 0 8px 0; - display: flex; - align-items: center; - cursor: pointer; -`; - -const Icon = styled.i<{ active?: boolean, disabled: boolean }>` - font-size: 16px; - line-height: 16px; - color: ${({active, disabled}) => (active && !disabled ? 'var(--button-bg)' : 'rgba(var(--center-channel-color-rgb), 0.56)')}; -`; - -const BigText = styled.div` - font-size: 14px; - line-height: 20px; - font-weight: 400; -`; - -const ChannelActionButton = styled(SecondaryButtonLarger)` - margin-top: 8px; - height: 40px; -`; - -export const StyledChannelSelector = styled(ChannelSelector)` - background-color: ${(props) => (props.isDisabled ? 'rgba(var(--center-channel-bg-rgb), 0.16)' : 'var(--center-channel-bg)')}; - .playbooks-rselect__control { - padding: 4px 16px 4px 3.2rem; - - &:before { - left: 16px; - top: 8px; - position: absolute; - color: rgba(var(--center-channel-color-rgb), 0.56); - content: '\f0349'; - font-size: 18px; - font-family: 'compass-icons', mattermosticons; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - } -`; - -export const ChannelModeRadio = styled(RadioInput)` - && { - margin: 0 8px; - } -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/clear_indicator.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/clear_indicator.tsx deleted file mode 100644 index 74dddec4119..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/clear_indicator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import ClearIcon from 'src/components/assets/icons/clear_icon'; - -const ClearIndicator = ({clearValue}: {clearValue: () => void}) => ( -
    - -
    -); - -export default ClearIndicator; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users.tsx deleted file mode 100644 index 7ee6e353868..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users.tsx +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useState} from 'react'; - -import {ActionFunc} from 'mattermost-redux/types/actions'; - -import {FormattedMessage, useIntl} from 'react-intl'; - -import {AutomationHeader, AutomationTitle, SelectorWrapper} from 'src/components/backstage/playbook_edit/automation/styles'; -import {Toggle} from 'src/components/backstage/playbook_edit/automation/toggle'; -import InviteUsersSelector from 'src/components/backstage/playbook_edit/automation/invite_users_selector'; -import ConfirmModal from 'src/components/widgets/confirmation_modal'; - -interface Props { - enabled: boolean; - disabled?: boolean; - onToggle: () => void; - searchProfiles: (term: string) => ActionFunc; - getProfiles: () => ActionFunc; - userIds: string[]; - preAssignedUserIds: string[]; - onAddUser: (userId: string) => void; - onRemoveUser: (userId: string) => void; - onRemovePreAssignedUser: (userId: string) => void; - onRemovePreAssignedUsers: () => void; -} - -interface UserInfo { - userId: string; - username: string; -} - -export const InviteUsers = (props: Props) => { - const {formatMessage} = useIntl(); - const [userToRemove, setUserToRemove] = useState(null); - const [showRemovePreAssigneeModal, setShowRemovePreAssigneeModal] = useState(false); - - const handleToggle = () => { - if (props.preAssignedUserIds.length > 0 && props.enabled) { - setShowRemovePreAssigneeModal(true); - return; - } - props.onToggle(); - }; - - const handleRemoveUser = (userId: string, username: string) => { - if (props.preAssignedUserIds.includes(userId)) { - setUserToRemove({userId, username}); - return; - } - props.onRemoveUser(userId); - }; - - return ( - <> - - - - - - - - - - - all pre-assignments.{br}{br}Are you sure you want to disable invitations?'}, - {br:
    , strong: (x: React.ReactNode) => {x}} - )} - confirmButtonText={formatMessage({defaultMessage: 'Disable invitation'})} - onConfirm={() => { - props.onRemovePreAssignedUsers(); - setShowRemovePreAssigneeModal(false); - }} - onCancel={() => setShowRemovePreAssigneeModal(false)} - /> - {name}
    is pre-assigned to one or more tasks. Not automatically inviting this user will clear their pre-assignments.{br}{br}Are you sure you want to stop inviting this user as a member of the run?'}, - {br:
    , i: (x: React.ReactNode) => {x}, name: userToRemove?.username} - )} - confirmButtonText={formatMessage({defaultMessage: 'Remove user'})} - onConfirm={() => { - if (userToRemove) { - props.onRemovePreAssignedUser(userToRemove.userId); - } - setUserToRemove(null); - }} - onCancel={() => setUserToRemove(null)} - /> - - ); -}; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users_selector.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users_selector.tsx deleted file mode 100644 index ae2fbb99326..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/invite_users_selector.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import React, {useEffect, useState} from 'react'; -import {useSelector} from 'react-redux'; -import ReactSelect, {ControlProps, GroupType, OptionsType} from 'react-select'; - -import styled from 'styled-components'; -import {ActionFunc} from 'mattermost-redux/types/actions'; -import {UserProfile} from '@mattermost/types/users'; -import {GlobalState} from '@mattermost/types/store'; -import {getUser} from 'mattermost-redux/selectors/entities/users'; - -import {FormattedMessage, useIntl} from 'react-intl'; - -import Profile from 'src/components/profile/profile'; -import {useEnsureProfiles} from 'src/hooks'; - -import MenuList from 'src/components/backstage/playbook_edit/automation/menu_list'; - -interface Props { - userIds: string[]; - onAddUser: (userid: string) => void; - onRemoveUser: (userid: string, username: string) => void; - searchProfiles: (term: string) => ActionFunc; - getProfiles: () => ActionFunc; - isDisabled: boolean; -} - -const InviteUsersSelector = (props: Props) => { - const {formatMessage} = useIntl(); - const [searchTerm, setSearchTerm] = useState(''); - const invitedUsers = useSelector((state: GlobalState) => props.userIds.map((id) => getUser(state, id))); - const [searchedUsers, setSearchedUsers] = useState([]); - useEnsureProfiles(props.userIds); - - // Update the options when the search term is updated - useEffect(() => { - const updateOptions = async (term: string) => { - let profiles; - if (term.trim().length === 0) { - profiles = props.getProfiles(); - } else { - profiles = props.searchProfiles(term); - } - - //@ts-ignore - profiles.then(({data}: { data: UserProfile[] }) => { - setSearchedUsers(data || []); - }); - }; - - updateOptions(searchTerm); - }, [searchTerm]); - - let invitedProfiles: UserProfile[] = []; - let nonInvitedProfiles: UserProfile[] = []; - - if (searchTerm.trim().length === 0) { - // Filter out all the undefined users, which will cast to false in the filter predicate - invitedProfiles = invitedUsers.filter((user) => user); - nonInvitedProfiles = searchedUsers.filter( - (profile: UserProfile) => !props.userIds.includes(profile.id), - ); - } else { - searchedUsers.forEach((profile: UserProfile) => { - if (props.userIds.includes(profile.id)) { - invitedProfiles.push(profile); - } else { - nonInvitedProfiles.push(profile); - } - }); - } - - let options: UserProfile[] | GroupType[] = nonInvitedProfiles; - if (invitedProfiles.length !== 0) { - options = [ - {label: 'SELECTED', options: invitedProfiles}, - {label: 'ALL', options: nonInvitedProfiles}, - ]; - } - - let badgeContent = ''; - const numInvitedMembers = props.userIds.length; - if (numInvitedMembers > 0) { - badgeContent = `${numInvitedMembers} SELECTED`; - } - - // Type guard to check whether the current options is a group or a plain list - const isGroup = (option: UserProfile | GroupType): option is GroupType => ( - (option as GroupType).label - ); - - return ( - true} - isDisabled={props.isDisabled} - isMulti={false} - controlShouldRenderValue={false} - onChange={(userAdded: UserProfile) => props.onAddUser(userAdded.id)} - getOptionValue={(user: UserProfile) => user.id} - formatOptionLabel={(option: UserProfile) => ( - props.onRemoveUser(option.id, option.username)} - id={option.id} - invitedUsers={(options.length > 0 && isGroup(options[0])) ? options[0].options : []} - /> - )} - defaultMenuIsOpen={false} - openMenuOnClick={true} - isClearable={false} - placeholder={formatMessage({defaultMessage: 'Search for people'})} - components={{DropdownIndicator: () => null, IndicatorSeparator: () => null, MenuList}} - styles={{ - control: (provided: ControlProps) => ({ - ...provided, - minHeight: 34, - }), - }} - classNamePrefix='invite-users-selector' - captureMenuScroll={false} - /> - ); -}; - -export default InviteUsersSelector; - -interface UserLabelProps { - onRemove: () => void; - id: string; - invitedUsers: OptionsType; -} - -const UserLabel = (props: UserLabelProps) => { - let icon = ; - if (props.invitedUsers.find((user: UserProfile) => user.id === props.id)) { - icon = ; - } - - return ( - <> - - {icon} - - ); -}; - -const Remove = styled.span` - display: inline-block; - - font-weight: 600; - font-size: 12px; - line-height: 9px; - color: rgba(var(--center-channel-color-rgb), 0.56); - - :hover { - cursor: pointer; - } -`; - -const StyledProfile = styled(Profile)` - && .image { - width: 24px; - height: 24px; - } -`; - -const PlusIcon = styled.i` - // Only shows on hover, controlled in the style from - // .invite-users-selector__option--is-focused - display: none; - - :before { - font-family: compass-icons; - font-size: 14.4px; - line-height: 17px; - color: var(--button-bg); - content: "\f0415"; - font-style: normal; - } -`; - -const StyledReactSelect = styled(ReactSelect)` - flex-grow: 1; - background-color: ${(props) => (props.isDisabled ? 'rgba(var(--center-channel-bg-rgb), 0.16)' : 'var(--center-channel-bg)')}; - - .invite-users-selector__input { - color: var(--center-channel-color); - } - - .invite-users-selector__menu { - background-color: transparent; - box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12); - } - - - .invite-users-selector__option { - height: 36px; - padding: 6px 21px 6px 12px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - } - - .invite-users-selector__option--is-selected { - background-color: var(--center-channel-bg); - color: var(--center-channel-color); - } - - .invite-users-selector__option--is-focused { - background-color: rgba(var(--button-bg-rgb), 0.04); - - ${PlusIcon} { - display: inline-block; - } - } - - .invite-users-selector__control { - -webkit-transition: all 0.15s ease; - -webkit-transition-delay: 0s; - -moz-transition: all 0.15s ease; - -o-transition: all 0.15s ease; - transition: all 0.15s ease; - transition-delay: 0s; - background-color: transparent; - border-radius: 4px; - border: none; - box-shadow: inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16); - width: 100%; - height: 4rem; - font-size: 14px; - padding-left: 3.2rem; - padding-right: 16px; - - &--is-focused { - box-shadow: inset 0 0 0px 2px var(--button-bg); - } - - &:before { - left: 16px; - top: 8px; - position: absolute; - color: rgba(var(--center-channel-color-rgb), 0.56); - content: '\f0349'; - font-size: 18px; - font-family: 'compass-icons', mattermosticons; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - - &:after { - padding: 0px 4px; - - /* Light / 8% Center Channel Text */ - background: rgba(var(--center-channel-color-rgb), 0.08); - border-radius: 4px; - - - content: '${(props) => !props.isDisabled && props.badgeContent}'; - - font-weight: 600; - font-size: 10px; - line-height: 16px; - } - } - - .invite-users-selector__option { - &:active { - background-color: rgba(var(--center-channel-color-rgb), 0.08); - } - } - - .invite-users-selector__group-heading { - height: 32px; - padding: 8px 12px 8px; - font-size: 12px; - font-weight: 600; - line-height: 16px; - color: rgba(var(--center-channel-color-rgb), 0.56); - } -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/menu_list.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/menu_list.tsx deleted file mode 100644 index ca9e3bc55c5..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/menu_list.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, {useCallback} from 'react'; - -import styled from 'styled-components'; -import {Scrollbars} from 'react-custom-scrollbars'; -import {MenuListComponentProps, OptionTypeBase} from 'react-select'; - -const MenuListWrapper = styled.div` - background-color: var(--center-channel-bg); - border: 1px solid rgba(var(--center-channel-color-rgb), 0.16); - border-radius: 4px; - - max-height: 280px; -`; - -const StyledScrollbars = styled(Scrollbars)` - height: 300px; -`; - -const ThumbVertical = styled.div` - background-color: rgba(var(--center-channel-color-rgb), 0.24); - border-radius: 2px; - width: 4px; - min-height: 45px; - margin-left: -2px; - margin-top: 6px; -`; - -const MenuList = (props: MenuListComponentProps) => { - const renderThumbVertical = useCallback((thumbProps) => { - const thumbPropsWithoutStyle = {...thumbProps}; - Reflect.deleteProperty(thumbPropsWithoutStyle, 'style'); - return ; - }, []); - - return ( - - - {props.children} - - - ); -}; - -export default MenuList; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/patterned_input.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/patterned_input.tsx deleted file mode 100644 index d6dd5592016..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/patterned_input.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled, {css} from 'styled-components'; - -import {SelectorWrapper} from 'src/components/backstage/playbook_edit/automation/styles'; - -interface Props { - enabled: boolean; - placeholderText: string; - errorText: string; - input: string; - type: string; - pattern: string; - onChange: (updatedInput: string) => void; - maxLength?: number; -} - -export const PatternedInput = (props: Props) => ( - - props.onChange(e.target.value)} - pattern={props.pattern} - placeholder={props.placeholderText} - maxLength={props.maxLength} - /> - - {props.errorText} - - -); - -const ErrorMessage = styled.div` - color: var(--error-text); - margin-left: auto; - display: none; -`; - -interface TextBoxProps { - disabled: boolean; -} - -const TextBox = styled.input` - ::placeholder { - color: var(--center-channel-color); - opacity: 0.64; - } - background: ${(props) => (props.disabled ? 'auto' : 'var(--center-channel-bg)')}; - height: 40px; - width: 100%; - color: var(--center-channel-color); - border-radius: 4px; - border: none; - box-shadow: inset 0 0 0 1px rgba(var(--center-channel-color-rgb), 0.16); - font-size: 14px; - padding-left: 16px; - padding-right: 16px; - - ${(props) => !props.disabled && props.value && css` - :invalid:not(:focus) { - box-shadow: inset 0 0 0 1px var(--error-text); - - & + ${ErrorMessage} { - display: inline-block; - } - } - `} -`; - diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/styles.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/styles.tsx deleted file mode 100644 index 632fe2e8cdd..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/styles.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import styled from 'styled-components'; - -export const AutomationHeader = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; -`; - -export const AutomationTitle = styled.div` - display: flex; - flex-direction: row; - width: 350px; - align-items: center; - column-gap: 12px; -`; - -export const AutomationLabel = styled.label<{disabled?: boolean}>` - display: flex; - flex-direction: row; - align-items: center; - column-gap: 12px; - font-weight: inherit; - margin-bottom: 0; - cursor: ${({disabled}) => (disabled ? 'default' : 'pointer')}; -`; - -export const SelectorWrapper = styled.div` - margin: 0; - width: 300px; - min-height: 40px; -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/toggle.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/toggle.tsx deleted file mode 100644 index 8767be6e617..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/toggle.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import styled from 'styled-components'; - -interface ToggleProps { - children?: React.ReactNode - isChecked: boolean; - disabled?: boolean; - onChange: () => void; -} - -export const Toggle = (props: ToggleProps) => { - return ( - - ); -}; - -interface DisabledProps { - disabled?: boolean; -} - -const RoundSwitch = styled.span` - position: relative; - display: inline-block; - top: 0; - left: 0; - right: 0; - bottom: 0; - transition: .4s; - - // Outer rectangle - width: 40px; - height: 24px; - border-radius: 14px; - background: rgba(var(--center-channel-color-rgb), ${({disabled}) => (disabled ? '0.08' : '0.24')}); - - // Inner circle - ::before { - position: absolute; - width: 20px; - height: 20px; - left: 2px; - top: calc(50% - 20px/2); - - border-radius: 50%; - background: var(--center-channel-bg); - box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.08); - - content: ""; - transition: .4s; - } - - input:checked + && { - background-color: ${({disabled}) => (disabled ? 'var(--button-bg-30)' : 'var(--button-bg)')} - } - - input:checked + &&::before { - transform: translateX(16px); - } - -`; - -const InvisibleInput = styled.input` - display: none; -`; - -const Label = styled.label` - display: flex; - align-items: center; - column-gap: 12px; - font-weight: inherit; - line-height: 16px; - cursor: ${({disabled}) => (disabled ? 'default' : 'pointer')}; - margin-bottom: 0; -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/automation/webhook_setting.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/automation/webhook_setting.tsx deleted file mode 100644 index 8c29b1be6e7..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/automation/webhook_setting.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -import {AutomationHeader, AutomationTitle, SelectorWrapper} from 'src/components/backstage/playbook_edit/automation/styles'; -import {Toggle} from 'src/components/backstage/playbook_edit/automation/toggle'; -import PatternedTextArea from 'src/components/patterned_text_area'; - -interface Props { - enabled: boolean; - disabled?: boolean; - onToggle: () => void; - textOnToggle: string; - placeholderText: string; - errorText: string; - input: string; - pattern: string; - delimiter?: string; - onChange?: (updatedInput: string) => void; - onBlur?: (updatedInput: string) => void; - maxLength?: number; - rows?: number; - maxRows?: number; - maxErrorText?: string; -} - -export const WebhookSetting = (props: Props) => { - return ( - - - - {props.textOnToggle} - - - - - - - ); -}; - diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_edit.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_edit.tsx deleted file mode 100644 index c8cfd406c28..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_edit.tsx +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useState} from 'react'; -import styled from 'styled-components'; -import {FormattedMessage, useIntl} from 'react-intl'; - -import {Metric, MetricType} from 'src/types/playbook'; -import {PrimaryButton} from 'src/components/assets/buttons'; -import {ErrorText, HelpText, StyledInput} from 'src/components/backstage/playbook_runs/shared'; -import {ClockOutline, DollarSign, PoundSign} from 'src/components/backstage/playbook_edit/styles'; -import {isMetricValueValid, metricToString, stringToMetric} from 'src/components/backstage/playbook_edit/metrics/shared'; -import MetricInput from 'src/components/backstage/playbook_runs/playbook_run/metrics/metric_input'; -import {BaseTextArea} from 'src/components/assets/inputs'; -import {VerticalSpacer} from 'src/components/backstage/styles'; - -type SetState = (prevState: Metric) => Metric; - -interface Props { - metric: Metric; - setMetric: (setState: SetState) => void; - otherTitles: string[]; - onAdd: (target: number | null) => void; - deleteClick: () => void; - saveToggle: boolean; - saveFailed: () => void; -} - -const MetricEdit = ({metric, setMetric, otherTitles, onAdd, deleteClick, saveToggle, saveFailed}: Props) => { - const {formatMessage} = useIntl(); - const [curTargetString, setCurTargetString] = useState(() => metricToString(metric.target, metric.type)); - const [curSaveToggle, setCurSaveToggle] = useState(saveToggle); - const [titleError, setTitleError] = useState(''); - const [targetError, setTargetError] = useState(''); - - const errorTitleDuplicate = formatMessage({defaultMessage: 'A metric with the same name already exists. Please add a unique name for each metric.'}); - const errorTitleMissing = formatMessage({defaultMessage: 'Please add a title for your metric.'}); - const errorTargetCurrencyInteger = formatMessage({defaultMessage: 'Please enter a number, or leave the target blank.'}); - const errorTargetDuration = formatMessage({defaultMessage: 'Please enter a duration in the format: dd:hh:mm (e.g., 12:00:00), or leave the target blank.'}); - - const verifyAndSave = (): boolean => { - // Is the title unique? - if (otherTitles.includes(metric.title)) { - setTitleError(errorTitleDuplicate); - return false; - } - - // Is the title set? - if (metric.title === '') { - setTitleError(errorTitleMissing); - return false; - } - - // Is the target valid? - if (!isMetricValueValid(metric.type, curTargetString)) { - setTargetError(metric.type === MetricType.MetricDuration ? errorTargetDuration : errorTargetCurrencyInteger); - return false; - } - - // target is valid. Convert it and save the metric. - const target = stringToMetric(curTargetString, metric.type); - onAdd(target); - return true; - }; - - if (saveToggle !== curSaveToggle) { - // we've been asked to save, either internally or externally, so verify and save if possible. - setCurSaveToggle(saveToggle); - const success = verifyAndSave(); - if (!success) { - saveFailed(); - } - } - - let inputIcon = ; - let typeTitle = ( - - ); - if (metric.type === MetricType.MetricInteger) { - inputIcon = ; - typeTitle = ( - - ); - } else if (metric.type === MetricType.MetricDuration) { - inputIcon = ; - typeTitle = ( - - ); - } - - return ( - - - {typeTitle}}} - tagName={React.Fragment} - /> - - - - {formatMessage({defaultMessage: 'Title'})} - { - const title = e.target.value; - setMetric((prevState) => ({...prevState, title})); - setTitleError(''); - }} - autoFocus={true} - maxLength={64} - /> - - - - { - setCurTargetString(e.target.value.trim()); - setTargetError(''); - }} - /> - - {formatMessage({defaultMessage: 'Description'})} - { - const description = e.target.value; - setMetric((prevState) => ({...prevState, description})); - }} - /> - {formatMessage({defaultMessage: 'Add details on what this metric is about and how it should be filled in. This description will be available on the retrospective page for each run where values for these metrics will be input.'})} - - {formatMessage({defaultMessage: 'Save'})} - - - ); -}; - -const Container = styled.div` - flex: 1; -`; - -const EditHeader = styled.div` - display: flex; - align-items: center; - font-size: 14px; - line-height: 20px; - padding: 12px 24px; - color: rgba(var(--center-channel-color-rgb), 0.64); - background: rgba(var(--center-channel-color-rgb), 0.04); - border-radius: 4px 4px 0 0; -`; - -const Button = styled.button` - font-size: 18px; - padding: 4px 1px; - background: none; - border-radius: 4px; - border: 0; - margin-left: auto; - - :hover { - background: rgba(var(--center-channel-color-rgb), 0.08); - } -`; - -const EditContainer = styled.div` - font-size: 14px; - line-height: 20px; - padding: 16px 24px 24px; - margin-bottom: 12px; - color: var(--center-channel-color); - background: var(--center-channel-bg); - border-radius: 0 0 4px 4px; -`; - -const Bold = styled.span` - display: flex; - align-items: center; - font-weight: 600; - - svg { - margin: 0 5px; - } -`; - -const Title = styled.div` - font-weight: 600; - margin: 0 0 8px 0; -`; - -const Error = ({text}: { text: string }) => ( - text === '' ? null : {text} -); -const StyledTextarea = styled(BaseTextArea)` - width: 100%; - margin-bottom: -4px; -`; - -export default MetricEdit; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_view.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_view.tsx deleted file mode 100644 index d030b8540f3..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metric_view.tsx +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import styled from 'styled-components'; -import {useIntl} from 'react-intl'; - -import {Metric, MetricType} from 'src/types/playbook'; -import {ClockOutline, DollarSign, PoundSign} from 'src/components/backstage/playbook_edit/styles'; -import {metricToString} from 'src/components/backstage/playbook_edit/metrics/shared'; - -interface Props { - metric: Metric; - editClick: () => void; - deleteClick: () => void; - disabled: boolean; -} - -const MetricView = ({metric, editClick, deleteClick, disabled}: Props) => { - const {formatMessage} = useIntl(); - const perRun = formatMessage({defaultMessage: 'per run'}); - - let icon = ; - if (metric.type === MetricType.MetricInteger) { - icon = ; - } else if (metric.type === MetricType.MetricDuration) { - icon = ; - } - - const targetStr = metricToString(metric.target, metric.type, true); - const target = metric.target === null ? '' : `${targetStr} ${perRun}`; - - return ( - - {icon} - - {metric.title} - - - - - - - - - - ); -}; - -const ViewContainer = styled.div` - flex: 1; - display: flex; - font-size: 14px; - line-height: 20px; - padding: 12px 16px 16px; - margin-bottom: 12px; - color: var(--center-channel-color); - background: var(--center-channel-bg); - border: 1px solid rgba(var(--center-channel-color-rgb), 0.08); - border-radius: 4px; -`; - -const Lhs = styled.div` - font-size: 18px; - color: rgba(var(--center-channel-color-rgb), 0.64); - padding: 0 6px 0 0; - - svg { - margin-top: 2px; - } -`; - -const Centre = styled.div` - display: flex; - flex-direction: column; - flex: 1; - font-size: 14px; - line-height: 20px; - color: rgba(var(--center-channel-color-rgb), 0.72); -`; - -const Rhs = styled.div` - display: flex; - align-items: flex-start; - color: rgba(var(--center-channel-color-rgb), 0.56); -`; - -const Button = styled.button` - font-size: 18px; - padding: 4px 1px; - background: none; - border-radius: 4px; - border: 0; - margin-top: -4px; - - :hover { - background: rgba(var(--center-channel-color-rgb), 0.08); - } -`; - -const HorizontalSpacer = styled.div<{ size: number }>` - margin-left: ${(props) => props.size}px; -`; - -const Detail = ({title, text}: { title: string, text: string }) => { - if (!text) { - return (<>); - } - return ( - - {title} - {text} - - ); -}; - -const DetailDiv = styled.div` - margin-top: 4px; -`; - -const DescrText = styled.span` - padding-left: 0.3em; -`; - -const Title = styled.div` - font-weight: 600; - color: var(--center-channel-color); -`; - -const Bold = styled.span` - font-weight: 600; -`; - -export default MetricView; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metrics.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metrics.tsx deleted file mode 100644 index 10124ac69c9..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/metrics.tsx +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useState} from 'react'; -import styled from 'styled-components'; -import {useIntl} from 'react-intl'; - -import {KeyVariantCircleIcon} from '@mattermost/compass-icons/components'; - -import {TertiaryButton} from 'src/components/assets/buttons'; -import DotMenu, {DropdownMenuItem} from 'src/components/dot_menu'; -import { - DraftPlaybookWithChecklist, - Metric, - MetricType, - PlaybookWithChecklist, - newMetric, -} from 'src/types/playbook'; -import MetricEdit from 'src/components/backstage/playbook_edit/metrics/metric_edit'; -import MetricView from 'src/components/backstage/playbook_edit/metrics/metric_view'; -import {ClockOutline, DollarSign, PoundSign} from 'src/components/backstage/playbook_edit/styles'; -import ConfirmModalLight from 'src/components/widgets/confirmation_modal_light'; -import {DefaultFooterContainer} from 'src/components/widgets/generic_modal'; -import ConditionalTooltip from 'src/components/widgets/conditional_tooltip'; -import {useAllowPlaybookAndRunMetrics} from 'src/hooks'; -import UpgradeModal from 'src/components/backstage/upgrade_modal'; -import {AdminNotificationType} from 'src/constants'; - -enum TaskType { - add, - edit, - delete, -} - -export interface EditingMetric { - index: number; - metric: Metric; -} - -interface Task { - type: TaskType; - addType?: MetricType; - index?: number; -} - -interface Props { - playbook: PlaybookWithChecklist | DraftPlaybookWithChecklist; - setPlaybook: React.Dispatch>; - setChangesMade?: (b: boolean) => void; - curEditingMetric: EditingMetric | null; - setCurEditingMetric: React.Dispatch>; - disabled: boolean; -} - -const Metrics = ({ - playbook, - setPlaybook, - setChangesMade, - curEditingMetric, - setCurEditingMetric, - disabled, -}: Props) => { - const {formatMessage} = useIntl(); - const [saveMetricToggle, setSaveMetricToggle] = useState(false); - const [nextTask, setNextTask] = useState(null); - const [deletingIdx, setDeletingIdx] = useState(-1); - const metricsAvailable = useAllowPlaybookAndRunMetrics(); - const [showUpgradeModal, setShowUpgradeModal] = useState(false); - - const deleteBaseMessage = formatMessage({defaultMessage: 'If you delete this metric, the values for it will not be collected for any future runs.'}); - const deleteExistingMessage = deleteBaseMessage + ' ' + formatMessage({defaultMessage: 'You will still be able to access historical data for this metric.'}); - const deleteMessage = deletingIdx >= 0 && deletingIdx < playbook.metrics.length && playbook.metrics[deletingIdx].id !== '' ? deleteExistingMessage : deleteBaseMessage; - - const requestAddMetric = (addType: MetricType) => { - // Only add a new metric if we aren't currently editing. - if (!curEditingMetric) { - addMetric(addType, playbook.metrics.length); - return; - } - - // We're editing. Try to close it, and if successful add the new metric. - setNextTask({type: TaskType.add, addType}); - setSaveMetricToggle((prevState) => !prevState); - }; - - const requestEditMetric = (index: number) => { - // Edit a metric immediately if we aren't currently editing. - if (!curEditingMetric) { - setCurEditingMetric({ - index, - metric: {...playbook.metrics[index]}, - }); - return; - } - - // We're editing. Try to close it, and if successful edit the metric. - setNextTask({type: TaskType.edit, index}); - setSaveMetricToggle((prevState) => !prevState); - }; - - const requestDeleteMetric = (index: number) => { - // Confirm delete immediately if we aren't currently editing, or editing the requested idx. - if (!curEditingMetric || curEditingMetric.index === index) { - setDeletingIdx(index); - return; - } - - // We're editing a different metric. Try to close it, and if successful delete the requested metric. - setNextTask({type: TaskType.delete, index}); - setSaveMetricToggle((prevState) => !prevState); - }; - - const addMetric = (metricType: MetricType, index: number) => { - setCurEditingMetric({ - index, - metric: newMetric(metricType), - }); - - setChangesMade?.(true); - }; - - const saveMetric = (target: number | null) => { - let length = playbook.metrics.length; - - if (curEditingMetric) { - const metric = {...curEditingMetric.metric, target}; - setPlaybook((pb) => { - const metrics = [...pb.metrics]; - metrics.splice(curEditingMetric.index, 1, metric); - length = metrics.length; - - return { - ...pb, - metrics, - }; - }); - setChangesMade?.(true); - } - - // Do we have a requested task ready to do next? - if (nextTask?.type === TaskType.add) { - // Typescript needs defaults (even though they will be present) - addMetric(nextTask?.addType || MetricType.MetricDuration, length); - } else if (nextTask?.type === TaskType.edit) { - // The following is because if editIndex === 0, 0 is falsey - // eslint-disable-next-line no-undefined - const index = nextTask.index === undefined ? -1 : nextTask.index; - setCurEditingMetric({index, metric: playbook.metrics[index]}); - } else if (nextTask?.type === TaskType.delete) { - // The following is because if editIndex === 0, 0 is falsey - // eslint-disable-next-line no-undefined - const index = nextTask.index === undefined ? -1 : nextTask.index; - setDeletingIdx(index); - } else { - setCurEditingMetric(null); - } - - setNextTask(null); - }; - - const confirmedDelete = () => { - setPlaybook((pb) => { - const metrics = [...pb.metrics]; - metrics.splice(deletingIdx, 1); - - return { - ...pb, - metrics, - }; - }); - setChangesMade?.(true); - setDeletingIdx(-1); - setCurEditingMetric(null); - }; - - // If we're editing a metric, we need to add (or replace) the curEditing metric into the metrics array - const metrics = [...playbook.metrics]; - if (curEditingMetric) { - metrics.splice(curEditingMetric.index, 1, curEditingMetric.metric); - } - - const addMetricMsg = formatMessage({defaultMessage: 'Add Metric'}); - let addMetricButton = ( - - setShowUpgradeModal(true)}> - - {addMetricMsg} - - - - ); - if (metricsAvailable) { - addMetricButton = ( - = 4} - id={'max-metrics-tooltip'} - content={'You may only add up to 4 key metrics'} - disableChildrenOnShow={true} - > - - - {formatMessage({defaultMessage: 'Add Metric'})} - - } - disabled={disabled || metrics.length >= 4} - placement='bottom-start' - > - requestAddMetric(MetricType.MetricDuration)}> - } - title={formatMessage({defaultMessage: 'Duration (in dd:hh:mm)'})} - description={formatMessage({defaultMessage: 'e.g., Time to acknowledge, Time to resolve'})} - /> - - requestAddMetric(MetricType.MetricCurrency)}> - } - title={formatMessage({defaultMessage: 'Cost'})} - description={formatMessage({defaultMessage: 'e.g., Sales impact, Purchases'})} - /> - - requestAddMetric(MetricType.MetricInteger)}> - } - title={formatMessage({defaultMessage: 'Integer'})} - description={formatMessage({defaultMessage: 'e.g., Resource count, Customers affected'})} - /> - - - - ); - } - - return ( -
    - { - metrics.map((metric, idx) => ( - idx === curEditingMetric?.index ? - setCurEditingMetric((prevState) => { - if (prevState) { - return {index: prevState.index, metric: setState(prevState.metric)}; - } - - // This can't happen, because we wouldn't be here if curEditingMetric === null - // (and if curEditingMetric isn't null, prevState cannot be null) -- but typescript doesn't know that. - return null; - })} - otherTitles={playbook.metrics.flatMap((m, i) => (i === idx ? [] : m.title))} - onAdd={saveMetric} - deleteClick={() => requestDeleteMetric(idx)} - saveToggle={saveMetricToggle} - saveFailed={() => setNextTask(null)} - /> : - requestEditMetric(idx)} - deleteClick={() => requestDeleteMetric(idx)} - disabled={disabled} - key={metric.id} - /> - )) - } - {addMetricButton} - = 0} - title={formatMessage({defaultMessage: 'Are you sure you want to delete?'})} - message={deleteMessage} - confirmButtonText={formatMessage({defaultMessage: 'Delete metric'})} - onConfirm={confirmedDelete} - onCancel={() => setDeletingIdx(-1)} - components={{FooterContainer: ConfirmModalFooter}} - /> - setShowUpgradeModal(false)} - /> -
    - ); -}; - -interface MetricTypeProps { - icon: JSX.Element; - title: string; - description: string; -} - -const MetricTypeOption = ({icon, title, description}: MetricTypeProps) => ( - - {icon} - - {title} - {description} - - -); - -const HorizontalContainer = styled.div` - display: flex; - align-items: start; - - > i { - color: rgba(var(--center-channel-color-rgb), 0.56); - margin-top: 2px; - } - - > svg { - color: rgba(var(--center-channel-color-rgb), 0.56); - margin: 2px 7px 0 0; - } -`; - -const VerticalContainer = styled.div` - display: flex; - flex-direction: column; -`; - -const OptionTitle = styled.div` - font-size: 14px; - line-height: 20px; -`; - -const OptionDesc = styled.div` - font-size: 12px; - line-height: 16px; - color: rgba(var(--center-channel-color-rgb), 0.56); -`; - -const ConfirmModalFooter = styled(DefaultFooterContainer)` - align-items: center; - margin-bottom: 24px; - - button.confirm { - background: var(--error-text); - } - - button.cancel { - background: rgba(var(--error-text-color-rgb), 0.08); - color: var(--error-text); - } -`; - -const UpgradeButton = styled.div` - position: relative; -`; - -const PositionedKeyVariantCircleIcon = styled(KeyVariantCircleIcon)` - position: absolute; - margin-left: -12px; - top: -4px; - color: var(--online-indicator); -`; - -export default Metrics; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/shared.ts b/webapp/playbooks/src/components/backstage/playbook_edit/metrics/shared.ts deleted file mode 100644 index ac267be61b8..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/metrics/shared.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {Duration} from 'luxon'; - -import {MetricType} from 'src/types/playbook'; -import {formatDuration} from 'src/components/formatted_duration'; - -export const metricToString = (target: number | null | undefined, type: MetricType, naturalDuration = false) => { - if (target === null || target === undefined) { - return ''; - } - - if (type === MetricType.MetricInteger || type === MetricType.MetricCurrency) { - return target.toString(); - } - - if (naturalDuration) { - return formatDuration(Duration.fromMillis(target), 'long'); - } - - const dur = Duration.fromMillis(target).shiftTo('days', 'hours', 'minutes'); - const dd = dur.days.toString().padStart(2, '0'); - const hh = dur.hours.toString().padStart(2, '0'); - const mm = dur.minutes.toString().padStart(2, '0'); - return `${dd}:${hh}:${mm}`; -}; - -export const stringToMetric = (target: string, type: MetricType) => { - if (target === '') { - return null; - } - - if (type === MetricType.MetricInteger || type === MetricType.MetricCurrency) { - return parseInt(target, 10); - } - - // assuming we've verified this is a duration in the format dd:mm:ss - const ddmmss = target.split(':').map((c) => parseInt(c, 10)); - return Duration.fromObject({ - days: ddmmss[0], - hours: ddmmss[1], - minutes: ddmmss[2], - }).as('milliseconds'); -}; - -export const isMetricValueValid = (type: MetricType, value: string) => { - if (type === MetricType.MetricDuration) { - const regex = /(^$|^\d{1,2}:\d{1,2}:\d{1,2}$)/; - if (!regex.test(value)) { - return false; - } - } else { - const regex = /^\d*$/; - if (!regex.test(value)) { - return false; - } - } - return true; -}; diff --git a/webapp/playbooks/src/components/backstage/playbook_edit/styles.tsx b/webapp/playbooks/src/components/backstage/playbook_edit/styles.tsx deleted file mode 100644 index ea40b60d10f..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_edit/styles.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import styled from 'styled-components'; -import {mdiClockOutline, mdiCurrencyUsd, mdiPound} from '@mdi/js'; -import Icon from '@mdi/react'; -import React from 'react'; - -export const Section = styled.div` - margin: 32px 0; -`; - -export const SectionTitle = styled.div` - font-weight: 600; - margin: 0 0 32px 0; -`; - -export const SidebarBlock = styled.div` - margin: 0 0 40px; -`; - -export const ClockOutline = ({sizePx, color}: {sizePx: number, color?: string}) => ( - -); - -export const DollarSign = ({sizePx, color}: {sizePx: number, color?: string}) => ( - -); - -export const PoundSign = ({sizePx, color}: {sizePx: number, color?: string}) => ( - -); diff --git a/webapp/playbooks/src/components/backstage/playbook_editor/controls.tsx b/webapp/playbooks/src/components/backstage/playbook_editor/controls.tsx deleted file mode 100644 index 4059bce1adb..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_editor/controls.tsx +++ /dev/null @@ -1,592 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import styled, {css} from 'styled-components'; -import React, {PropsWithChildren, useEffect, useMemo} from 'react'; -import {useDispatch, useSelector} from 'react-redux'; -import {Link} from 'react-router-dom'; - -import { - AccountMultipleOutlineIcon, - ArchiveOutlineIcon, - CloseIcon, - ContentCopyIcon, - ExportVariantIcon, - LinkVariantIcon, - LockOutlineIcon, - PencilOutlineIcon, - PlayOutlineIcon, - PlusIcon, - RestoreIcon, - StarIcon, - StarOutlineIcon, -} from '@mattermost/compass-icons/components'; - -import {OverlayTrigger, Tooltip} from 'react-bootstrap'; - -import {getTeam} from 'mattermost-redux/selectors/entities/teams'; -import {Team} from '@mattermost/types/teams'; -import {GlobalState} from '@mattermost/types/store'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {FormattedMessage, FormattedNumber, useIntl} from 'react-intl'; -import {createGlobalState} from 'react-use'; - -import {navigateToPluginUrl, pluginUrl} from 'src/browser_routing'; -import { - PlaybookPermissionsMember, - useAllowMakePlaybookPrivate, - useHasPlaybookPermission, - useHasTeamPermission, -} from 'src/hooks'; -import {useToaster} from 'src/components/backstage/toast_banner'; -import { - archivePlaybook, - autoFollowPlaybook, - autoUnfollowPlaybook, - duplicatePlaybook as clientDuplicatePlaybook, - clientFetchPlaybookFollowers, - getSiteUrl, - playbookExportProps, - restorePlaybook, - telemetryEvent, - telemetryEventForPlaybook, -} from 'src/client'; -import {OVERLAY_DELAY} from 'src/constants'; -import {ButtonIcon, PrimaryButton, SecondaryButton} from 'src/components/assets/buttons'; -import CheckboxInput from 'src/components/backstage/runs_list/checkbox_input'; -import {displayEditPlaybookAccessModal, openPlaybookRunModal} from 'src/actions'; -import {PlaybookPermissionGeneral} from 'src/types/permissions'; -import DotMenu, {DropdownMenuItem as DropdownMenuItemBase, DropdownMenuItemStyled, iconSplitStyling} from 'src/components/dot_menu'; -import useConfirmPlaybookArchiveModal from 'src/components/backstage/archive_playbook_modal'; -import CopyLink from 'src/components/widgets/copy_link'; -import useConfirmPlaybookRestoreModal from 'src/components/backstage/restore_playbook_modal'; -import {usePlaybookMembership, useUpdatePlaybookFavorite} from 'src/graphql/hooks'; -import {StyledDropdownMenuItem} from 'src/components/backstage/shared'; -import {copyToClipboard} from 'src/utils'; -import {useLHSRefresh} from 'src/components/backstage/lhs_navigation'; -import useConfirmPlaybookConvertPrivateModal from 'src/components/backstage/convert_private_playbook_modal'; -import {PlaybookRunEventTarget} from 'src/types/telemetry'; - -type ControlProps = { - playbook: { - id: string, - public: boolean, - default_playbook_member_role: string, - default_owner_id: string, - default_owner_enabled: boolean, - title: string, - delete_at: number, - team_id: string, - description: string, - members: PlaybookPermissionsMember[], - } - refetch?: () => void; -}; - -type StyledProps = {className?: string;}; - -const StyledLink = styled(Link)` - a&& { - color: rgba(var(--center-channel-color-rgb), 0.56); - font-weight: 600; - font-size: 14px; - display: inline-flex; - flex-shrink: 0; - align-items: center; - border-radius: 4px; - height: 36px; - padding: 0 8px; - - - &:hover, - &:focus { - background: rgba(var(--button-bg-rgb), 0.08); - color: var(--button-bg); - text-decoration: none; - } - } - - span { - padding-right: 8px; - } - - i { - font-size: 18px; - } -`; - -export const Back = styled((props: StyledProps) => { - return ( - - - - - ); -})` - -`; - -export const Members = (props: {playbookId: string, numMembers: number, refetch: () => void}) => { - const dispatch = useDispatch(); - return ( - dispatch(displayEditPlaybookAccessModal(props.playbookId, props.refetch))} - > - - - - ); -}; - -export const CopyPlaybook = ({playbook: {title, id}}: ControlProps) => { - return ( - - ); -}; - -const changeFollowing = async (playbookId: string, userId: string, following: boolean) => { - if (!playbookId || !userId) { - return null; - } - - try { - if (following) { - await autoFollowPlaybook(playbookId, userId); - } else { - await autoUnfollowPlaybook(playbookId, userId); - } - return following; - } catch { - return null; - } -}; - -const useFollowerIds = createGlobalState(null); -const useIsFollowing = createGlobalState(false); - -export const useEditorFollowersMeta = (playbookId: string) => { - const [followerIds, setFollowerIds] = useFollowerIds(); - const [isFollowing, setIsFollowing] = useIsFollowing(); - const currentUserId = useSelector(getCurrentUserId); - - const refresh = async () => { - if (!playbookId || !currentUserId) { - return; - } - const followers = await clientFetchPlaybookFollowers(playbookId); - setFollowerIds(followers); - setIsFollowing(followers.includes(currentUserId)); - }; - - useEffect(() => { - if (followerIds === null) { - setFollowerIds([]); - refresh(); - } - }, [followerIds]); - - const setFollowing = async (following: boolean) => { - setIsFollowing(following); - await changeFollowing(playbookId, currentUserId, following); - refresh(); - }; - - return {followerIds: followerIds ?? [], isFollowing, setFollowing}; -}; - -export const AutoFollowToggle = ({playbook}: ControlProps) => { - const {formatMessage} = useIntl(); - const {isFollowing, setFollowing} = useEditorFollowersMeta(playbook.id); - - const archived = playbook.delete_at !== 0; - - let toolTipText = formatMessage({defaultMessage: 'Select this to automatically receive updates when this playbook is run.'}); - if (isFollowing) { - toolTipText = formatMessage({defaultMessage: 'You automatically receive updates when this playbook is run.'}); - } - - const tooltip = ( - - {toolTipText} - - ); - - return ( - - -
    - -
    -
    -
    - ); -}; - -const LEARN_PLAYBOOKS_TITLE = 'Learn how to use playbooks'; -export const playbookIsTutorialPlaybook = (playbookTitle?: string) => playbookTitle === LEARN_PLAYBOOKS_TITLE; - -export const RunPlaybook = ({playbook}: ControlProps) => { - const dispatch = useDispatch(); - const {formatMessage} = useIntl(); - const team = useSelector((state) => getTeam(state, playbook?.team_id || '')); - const isTutorialPlaybook = playbookIsTutorialPlaybook(playbook.title); - const hasPermissionToRunPlaybook = useHasPlaybookPermission(PlaybookPermissionGeneral.RunCreate, playbook); - const enableRunPlaybook = playbook.delete_at === 0 && hasPermissionToRunPlaybook; - const refreshLHS = useLHSRefresh(); - return ( - { - dispatch(openPlaybookRunModal({ - onRunCreated: (runId, channelId, statsData) => { - navigateToPluginUrl(`/runs/${runId}?from=run_modal`); - refreshLHS(); - telemetryEvent(PlaybookRunEventTarget.Create, {...statsData, place: 'backstage_playbook_editor'}); - }, - playbookId: playbook.id, - teamId: team.id, - })); - }} - disabled={!enableRunPlaybook} - title={enableRunPlaybook ? formatMessage({defaultMessage: 'Run Playbook'}) : formatMessage({defaultMessage: 'You do not have permissions'})} - data-testid='run-playbook' - > - - {isTutorialPlaybook ? ( - - ) : ( - - )} - - ); -}; - -export const JoinPlaybook = ({playbook: {id: playbookId}, refetch}: ControlProps & {refetch: () => void;}) => { - const {formatMessage} = useIntl(); - const currentUserId = useSelector(getCurrentUserId); - const {join} = usePlaybookMembership(playbookId, currentUserId); - const {setFollowing} = useEditorFollowersMeta(playbookId); - - return ( - { - await join(); - await setFollowing(true); - refetch(); - }} - data-testid='join-playbook' - > - - {formatMessage({defaultMessage: 'Join playbook'})} - - ); -}; - -export const FavoritePlaybookMenuItem = (props: {playbookId: string, isFavorite: boolean}) => { - const {formatMessage} = useIntl(); - const updatePlaybookFavorite = useUpdatePlaybookFavorite(props.playbookId); - - const toggleFavorite = async () => { - await updatePlaybookFavorite(!props.isFavorite); - }; - return ( - - {props.isFavorite ? ( - <>{formatMessage({defaultMessage: 'Unfavorite'})} - ) : ( - <>{formatMessage({defaultMessage: 'Favorite'})} - )} - - ); -}; - -export const CopyPlaybookLinkMenuItem = (props: {playbookId: string}) => { - const {formatMessage} = useIntl(); - const {add: addToast} = useToaster(); - - return ( - { - copyToClipboard(getSiteUrl() + '/playbooks/playbooks/' + props.playbookId); - addToast({content: formatMessage({defaultMessage: 'Copied!'})}); - }} - > - - - - ); -}; - -export const LeavePlaybookMenuItem = (props: {playbookId: string}) => { - const currentUserId = useSelector(getCurrentUserId); - const refreshLHS = useLHSRefresh(); - - const {leave} = usePlaybookMembership(props.playbookId, currentUserId); - return ( - { - await leave(); - refreshLHS(); - }} - > - - - - ); -}; - -type TitleMenuProps = { - className?: string; - editTitle: () => void; - refetch: () => void; -} & PropsWithChildren; -const TitleMenuImpl = ({playbook, children, className, editTitle, refetch}: TitleMenuProps) => { - const dispatch = useDispatch(); - const {formatMessage} = useIntl(); - const [exportHref, exportFilename] = playbookExportProps(playbook); - const [confirmArchiveModal, openDeletePlaybookModal] = useConfirmPlaybookArchiveModal(() => { - if (playbook) { - archivePlaybook(playbook.id); - navigateToPluginUrl('/playbooks'); - } - }); - const [confirmRestoreModal, openConfirmRestoreModal] = useConfirmPlaybookRestoreModal((playbookId: string) => restorePlaybook(playbookId)); - const [confirmConvertPrivateModal, setShowMakePrivateConfirm] = useConfirmPlaybookConvertPrivateModal({playbookId: playbook.id, refetch}); - - const refreshLHS = useLHSRefresh(); - const {add: addToast} = useToaster(); - - const currentUserId = useSelector(getCurrentUserId); - - const archived = playbook.delete_at !== 0; - const currentUserMember = useMemo(() => playbook?.members.find(({user_id}) => user_id === currentUserId), [playbook?.members, currentUserId]); - - const permissionForDuplicate = useHasTeamPermission(playbook.team_id, 'playbook_public_create'); - const permissionToMakePrivate = useHasPlaybookPermission(PlaybookPermissionGeneral.Convert, playbook); - const licenseToMakePrivate = useAllowMakePlaybookPrivate(); - const isEligibleToMakePrivate = currentUserMember && playbook.public && permissionToMakePrivate && licenseToMakePrivate; - - const {leave} = usePlaybookMembership(playbook.id, currentUserId); - - return ( - <> - - {children} - - - } - > - {currentUserMember && ( - <> - dispatch(displayEditPlaybookAccessModal(playbook.id, refetch))} - > - - - -
    - - - - - - )} - { - const newID = await clientDuplicatePlaybook(playbook.id); - navigateToPluginUrl(`/playbooks/${newID}/outline`); - addToast({content: formatMessage({defaultMessage: 'Successfully duplicated playbook'})}); - refreshLHS(); - telemetryEventForPlaybook(playbook.id, 'playbook_duplicate_clicked_in_playbook'); - }} - disabled={!permissionForDuplicate} - disabledAltText={formatMessage({defaultMessage: 'Duplicate is disabled for this team.'})} - > - - - - telemetryEventForPlaybook(playbook.id, 'playbook_export_clicked_in_playbook')} - > - - - - {isEligibleToMakePrivate && ( - { - telemetryEventForPlaybook(playbook.id, 'playbook_makeprivate'); - setShowMakePrivateConfirm(true); - }} - > - - - - ) - } - {currentUserMember && ( - <> -
    - { - await leave(); - refetch(); - }} - > - - - -
    - {archived ? ( - openConfirmRestoreModal(playbook, () => refetch())} - > - - - - ) : ( - openDeletePlaybookModal(playbook)} - > - - - - - - )} - - )} - - {confirmArchiveModal} - {confirmRestoreModal} - {confirmConvertPrivateModal} - - ); -}; - -const DropdownMenuItem = styled(DropdownMenuItemBase)` - ${iconSplitStyling}; - min-width: 220px; -`; - -export const TitleMenu = styled(TitleMenuImpl)` -`; - -const buttonCommon = css` - padding: 0 16px; - height: 36px; - gap: 8px; - - i::before { - margin-left: 0; - margin-right: 0; - font-size: 1.05em; - } -`; - -const PrimaryButtonLarger = styled(PrimaryButton)` - ${buttonCommon}; -`; - -export const SecondaryButtonLarger = styled(SecondaryButton)` - ${buttonCommon}; -`; - -const CheckboxInputStyled = styled(CheckboxInput)` - padding: 8px 16px; - font-size: 14px; - height: 36px; - - &:hover { - background-color: transparent; - } -`; - -const SecondaryButtonLargerCheckbox = styled(SecondaryButtonLarger) <{checked: boolean}>` - border: 1px solid rgba(var(--center-channel-color-rgb), 0.24); - color: rgba(var(--center-channel-color-rgb), 0.56); - padding: 0; - - &:hover:enabled { - background-color: rgba(var(--center-channel-color-rgb), 0.08); - } - - ${({checked}) => checked && css` - border: 1px solid var(--button-bg); - color: var(--button-bg); - - &:hover:enabled { - background-color: rgba(var(--button-bg-rgb), 0.12); - } - `} -`; - -const ButtonIconStyled = styled(ButtonIcon)` - display: inline-flex; - align-items: center; - font-size: 14px; - line-height: 24px; - font-weight: 600; - border-radius: 4px; - padding: 0px 8px; - margin: 0; - color: rgba(var(--center-channel-color-rgb),0.56); - height: 36px; - width: auto; -`; - -export const TitleButton = styled.div` - padding-left: 16px; - display: inline-flex; - border-radius: 4px; - color: rgba(var(--center-channel-color-rgb), 0.64); - fill: rgba(var(--center-channel-color-rgb), 0.64); - - &:hover { - background: rgba(var(--link-color-rgb), 0.08); - color: rgba(var(--link-color-rgb), 0.72); - } -`; - -const RedText = styled.div` - color: var(--error-text); -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/broadcast_channels_selector.tsx b/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/broadcast_channels_selector.tsx deleted file mode 100644 index 213126772d5..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/broadcast_channels_selector.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React, {ReactNode} from 'react'; -import styled from 'styled-components'; - -import ReactSelect, {StylesConfig, ValueType} from 'react-select'; -import {useSelector} from 'react-redux'; -import {getMyChannels} from 'mattermost-redux/selectors/entities/channels'; -import General from 'mattermost-redux/constants/general'; - -import {Channel} from '@mattermost/types/channels'; -import {GlobalState} from '@mattermost/types/store'; - -import {useIntl} from 'react-intl'; - -import Dropdown from 'src/components/dropdown'; - -export interface Props { - id?: string; - onChannelsSelected: (channelIds: string[]) => void; - channelIds: string[]; - placeholder?: string; - children?: ReactNode; - broadcastEnabled: boolean; -} - -const getMyPublicAndPrivateChannels = (state: GlobalState) => getMyChannels(state).filter((channel) => - channel.type !== General.DM_CHANNEL && channel.type !== General.GM_CHANNEL && channel.delete_at === 0, -); - -const filterChannels = (channelIDs: string[], channels: Channel[]): Channel[] => { - if (!channelIDs || !channels) { - return []; - } - - const channelsMap = new Map(); - channels.forEach((channel: Channel) => channelsMap.set(channel.id, channel)); - - const result: Channel[] = []; - channelIDs.forEach((id: string) => { - let filteredChannel: Channel; - const channel = channelsMap.get(id); - if (channel && channel.delete_at === 0) { - filteredChannel = channel; - } else { - filteredChannel = {display_name: '', id} as Channel; - } - result.push(filteredChannel); - }); - return result; -}; - -const sortChannels = (allChannels: Channel[], selectedChannelIds: string[]): Channel[] => { - const selectedChannels: Channel[] = []; - const otherChannels: Channel[] = []; - for (let i = 0; i < allChannels.length; i++) { - if (selectedChannelIds.indexOf(allChannels[i].id) === -1) { - otherChannels.push(allChannels[i]); - } else { - selectedChannels.push(allChannels[i]); - } - } - return [...selectedChannels, ...otherChannels]; -}; - -const BroadcastChannels = (props: Props) => { - const {formatMessage} = useIntl(); - const selectableChannels = sortChannels(useSelector(getMyPublicAndPrivateChannels), props.channelIds); - - const target = ( -
    - {props.children} -
    - ); - - const getOptionValue = (channel: Channel) => { - return channel.id; - }; - - const filterOption = (option: {label: string, value: string, data: Channel}, term: string): boolean => { - const channel = option.data as Channel; - - if (term.trim().length === 0) { - return true; - } - - return channel.name.toLowerCase().includes(term.toLowerCase()) || - channel.display_name.toLowerCase().includes(term.toLowerCase()) || - channel.id.toLowerCase() === term.toLowerCase(); - }; - - const values = filterChannels(props.channelIds, selectableChannels); - - const handleChannel = (id: string) => { - const idx = props.channelIds.indexOf(id); - if (idx === -1) { - props.onChannelsSelected([...props.channelIds, id]); - } else { - props.onChannelsSelected([...props.channelIds.slice(0, idx), ...props.channelIds.slice(idx + 1)]); - } - }; - - return ( - - ) => handleChannel((option as Channel).id)} - getOptionValue={getOptionValue} - formatOptionLabel={(option: Channel) => ( - channelId === option.id)} - /> - )} - value={values} - placeholder={props.placeholder || formatMessage({defaultMessage: 'Search for a channel'})} - components={{DropdownIndicator: null, IndicatorSeparator: null}} - isDisabled={false} - styles={selectStyles} - captureMenuScroll={false} - /> - - ); -}; - -// styles for the select component -const selectStyles: StylesConfig = { - control: (provided) => ({...provided, minWidth: 240, margin: 8}), - menu: () => ({boxShadow: 'none', width: '340px'}), - option: (provided, state) => { - return { - ...provided, - backgroundColor: state.isFocused ? 'rgba(var(--button-bg-rgb), 0.08)' : 'var(--center-channel-bg)', - color: 'unset', - }; - }, -}; - -export default BroadcastChannels; - -const StyledReactSelect = styled(ReactSelect)` - font-weight: 400; - font-size: 14px; - line-height: 20px; - color: var(--center-channel-color); -`; - -interface ChannelLabelProps { - channel: Channel; - selected: boolean | undefined; - broadcastEnabled: boolean -} - -const ChannelLabel = (props: ChannelLabelProps) => { - const {formatMessage} = useIntl(); - - return ( - - - {props.channel.display_name || formatMessage({defaultMessage: 'Unknown Channel'})} - - {props.selected && - } - - ); -}; - -const CheckIcon = styled.i<{disabled: boolean}>` - color: ${(props) => (props.disabled ? 'rgba(var(--center-channel-color-rgb),0.48)' : 'var(--button-bg)')}; - font-size: 22px; - position: absolute; - right: 0; -`; - -const ChannelLabelWrapper = styled.span<{disabled: boolean}>` - ${({disabled: enabled}) => enabled && ` - text-decoration: line-through; - color: rgba(var(--center-channel-color-rgb),0.48); - `} -`; diff --git a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/retrospective_interval_selector.tsx b/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/retrospective_interval_selector.tsx deleted file mode 100644 index 364c48c02dd..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/retrospective_interval_selector.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {useIntl} from 'react-intl'; - -import React, {useMemo} from 'react'; -import {Duration} from 'luxon'; - -import {Mode, Option, useMakeOption} from 'src/components/datetime_input'; -import {StyledSelect} from 'src/components/backstage/styles'; - -interface Props { - seconds: number; - onChange: (seconds: number) => void; - disabled?: boolean; - -} -const RetrospectiveIntervalSelector = (props: Props) => { - const {formatMessage} = useIntl(); - const makeOption = useMakeOption(Mode.DurationValue); - - const options = useMemo(() => [ - makeOption({seconds: 0}, formatMessage({defaultMessage: 'Once'})), - makeOption({hours: 1}), - makeOption({hours: 4}), - makeOption({hours: 24}), - makeOption({days: 7}), - ], [formatMessage, makeOption]); - - const onChange = (option: Option) => { - if (!Duration.isDuration(option.value)) { - return; - } - props.onChange(option.value.as('seconds')); - }; - - return ( - Duration.isDuration(option.value) && option.value.as('seconds') === props.seconds)} - onChange={onChange} - options={options} - isClearable={false} - isDisabled={props.disabled} - /> - ); -}; - -export default RetrospectiveIntervalSelector; diff --git a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/update_timer_selector.tsx b/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/update_timer_selector.tsx deleted file mode 100644 index 82c2df801a7..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/update_timer_selector.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {useMemo} from 'react'; - -import DateTimeSelector from 'src/components/datetime_selector'; - -import { - Mode, - Option, - ms, - useMakeOption, -} from 'src/components/datetime_input'; - -import {Placeholder} from 'src/components/backstage/playbook_editor/outline/section_status_updates'; - -interface Props { - seconds: number; - setSeconds: (seconds: number) => void; -} - -const UpdateTimer = (props: Props) => { - const makeOption = useMakeOption(Mode.DurationValue); - - const defaults = useMemo(() => { - const options = [ - makeOption({hours: 1}), - makeOption({days: 1}), - makeOption({days: 7}), - ]; - - let value: Option | undefined; - if (props.seconds) { - value = makeOption({seconds: props.seconds}); - - const matched = options.find((o) => value && ms(o.value) === ms(value.value)); - if (matched) { - value = matched; - } else { - options.push(value); - } - options.sort((a, b) => ms(a.value) - ms(b.value)); - } - - return {options, value}; - }, [props.seconds]); - - return ( - } - date={props.seconds} - mode={Mode.DurationValue} - onlyPlaceholder={true} - suggestedOptions={defaults.options} - onSelectedChange={(value) => { - props.setSeconds((value?.value?.toMillis() || 0) / 1000); - }} - /> - ); -}; - -export default UpdateTimer; diff --git a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/webhooks_input.tsx b/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/webhooks_input.tsx deleted file mode 100644 index c270645f8e8..00000000000 --- a/webapp/playbooks/src/components/backstage/playbook_editor/outline/inputs/webhooks_input.tsx +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React, {ReactNode, useState} from 'react'; -import {useIntl} from 'react-intl'; -import styled, {css} from 'styled-components'; - -import Dropdown from 'src/components/dropdown'; -import {CancelSaveButtons} from 'src/components/checklist_item/inputs'; - -type Props = { - urls: string[]; - onChange: (urls: string[]) => Promise; - errorText?: string; - rows?: number; - maxRows?: number; - maxErrorText?: string; - maxLength?: number; - children?: ReactNode; - webhooksDisabled: boolean; -} - -export const WebhooksInput = (props: Props) => { - const {formatMessage} = useIntl(); - const [invalid, setInvalid] = useState(false); - const [errorText, setErrorText] = useState(props.errorText || formatMessage({defaultMessage: 'Invalid webhook URLs'})); - const [urls, setURLs] = useState(props.urls); - - const [isOpen, setOpen] = useState(false); - const toggleOpen = () => { - setOpen(!isOpen); - }; - - const onChange = async (newURLs: string) => { - setURLs(newURLs.split('\n')); - }; - - const target = ( -
    - {props.children} -
    - ); - - const errorTextTemp = props.errorText || formatMessage({defaultMessage: 'Invalid webhook URLs'}); - - const isValid = (newURLs: string | undefined): boolean => { - const maxRows = props.maxRows || 64; - const maxErrorText = props.maxErrorText || formatMessage({defaultMessage: 'Invalid entry: the maximum number of webhooks allowed is 64'}); - - if (newURLs && newURLs.split('\n').filter((v) => v.trim().length > 0).length > maxRows) { - setInvalid(true); - setErrorText(maxErrorText); - return false; - } - - if (newURLs && !isPatternValid(newURLs, 'https?://.*', '\n')) { - setInvalid(true); - setErrorText(errorTextTemp); - return false; - } - - setInvalid(false); - return true; - }; - - return ( - - -