├── .gitignore ├── README.md ├── applets ├── twilio-conversations │ ├── README.md │ ├── code │ │ ├── retool │ │ │ ├── retool-custom-component.html │ │ │ └── twilio-conversations-retool-app.json │ │ └── twilio-functions │ │ │ └── chat_token.js │ ├── docs │ │ ├── retool-setup.md │ │ └── twilio-setup.md │ └── images │ │ └── twilio-converations.png └── twilio-dialer │ ├── README.md │ ├── code │ ├── retool │ │ ├── retool-custom-component.html │ │ └── twilio-dialer-retool-app.json │ └── twilio-functions │ │ ├── twiml-app.js │ │ └── voice-token.js │ ├── docs │ ├── retool-setup.md │ └── twilio-setup.md │ └── images │ └── twilio-dialer.png ├── embed-url-generator ├── README.md ├── code │ └── embed_url_generator.json └── images │ ├── 1_create_api_key.png │ ├── 2_add_api_generator_as_resource.png │ ├── 3_update_app_with_new_resource.jpeg │ ├── 4_copy_app_uuid.jpeg │ ├── 5_copy_group_id.gif │ └── 6_embed_url_app.png ├── incident-central ├── README.md ├── code │ ├── Incident-Central-Details.json │ ├── Incident-Central-Home.json │ ├── Incident-Navbar.json │ ├── Report-Incident.json │ └── extras │ │ └── Database-Setup.json ├── images │ ├── home-page.png │ ├── incident-central-demo.gif │ ├── incident-details.png │ ├── report-incident-created.png │ └── report-incident.png └── setup-guides │ ├── README.md │ ├── images │ ├── adjust-navbar-link.png │ ├── adjust-report-incident-link.png │ ├── fill-in-slack-team-id.png │ ├── pagerduty-api-resource.png │ └── slack-api-resource.png │ ├── set-up-database.md │ ├── set-up-pagerduty.md │ ├── set-up-retool-apps.md │ ├── set-up-retool-resources.md │ └── set-up-slack.md ├── snowflake-resource-optimization ├── README.md ├── code │ └── Snowflake-Resource-Optimization-Setup-Configuration.json ├── images │ ├── home-page.png │ ├── setup-config-demo.gif │ ├── statement-timeouts.png │ ├── users.png │ └── warehouses.png └── setup-guides │ ├── README.md │ ├── images │ └── snowflake-resource.png │ ├── set-up-retool-app.md │ ├── set-up-retool-resource.md │ └── set-up-snowflake.md ├── snowflake-uar-reviews ├── README.md ├── code │ ├── Compliance_Review_Portal.json │ ├── Create_Review_Cycle.json │ └── Manager_Review_Portal.json ├── images │ ├── compliance_review.png │ ├── create_cycle_employees.png │ ├── create_cycle_name_cycle.png │ ├── create_cycle_system_roles.png │ ├── manager_review_bulk_approve.png │ ├── manager_review_bulk_revoke.png │ └── manager_review_landing.png └── setup-guides │ ├── images │ └── snowflake_resource.png │ ├── set-up-retool-app.md │ ├── set-up-retool-resource.md │ └── set-up-snowflake.md └── usage-viewer ├── README.md ├── code ├── usage_analytics.json └── usage_analytics_spaces.json └── images └── usage_analytics.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ✨ Retool App Exchange ✨ 2 | 3 | The Retool App Exchange is a collection of open source apps and applets built by Retools and community members. 4 | 5 | We welcome you to download apps to use, contribute extensions to apps, or contribute new apps that you think others might find helpful. 6 | 7 | Each app consists of ready-to-use code that you can download and set up in your own Retool instance, plus a detailed setup guide. 8 | 9 | 10 | ### 🛠 Updates 11 | * [Apr 2023] [Embed URL Generator](./embed-url-generator) - An app for customers of [Retool Embed](https://retool.com/products/embed) that lets you preview embedded Retool apps with different metadata and other parameters. 12 | * [Sep 2022] [Twilio Conversations](./applets/twilio-conversations) - An applet with an embeded Twilio Conversations client, enabling bi-directional messaging with an SMS client 13 | * [Aug 2022] [Snowflake: User Access Roles Review](./snowflake-uar-reviews/) - An app that allows managers and compliance teams to audit and review user access roles in Snowflake. 14 | * [Jul 2022] [Twilio Dialer](./applets/twilio-dialer/) - An applet with an embedded Twilio dialer, enabling outbound voice calls 15 | * [May 2022] [Usage Viewer](./usage-viewer/) - An app that helps you identify top apps by pageviews and queries in your Retool instance 16 | * [Mar 2022] [Snowflake Resource Optimization: Setup & Configuration](./snowflake-resource-optimization/) - An app that helps you monitor & manage Snowflake resource consumption 17 | * [Jan 2022] [Incident Central](./incident-central/) - An incident response hub for engineering teams 18 | 19 | ### 👩‍💻 Contributing 20 | To contribute, open a Github Issue on this repo, and let us know what you are thinking of contributing! We encourage you to reach out before you get started building to get early feedback. 21 | -------------------------------------------------------------------------------- /applets/twilio-conversations/README.md: -------------------------------------------------------------------------------- 1 | # Twilio Conversations 2 | 3 | ## Overview 4 | This applet demonstrates how to embed Twilio Conversations in a Retool app to enable bi-directional chat with an SMS client. The app requires an SMS-enabled phone number which you must register with Twilio in order to test. 5 | 6 | This serves as an example of how to use Retool [custom components](https://docs.retool.com/docs/custom-components) to add advanced functionality to your apps. You'd need to extensively modify the parts outside of the custom component to use it for anything real! 7 | 8 | If you'd just like a guided tour of how the applet works, check out our [video walkthrough](https://www.youtube.com/watch?v=NL8liiIYN0o). Otherwise, read on to learn how to set it up in your own environment! 9 |

10 | 11 |

12 | 13 | ## Who is this for? 14 | This applet will be most interesting for developers who are familiar with the core concepts of building Retool apps and are looking to take the next step with custom components. 15 | 16 | If you're just getting started with Retool, check out [Retool University](https://docs.retool.com/docs/retool-university)! 17 | 18 | ## Setting up the app 19 | To send and receive messages from your Retool app, you'll first need to get up and running with Twilio: 20 | 21 | [Twilio Setup](./docs/twilio-setup.md) 22 | 23 | Once you've gotten Twilio set up, follow these steps to get it all working in Retool: 24 | 25 | [Retool Setup](./docs/retool-setup.md) 26 | 27 | ## How to contribute 28 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 29 | -------------------------------------------------------------------------------- /applets/twilio-conversations/code/retool/retool-custom-component.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | Embedded Twilio Conversations Client for Retool 19 | 20 | 25 | 26 | 104 | 105 | 106 |

Twilio Conversations Custom Component

107 | 108 | 109 | -------------------------------------------------------------------------------- /applets/twilio-conversations/code/twilio-functions/chat_token.js: -------------------------------------------------------------------------------- 1 | exports.handler = function (context, event, callback) { 2 | 3 | const IDENTITY = event.request.headers.identity; 4 | 5 | const { ACCOUNT_SID } = context; 6 | 7 | // set these values in your .env file 8 | const { API_KEY_SID, API_KEY_SECRET, CONVERSATIONS_SID } = context; 9 | 10 | const { AccessToken } = Twilio.jwt; 11 | const { ChatGrant } = AccessToken; 12 | 13 | const accessToken = new AccessToken(ACCOUNT_SID, API_KEY_SID, API_KEY_SECRET); 14 | accessToken.identity = IDENTITY; 15 | const grant = new ChatGrant({ 16 | serviceSid: CONVERSATIONS_SID 17 | }); 18 | accessToken.addGrant(grant); 19 | 20 | const response = new Twilio.Response(); 21 | 22 | response.appendHeader('Content-Type', 'application/json'); 23 | response.setBody({ 24 | identity: IDENTITY, 25 | token: accessToken.toJwt(), 26 | }); 27 | callback(null, response); 28 | }; -------------------------------------------------------------------------------- /applets/twilio-conversations/docs/retool-setup.md: -------------------------------------------------------------------------------- 1 | # Retool Setup 2 | 3 | Make sure you've completed the steps described in the [Twilio Setup](./twilio-setup.md) documentation before proceeding! 4 | 5 | ## 1. Create a Retool REST API Resource to retrieve chat tokens 6 | 7 | In order for your Retool app to authenticate with Twilio, you'll need to get a valid chat token. To set this up: 8 | 9 | 1. Navigate to the `/resources` page in your instance and click "Create a new resource." 10 | 2. Select REST API as the resource type. 11 | 3. Name the resource “Twilio Conversations Chat Token”. 12 | 4. Specify the URL of your `chat_token` function as noted in step six of the Twilio setup. 13 | 5. Specify a header with a key of `identity` and a value of `{{current_user.email}}`. This is used to pass the email address of the Retool user to Twilio for use in the token identity. 14 | 6. Save your changes. 15 | 16 | ## 2. Create a Twilio API Resource to manage Conversations and Participants 17 | 18 | Your Retool app will need to delete previously created Conversations in order to start new Conversations with the same SMS number, as well as add new participants to the Conversation. To set this up: 19 | 20 | 1. Navigate to the `/resources` page in your instance and click "Create a new resource". 21 | 2. Select Twilio as the resource type. 22 | 3. Name the resource "Twilio Conversations API". 23 | 4. Specify your Twilio API Key SID as the value of `Account SID` and your Twilio API Key Secret as the value of `Auth Token`. 24 | - These credentials were created in Step 2 of the [Twilio Setup](twilio-setup.md). 25 | - The current version the Twilio resource doesn't correctly validate API Keys, so the "test connection" button won't work on the Resource configuration screen. 26 | 5. Save your changes. 27 | 28 | ## 3. Download the Retool app code 29 | 30 | Download [twilio-conversations-retool-app.json](/applets/twilio-conversations/code/retool/twilio-conversations-retool-app.json) from this repository. 31 | 32 | ## 4. Import the Retool app code 33 | On the [Retool main page](https://docs.retool.com/docs/protected-applications-getting-started#importing-the-application), click `Create new` and select `Import an app`. Upload the JSON file containing the app code, and name the app. 34 | 35 | You may need to connect the Resource Queries in the Retool app with the Resources you created in the previous steps: 36 | 37 | | Folder | Resource Query | Resource | 38 | | --- | --- | --- | 39 | | gets | getChatToken | Twilio Conversations Chat Token | 40 | | gets | getConversations | Twilio Conversations API | 41 | | writes | addParticipant | Twilio Conversations API | 42 | | writes | deleteConversations | Twilio Conversations API | 43 | 44 | ## 5. Explore the Retool app 45 | 46 | If everything is working as expected, you should now be able to send and receive messages between the Retool app and the phone number you registered! To use the app: 47 | 48 | 1. When first loading the app, you'll be prompted to enter your Twilio Conversations Service SID. This is the Service SID you retrieved in step 3 of the Twilio setup. 49 | 2. Once the app has initialized, fill out the SMS settings: 50 | - Enter the phone number you were assigned in step 0 of the Twilio setup as "Twilio Outbound number". 51 | - Enter the SMS-enabled phone number that you verified in step 1 of the Twilio setup as "Customer number" and click Submit. 52 | 4. Enter some text in the chat box and press "enter" to send! 53 | 54 | ## 6. Learn about how the custom component works 55 | 56 | You can read more about [Retool custom components](https://docs.retool.com/docs/custom-components). You can also view the code used in the Twilio Conversations component in Retool, the contents of which are included in [retool-custom-component.html](/applets/twilio-conversations/code/retool/retool-custom-component.html) for easy reading! 57 | 58 | Also, check out our in-depth [video walkthrough](https://www.youtube.com/watch?v=NL8liiIYN0o) of how this app is put together! 59 | -------------------------------------------------------------------------------- /applets/twilio-conversations/docs/twilio-setup.md: -------------------------------------------------------------------------------- 1 | # Twilio Setup 2 | Before you can get this Retool app working, you'll need to do some pre-work in Twilio. 3 | 4 | ## 0. Sign up for a Twilio account and get a phone number 5 | 6 | If you don't already have a Twilio account, you'll need to get one! The good news is that Twilio makes it easy (and free!) to get started. Just head over to https://www.twilio.com/try-twilio and sign up! 7 | 8 | As you go through the onboarding flow, indicate that you'll be building a messaging app. Once you're logged into Twilio console, you'll be prompted to get a Twilio phone number. Note the phone number you were assigned for later! 9 | 10 | ## 1. Verify your SMS-enabled phone number 11 | [Verify your phone number](https://support.twilio.com/hc/en-us/articles/223180048-Adding-a-Verified-Phone-Number-or-Caller-ID-with-Twilio) to enable outbound SMS to that number. Note that this is only necessary with trial Twilio accounts. 12 | 13 | ## 2. Create an API key/secret pair 14 | 15 | [Create an API key](https://www.twilio.com/docs/iam/keys/api-key-resource) and note the key's `SID` and `Secret` for later. 16 | 17 | ## 3. Create a Twilio Conversations Service 18 | [Create a Conversations Service](https://www.twilio.com/docs/conversations/api/service-resource) and note the `SID` for later. A Conversations Service is a top-level container for other resources in the Twilio Conversations REST API. 19 | 20 | Note: you must create a new Conversations Service for use exclusively with this demo! The demo deletes all Conversations inside of this service during initialization, so using an existing Conversations Service is unsafe! 21 | 22 | ## 4. Create a Twilio Functions Service 23 | A [Twilio Service](https://www.twilio.com/docs/runtime/functions/create-service) is a container for the server-less functions which make it possible for your Retool app to get a chat access token. 24 | 25 | ## 5. Set Twilio Service environment variables 26 | 27 | In your new Twilio Service, set the following environment variables: 28 | 29 | | Key | Value | 30 | | ---- | ---- | 31 | | `CONVERSATIONS_SID` | The `Conversations SID` you noted in step three | 32 | | `API_KEY_SID` | The `SID` you noted in step one | 33 | | `API_KEY_SECRET` | The `Secret` you noted in step one | 34 | 35 | ## 6. Configure Twilio Service dependencies 36 | 37 | The server-less functions running in Twilio need access to a few software libraries to function properly. Import the following NPM modules: 38 | 39 | | Module | Version | 40 | | --- | --- | 41 | | `lodash` | `4.17.11` | 42 | | `@twilio/runtime-handler` | `1.2.1` | 43 | | `twilio` | `3.29.2` | 44 | | `xmldom` | `0.1.27` | 45 | | `util` | `0.11.0` | 46 | 47 | ## 7. Create a function to issue chat tokens 48 | 49 | In order for the embedded Twilio Conversations client to authenticate with Twilio's services, it needs a valid access token. To create a function for issuing chat tokens: 50 | 51 | 1. Add a function 52 | 2. Specify a path `chat_token` 53 | 3. Set the function to `Public` 54 | - **Heads up! This means that anyone who can guess the URL of your function can issue valid access tokens. You would need to implement authentication on this endpoint for a production use-case!** 55 | 4. Copy and paste the contents of [chat_token.js](/applets/twilio-conversations/code/twilio-functions/chat_token.js) into the function and save 56 | 5. Copy the URL for this function for later use 57 | 58 | ## 8. Deploy your Twilio Service! 59 | 60 | This finalizes all of the changes you've made. 61 | 62 | ## Next steps 63 | Next, set up the Retool app to make calls using your Twilio service! 64 | 65 | [Retool Setup](./retool-setup.md) 66 | -------------------------------------------------------------------------------- /applets/twilio-conversations/images/twilio-converations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/applets/twilio-conversations/images/twilio-converations.png -------------------------------------------------------------------------------- /applets/twilio-dialer/README.md: -------------------------------------------------------------------------------- 1 | # Twilio Dialer 2 | 3 | ## Overview 4 | This applet demonstrates how to embed a Twilio dialer in a Retool app. The app is set up with a hardcoded table of "leads" to simulate a simple call center app. You can edit the contents of the leads table directly, or simply type your phone number in the field to try calling yourself. 5 | 6 | This serves as an example of how to use Retool [custom components](https://docs.retool.com/docs/custom-components) to add advanced functionality to your apps. You'd need to extensively modify the parts outside of the custom component to use it for anything real! 7 | 8 | If you'd just like a guided tour of how the applet works, check out our [video walkthrough](https://youtu.be/xPSS3kp3XEI). Otherwise, read on to learn how to set it up in your own environment! 9 |

10 | 11 |

12 | 13 | ## Who is this for? 14 | This applet will be most interesting for developers who are familiar with the core concepts of building Retool apps and are looking to take the next step with custom components. 15 | 16 | If you're just getting started with Retool, check out [Retool University](https://docs.retool.com/docs/retool-university)! 17 | 18 | ## Setting up the app 19 | To make outbound calls from your Retool app, you'll first need to get up and running with Twilio: 20 | 21 | [Twilio Setup](./docs/twilio-setup.md) 22 | 23 | Once you've gotten Twilio set up, follow these steps to get it all working in Retool: 24 | 25 | [Retool Setup](./docs/retool-setup.md) 26 | 27 | ## How to contribute 28 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 29 | -------------------------------------------------------------------------------- /applets/twilio-dialer/code/retool/retool-custom-component.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 12 | Embedded Twilio Dialer for Retool 13 | 61 | 62 | 63 |
64 |
65 |
66 | 67 | 68 | 72 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /applets/twilio-dialer/code/retool/twilio-dialer-retool-app.json: -------------------------------------------------------------------------------- 1 | {"uuid":"9cf3009e-f335-11ec-84e1-53690545aee8","page":{"id":79078,"data":{"appState":"[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"callButton\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"callButton\",\"type\",\"widget\",\"subtype\",\"ButtonWidget2\",\"namespace\",null,\"resourceName\",null,\"resourceDisplayName\",null,\"template\",[\"^3\",[\"horizontalAlign\",\"stretch\",\"clickable\",false,\"iconAfter\",\"\",\"submitTargetId\",null,\"hidden\",false,\"text\",\"{{ twilioDevice.model.status === \\\"ready_for_call\\\" ? \\\"Call\\\" : \\\"End\\\" }}\",\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"background\",\"{{ twilioDevice.model.status === \\\"ready_for_call\\\" ? theme.success : theme.danger }}\"]],\"styleVariant\",\"solid\",\"submit\",false,\"iconBefore\",\"{{ twilioDevice.model.status === \\\"ready_for_call\\\" ? \\\"/icon:bold/phone-telephone\\\" : \\\"/icon:bold/entertainment-control-button-stop\\\" }}\",\"events\",[\"~#iL\",[[\"^3\",[\"event\",\"click\",\"type\",\"widget\",\"method\",\"updateModel\",\"pluginId\",\"twilioDevice\",\"targetId\",null,\"params\",[\"^3\",[\"model\",\"{{ twilioDevice.model.status === \\\"ready_for_call\\\" ? {phoneNumber: phoneNumberEntry.value, status: \\\"retool_called\\\"} : {status: \\\"retool_ended\\\"} }}\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]],[\"^3\",[\"event\",\"click\",\"type\",\"widget\",\"method\",\"setHidden\",\"pluginId\",\"welcomeText\",\"targetId\",null,\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",\"{{phoneNumberEntry.validationMessage || getVoiceToken.error}}\",\"maintainSpaceWhenHidden\",false]],\"style\",[\"^3\",[]],\"position2\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"container\",\"\",\"rowGroup\",\"body\",\"subcontainer\",\"\",\"row\",0.6000000000000002,\"col\",10,\"height\",1,\"width\",2,\"tabNum\",0]]],\"mobilePosition2\",null,\"mobileAppPosition\",null,\"tabIndex\",null,\"^=\",\"\",\"createdAt\",\"~m1656103970637\",\"updatedAt\",\"~m1666196727621\",\"folder\",\"\",\"screen\",null]]],\"getVoiceToken\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"getVoiceToken\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",null,\"^7\",\"6c59b7bd-9a66-49da-aa46-59d336560a01\",\"^8\",\"Twilio Voice Token\",\"^9\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",true,\"paginationPaginationField\",\"\",\"headers\",\"\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"error\",null,\"privateParams\",[\"^:\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"resourceTypeOverride\",null,\"watchedParams\",[\"^:\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^:\",[[\"^3\",[\"event\",\"success\",\"type\",\"widget\",\"method\",\"updateModel\",\"pluginId\",\"twilioDevice\",\"targetId\",null,\"params\",[\"^3\",[\"model\",\"{\\n data: {{getVoiceToken.data}},\\n}\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"GET\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^:\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^;\",null,\"^<\",null,\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1656023746140\",\"^G\",\"~m1657759435164\",\"^H\",\"\",\"^I\",null]]],\"leadsTable\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"leadsTable\",\"^4\",\"widget\",\"^5\",\"TableWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"showCustomButton\",false,\"sortMappedValue\",[\"^3\",[]],\"_filteredSortedRenderedDataWithTypes\",null,\"heightType\",\"fixed\",\"normalizedData\",null,\"saveChangesDisabled\",\"\",\"columnTypeProperties\",[\"^3\",[\"phoneNumber\",[\"^3\",[]]]],\"columnWidths\",[\"^:\",[]],\"showSummaryFooter\",false,\"disableRowSelectInteraction\",false,\"columnWidthsMobile\",[\"^:\",[]],\"hasNextAfterCursor\",\"\",\"columnTypeSpecificExtras\",[\"^3\",[]],\"onRowAdded\",\"\",\"columnHeaderNames\",[\"^3\",[]],\"alwaysShowPaginator\",false,\"columnColors\",[\"^3\",[\"id\",\"\",\"name\",\"\",\"email\",\"\",\"phoneNumber\",\"\",\"phoneNumer\",\"\",\"phoneNumebr\",\"\",\"sales\",\"\"]],\"columnFrozenAlignments\",[\"^3\",[]],\"allowMultiRowSelect\",false,\"columnFormats\",[\"^3\",[\"phoneNumber\",\"TextDataCell\"]],\"columnRestrictedEditing\",[\"^3\",[]],\"showFilterButton\",true,\"_columnVisibility\",[\"^3\",[\"id\",true,\"name\",true,\"email\",true,\"phoneNumber\",true]],\"_columnSummaryTypes\",[\"^3\",[\"phoneNumber\",\"\"]],\"_columnsWithLegacyBackgroundColor\",[\"~#iOS\",[]],\"showAddRowButton\",false,\"_unfilteredSelectedIndex\",null,\"nextBeforeCursor\",\"\",\"columnVisibility\",[\"^3\",[\"id\",true,\"name\",true,\"email\",true,\"phoneNumber\",true]],\"selectedPageIndex\",\"0\",\"applyDynamicSettingsToColumnOrder\",true,\"rowColor\",[],\"actionButtonColumnName\",\"Actions\",\"resetAfterSave\",true,\"filterStackType\",\"and\",\"downloadRawData\",false,\"showFetchingIndicator\",true,\"serverPaginated\",false,\"data\",\"{{[{\\n \\\"id\\\": 1,\\n \\\"name\\\": \\\"Evan Weiss\\\",\\n \\\"email\\\": \\\"evan@weiss.com\\\",\\n \\\"phoneNumber\\\": \\\"+19255055511\\\"\\n}, {\\n \\\"id\\\": 2,\\n \\\"name\\\": \\\"Sue Shei\\\",\\n \\\"email\\\": \\\"sueshei@example.com\\\",\\n \\\"phoneNumber\\\": \\\"+19255593244\\\"\\n}, {\\n \\\"id\\\": 3,\\n \\\"name\\\": \\\"Jason Response\\\",\\n \\\"email\\\": \\\"jason@response.com\\\",\\n \\\"phoneNumber\\\": \\\"+19255595266\\\"\\n}, {\\n \\\"id\\\": 4,\\n \\\"name\\\": \\\"Cher Actor\\\",\\n \\\"email\\\": \\\"cher@example.com\\\",\\n \\\"phoneNumber\\\": \\\"+19255590643\\\"\\n}, {\\n \\\"id\\\": 5,\\n \\\"name\\\": \\\"Erica Widget\\\",\\n \\\"email\\\": \\\"erica@widget.org\\\",\\n \\\"phoneNumber\\\": \\\"+19255590604\\\"\\n}]}}\",\"displayedData\",null,\"actionButtons\",[\"^:\",[]],\"actionButtonSelectsRow\",true,\"selectRowByDefault\",true,\"defaultSortByColumn\",\"\",\"paginationOffset\",0,\"columnAlignment\",[\"^3\",[\"phoneNumber\",\"left\"]],\"columnSummaries\",[\"^ \"],\"showBoxShadow\",true,\"sortedDesc\",false,\"customButtonName\",\"\",\"columnMappersRenderAsHTML\",[\"^3\",[]],\"showRefreshButton\",true,\"pageSize\",5,\"useDynamicColumnSettings\",false,\"actionButtonPosition\",\"left\",\"dynamicRowHeights\",false,\"bulkUpdateAction\",\"\",\"afterCursor\",\"\",\"onCustomButtonPressQueryName\",\"\",\"changeSet\",[\"^ \"],\"sortedColumn\",\"\",\"_columnSummaryValues\",[\"^3\",[\"phoneNumber\",\"\"]],\"checkboxRowSelect\",true,\"_compatibilityMode\",false,\"showColumnBorders\",false,\"clearSelectionLabel\",\"Clear selection\",\"_renderedDataWithTypes\",null,\"columnAllowOverflow\",[\"^3\",[]],\"beforeCursor\",\"\",\"serverPaginationType\",\"limitOffsetBased\",\"onRowSelect\",\"\",\"showDownloadButton\",true,\"selectedIndex\",null,\"defaultSortDescending\",false,\"_sortedDisplayedDataIndices\",null,\"dynamicColumnSettings\",null,\"totalRowCount\",\"\",\"recordUpdates\",[],\"newRow\",null,\"emptyMessage\",\"No rows found\",\"columnEditable\",[\"^3\",[]],\"_viewerColumnSummaryTypes\",[\"^ \"],\"filters\",[],\"displayedDataIndices\",null,\"disableSorting\",[\"^3\",[]],\"columnMappers\",[\"^3\",[]],\"showClearSelection\",false,\"doubleClickToEdit\",true,\"overflowType\",\"pagination\",\"_reverseSortedDisplayedDataIndices\",null,\"showTableBorder\",true,\"selectedCell\",[\"^ \",\"index\",null,\"data\",null,\"columnName\",null],\"columns\",[\"^:\",[\"id\",\"name\",\"email\",\"phoneNumber\"]],\"defaultSelectedRow\",\"first\",\"freezeActionButtonColumns\",false,\"sort\",null,\"_columns\",[\"^:\",[\"id\",\"name\",\"email\",\"phoneNumber\"]],\"sortByRawValue\",[\"^3\",[]],\"calculatedColumns\",[\"^:\",[]],\"selectedRow\",[\"^ \",\"^K\",null,\"^L\",null],\"showPaginationOnTop\",false,\"_reverseDisplayedDataIndices\",null,\"nextAfterCursor\",\"\",\"useCompactMode\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^=\",\"\",\"^>\",\"body\",\"^?\",\"\",\"row\",1.2000000000000002,\"col\",0,\"^@\",8,\"^A\",8,\"^B\",0]]],\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1656117436953\",\"^G\",\"~m1657759435168\",\"^H\",\"\",\"^I\",null]]],\"leadsTableTitle\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"leadsTableTitle\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"## Leads\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^=\",\"\",\"^>\",\"body\",\"^?\",\"\",\"row\",0,\"col\",0,\"^@\",0.6,\"^A\",1,\"^B\",0]]],\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1656117442328\",\"^G\",\"~m1657759435168\",\"^H\",\"\",\"^I\",null]]],\"phoneNumberEntry\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"phoneNumberEntry\",\"^4\",\"widget\",\"^5\",\"TextInputWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"spellCheck\",false,\"readOnly\",\"\",\"iconAfter\",\"\",\"showCharacterCount\",false,\"autoComplete\",false,\"maxLength\",null,\"hidden\",false,\"customValidation\",\"{{/^\\\\+1[0-9]{10}$/.test(phoneNumberEntry.value) ? '' : 'Please enter a 9-digit US phone number prefixed with \\\"+1\\\"'}}\",\"patternType\",\"\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"_defaultValue\",\"\",\"showClear\",false,\"pattern\",\"\",\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"{{leadsTable.selectedRow.data.phoneNumber}}\",\"labelCaption\",\"\",\"labelWidth\",\"33\",\"autoFill\",\"\",\"placeholder\",\"Phone number\",\"label\",\"\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"\",\"minLength\",null,\"inputTooltip\",\"\",\"events\",[\"^3\",[]],\"autoCapitalize\",\"none\",\"loading\",false,\"disabled\",\"{{ getVoiceToken.error}}\",\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^=\",\"\",\"^>\",\"body\",\"^?\",\"\",\"row\",0.6000000000000002,\"col\",8,\"^@\",1,\"^A\",2,\"^B\",0]]],\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1656117765580\",\"^G\",\"~m1666196741721\",\"^H\",\"\",\"^I\",null]]],\"twilioDevice\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"twilioDevice\",\"^4\",\"widget\",\"^5\",\"CustomComponentWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"allowTopNavigation\",false,\"allowDownloads\",false,\"allowFullscreen\",false,\"allowForms\",false,\"model\",\"{status: \\\"ready_for_call\\\"}\",\"hidden\",\"\",\"allowCamera\",false,\"allowModals\",false,\"allowPopups\",false,\"iframeCode\",\"\\n\\n\\n\\n \\n Embedded Twilio Dialer for Retool\\n \\n \\n \\n
\\n
\\n
\\n\\n \\n \\n \\n \\n\\n\",\"allowMicrophone\",true,\"allowSameOrigin\",true,\"allowPopupsToEscapeSandbox\",false,\"allowPayment\",false,\"allowGeolocation\",false]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^=\",\"\",\"^>\",\"body\",\"^?\",\"\",\"row\",1.5999999999999983,\"col\",8,\"^@\",5.000000000000002,\"^A\",4,\"^B\",0]]],\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1656018956100\",\"^G\",\"~m1657759435169\",\"^H\",\"\",\"^I\",null]]],\"welcomeText\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"welcomeText\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"_defaultValue\",\"\",\"tooltipText\",\"\",\"value\",\"**Welcome to the Twilio dialer demo! Select a lead from the table or enter a phone number to call below!**\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",true]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^=\",\"\",\"^>\",\"body\",\"^?\",\"\",\"row\",0,\"col\",8,\"^@\",0.6,\"^A\",4,\"^B\",0]]],\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1657130540164\",\"^G\",\"~m1657759435169\",\"^H\",\"\",\"^I\",null]]],\"$main\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"$main\",\"^4\",\"frame\",\"^5\",\"Frame\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"type\",\"main\",\"sticky\",false]],\"^;\",[\"^3\",[]],\"^<\",null,\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1662507702498\",\"^G\",\"~m1662507702498\",\"^H\",\"\",\"^I\",null]]],\"getVoiceTokenError\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"getVoiceTokenError\",\"^4\",\"widget\",\"^5\",\"AlertWidget\",\"^6\",null,\"^7\",null,\"^8\",null,\"^9\",[\"^3\",[\"valueToCopy\",\"\",\"internalUrlHashParams\",\"\",\"hidden\",\"{{!getVoiceToken.error}}\",\"exportFileType\",\"csv\",\"buttonType\",\"action\",\"url\",\"\",\"newWindow\",false,\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"\",\"title\",\"getVoiceToken query failed\",\"type\",\"error\",\"exportFileName\",\"\",\"description\",\"{{getVoiceToken.error}}\",\"buttonText\",\"\"]],\"^;\",[\"^3\",[]],\"^<\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^=\",\"\",\"^>\",\"body\",\"^?\",\"\",\"row\",0,\"col\",1,\"^@\",1,\"^A\",7,\"^B\",0]]],\"^C\",null,\"^D\",null,\"^E\",null,\"^=\",\"\",\"^F\",\"~m1666196786615\",\"^G\",\"~m1666196895174\",\"^H\",\"\",\"^I\",null]]]]],\"^F\",null,\"version\",\"2.97.4\",\"appThemeId\",null,\"preloadedAppJavaScript\",\"$(function () {\\n var device;\\n\\n log(\\\"Requesting Access Token...\\\");\\n // Using a relative link to access the Voice Token function\\n $.getJSON(\\\"./token\\\")\\n .then(function (data) {\\n log(\\\"Got a token.\\\");\\n console.log(\\\"Token: \\\" + data.token);\\n\\n // Setup Twilio.Device\\n device = new Twilio.Device(data.token, {\\n // Set Opus as our preferred codec. Opus generally performs better, requiring less bandwidth and\\n // providing better audio quality in restrained network conditions. Opus will be default in 2.0.\\n codecPreferences: [\\\"opus\\\", \\\"pcmu\\\"],\\n // Use fake DTMF tones client-side. Real tones are still sent to the other end of the call,\\n // but the client-side DTMF tones are fake. This prevents the local mic capturing the DTMF tone\\n // a second time and sending the tone twice. This will be default in 2.0.\\n fakeLocalDTMF: true,\\n // Use `enableRingingState` to enable the device to emit the `ringing`\\n // state. The TwiML backend also needs to have the attribute\\n // `answerOnBridge` also set to true in the `Dial` verb. This option\\n // changes the behavior of the SDK to consider a call `ringing` starting\\n // from the connection to the TwiML backend to when the recipient of\\n // the `Dial` verb answers.\\n enableRingingState: true,\\n debug: true,\\n });\\n\\n device.on(\\\"ready\\\", function (device) {\\n log(\\\"Twilio.Device Ready!\\\");\\n });\\n\\n device.on(\\\"error\\\", function (error) {\\n log(\\\"Twilio.Device Error: \\\" + error.message);\\n });\\n\\n device.on(\\\"connect\\\", function (conn) {\\n log('Successfully established call ! ');\\n $('#modal-call-in-progress').modal('show')\\n });\\n\\n device.on(\\\"disconnect\\\", function (conn) {\\n log(\\\"Call ended.\\\");\\n $('.modal').modal('hide')\\n });\\n\\n })\\n .catch(function (err) {\\n console.log(err);\\n log(\\\"Could not get a token from server!\\\");\\n });\\n\\n // Bind button to make call\\n $('#btnDial').bind('click', function () {\\n $('#modal-dial').modal('hide')\\n\\n // get the phone number to connect the call to\\n var params = {\\n To: document.getElementById(\\\"phoneNumber\\\").value\\n };\\n\\n // output destination number\\n $(\\\"#txtPhoneNumber\\\").text(params.To)\\n \\n\\n console.log(\\\"Calling \\\" + params.To + \\\"...\\\");\\n if (device) {\\n var outgoingConnection = device.connect(params);\\n outgoingConnection.on(\\\"ringing\\\", function () {\\n log(\\\"Ringing...\\\");\\n });\\n }\\n\\n })\\n\\n // Bind button to hangup call\\n\\n $('.btnHangUp').bind('click', function () {\\n $('.modal').modal('hide')\\n log(\\\"Hanging up...\\\");\\n if (device) {\\n device.disconnectAll();\\n }\\n })\\n\\n // Activity log\\n function log(message) {\\n var logDiv = document.getElementById(\\\"log\\\");\\n logDiv.innerHTML += \\\"

> \\\" + message + \\\"

\\\";\\n logDiv.scrollTop = logDiv.scrollHeight;\\n }\\n\\n});\\n\",\"preloadedAppJSLinks\",[],\"testEntities\",[],\"tests\",[],\"appStyles\",\"\",\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^:\",[]],\"pageLoadValueOverrides\",[\"^:\",[]],\"customDocumentTitle\",\"\",\"customDocumentTitleEnabled\",false,\"customShortcuts\",[],\"isGlobalWidget\",false,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"folders\",[\"^:\",[]],\"queryStatusVisibility\",true,\"markdownLinkBehavior\",\"auto\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null,\"instrumentationEnabled\",false,\"experimentalPerfFeatures\",[\"^ \",\"batchCommitModelEnabled\",false,\"skipDepCycleCheckingEnabled\",false,\"serverDepGraphEnabled\",false,\"useRuntimeV2\",false],\"experimentalDataTabEnabled\",false]]]"},"changesRecord":[{"type":"WIDGET_REPOSITION2","payload":{"moves":[{"move":{"col":0,"row":-1,"width":0,"height":0},"moveType":"keyboard","widgetIds":["phoneNumberEntry","callButton","twilioDevice"]}],"largeScreen":true},"hideChangelogEntry":false},{"type":"WIDGET_REPOSITION2","payload":{"moves":[{"move":{"col":0,"row":-1,"width":0,"height":0},"moveType":"keyboard","widgetIds":["phoneNumberEntry","callButton","twilioDevice"]}],"largeScreen":true},"hideChangelogEntry":false},{"type":"WIDGET_REPOSITION2","payload":{"moves":[{"move":{"col":0,"row":-1,"width":0,"height":0},"moveType":"keyboard","widgetIds":["phoneNumberEntry","callButton","twilioDevice"]}],"largeScreen":true},"hideChangelogEntry":false},{"type":"WIDGET_REPOSITION2","payload":{"moves":[{"move":{"col":0,"row":-1,"width":0,"height":0},"moveType":"keyboard","widgetIds":["phoneNumberEntry","callButton","twilioDevice"]}],"largeScreen":true},"hideChangelogEntry":false},{"type":"WIDGET_REPOSITION2","payload":{"moves":[{"move":{"col":0,"row":-1,"width":0,"height":0},"moveType":"keyboard","widgetIds":["phoneNumberEntry","callButton","twilioDevice"]},{"move":{"row":-3,"height":0},"widgetIds":["phoneNumberEntry"]},{"move":{"row":-3,"height":0},"widgetIds":["callButton"]},{"move":{"row":-3,"height":0},"widgetIds":["twilioDevice"]}],"largeScreen":true},"hideChangelogEntry":false}],"gitSha":"49c209d35dd6f6405450a4162a84caddf1b6afd0","checksum":null,"createdAt":"2022-10-19T16:28:30.813Z","updatedAt":"2022-10-19T16:31:43.679Z","pageId":343,"userId":57,"branchId":"c930888a-bbea-48c4-a2e1-536965147038"},"modules":{}} -------------------------------------------------------------------------------- /applets/twilio-dialer/code/twilio-functions/twiml-app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if the given value is valid as phone number 3 | * @param {Number|String} number 4 | * @return {Boolean} 5 | */ 6 | function isAValidPhoneNumber(number) { 7 | return /^[\d\+\-\(\) ]+$/.test(number); 8 | } 9 | 10 | exports.handler = function (context, event, callback) { 11 | const twiml = new Twilio.twiml.VoiceResponse(); 12 | 13 | if (event.To) { 14 | /* 15 | * Wrap the phone number or client name in the appropriate TwiML verb 16 | * if is a valid phone number 17 | */ 18 | const attr = isAValidPhoneNumber(event.To) ? 'number' : 'client'; 19 | 20 | const dial = twiml.dial({ 21 | answerOnBridge: true, 22 | callerId: process.env.CALLER_ID, 23 | }); 24 | dial[attr]({}, event.To); 25 | } else { 26 | twiml.say('Thanks for calling!'); 27 | } 28 | 29 | callback(null, twiml); 30 | }; 31 | -------------------------------------------------------------------------------- /applets/twilio-dialer/code/twilio-functions/voice-token.js: -------------------------------------------------------------------------------- 1 | exports.handler = function (context, event, callback) { 2 | 3 | const IDENTITY = event.request.headers.identity; 4 | 5 | const { ACCOUNT_SID } = context; 6 | 7 | // set these values in your .env file 8 | const { TWIML_APPLICATION_SID, API_KEY_SID, API_KEY_SECRET } = context; 9 | 10 | const { AccessToken } = Twilio.jwt; 11 | const { VoiceGrant } = AccessToken; 12 | 13 | const accessToken = new AccessToken(ACCOUNT_SID, API_KEY_SID, API_KEY_SECRET); 14 | accessToken.identity = IDENTITY; 15 | const grant = new VoiceGrant({ 16 | outgoingApplicationSid: TWIML_APPLICATION_SID, 17 | incomingAllow: true, 18 | }); 19 | accessToken.addGrant(grant); 20 | 21 | const response = new Twilio.Response(); 22 | 23 | response.appendHeader('Content-Type', 'application/json'); 24 | response.setBody({ 25 | identity: IDENTITY, 26 | token: accessToken.toJwt(), 27 | }); 28 | callback(null, response); 29 | }; 30 | -------------------------------------------------------------------------------- /applets/twilio-dialer/docs/retool-setup.md: -------------------------------------------------------------------------------- 1 | # Retool Setup 2 | 3 | Make sure you've completed the steps described in the [Twilio Setup](./twilio-setup.md) documentation before proceeding! 4 | 5 | ## 1. Create a Retool REST API Resource to retrieve voice tokens 6 | 7 | In order for your Retool app to authenticate with Twilio, you'll need to get a valid voice token. To set this up: 8 | 9 | 1. Navigate to the `/resources` page in your instance and click "Create a new resource." 10 | 2. Select REST API as the resource type. 11 | 3. Name the resource “Twilio Voice Token.” 12 | 4. Specify the URL of your `voice-token` function as noted in step six of the Twilio setup. 13 | 5. Specify a header with a key of `identity` and a value of `{{current_user.email}}`. This is used to pass the email address of the Retool user to Twilio for use in the token identity. 14 | 6. Save your changes. 15 | 16 | ## 2. Download the Retool app code 17 | 18 | Download [twilio-dialer-retool-app.json](/applets/twilio-dialer/code/retool/twilio-dialer-retool-app.json) from this repository. 19 | 20 | ## 3. Import the Retool app code 21 | On the [Retool main page](https://docs.retool.com/docs/protected-applications-getting-started#importing-the-application), click `Create new` and select `Import an app`. Upload the JSON file containing the app code, and name the app. 22 | 23 | You may need to connect the `getVoiceToken` Resource Query to the REST API Resource you created previously. 24 | 25 | ## 4. Explore the Retool app 26 | 27 | If everything is working as expected, you should now be able to place outbound calls from the Retool app! The app is set up with a hardcoded table of "leads" to simulate a simple call center app. You can edit the contents of the leads table directly, or simply type your phone number in the field to try calling yourself. 28 | 29 | ## 5. Learn about how the custom component works 30 | 31 | You can read more about [Retool custom components](https://docs.retool.com/docs/custom-components). You can also view the code used in the Twilio Dialer component in Retool, the contents of which are included in [retool-custom-component.html](/applets/twilio-dialer/code/retool/retool-custom-component.html) for easy reading! 32 | 33 | Also, take 18 minutes to check out our [video walkthrough](https://youtu.be/xPSS3kp3XEI) of how this app is put together! -------------------------------------------------------------------------------- /applets/twilio-dialer/docs/twilio-setup.md: -------------------------------------------------------------------------------- 1 | # Twilio Setup 2 | Before you can get this Retool app working, you'll need to do some pre-work in Twilio. 3 | 4 | ## 0. Sign up for a Twilio account and get a phone number 5 | 6 | If you don't already have a Twilio account, you'll need to get one! The good news is that Twilio makes it easy (and free!) to get started. Just head over to https://www.twilio.com/try-twilio and sign up! 7 | 8 | As you go through the onboarding flow, indicate that you'll be building a voice app. Once you're logged into Twilio console, you'll be prompted to get a Twilio phone number. Note the phone number you were assigned for later! 9 | 10 | ## 1. Create a TwiML App 11 | 12 | A [TwiML Application](https://www.twilio.com/docs/usage/api/applications) tells Twilio how to handle the calls you'll make from your Retool app. Specify a name for your TwiML app and note the `TwiML App SID` for later. Leave all other settings blank, we'll fill them in later on! 13 | 14 | ## 2. Create an API key/secret pair 15 | 16 | [Create an API key](https://www.twilio.com/docs/iam/keys/api-key-resource) and note the key's `SID` and `Secret` for later. 17 | 18 | ## 3. Create a Twilio Service 19 | 20 | A [Twilio Service](https://www.twilio.com/docs/runtime/functions/create-service) is a container for the server-less functions which make it possible for your Retool app to get a voice access token, and for outbound calls to be routed correctly. 21 | 22 | ## 4. Set Twilio Service environment variables 23 | 24 | In your new Twilio Service, set the following environment variables: 25 | 26 | | Key | Value | 27 | | ---- | ---- | 28 | | `TWIML_APPLICATION_SID` | The `TwiML App SID` you noted in step one | 29 | | `API_KEY_SID` | The `API Key SID` you noted in step two | 30 | | `API_KEY_SECRET` | The `API Key Secret` you noted in step two | 31 | | `CALLER_ID` | The phone number you noted in step zero. Make sure to include a `+` and the country code at the beginning! | 32 | 33 | ## 5. Configure Twilio Service dependencies 34 | 35 | The server-less functions running in Twilio need access to a few software libraries to function properly. Import the following NPM modules: 36 | 37 | | Module | Version | 38 | | --- | --- | 39 | | `lodash` | `4.17.11` | 40 | | `@twilio/runtime-handler` | `1.2.3` | 41 | | `twilio` | `^3.77.3` | 42 | | `xmldom` | `0.1.27` | 43 | | `util` | `0.11.0` | 44 | 45 | ## 6. Create a function to issue voice tokens 46 | 47 | In order for the embedded Twilio dialer to authenticate with Twilio's services, it needs a valid access token. To create a function for issuing voice tokens: 48 | 49 | 1. Add a function 50 | 2. Specify a path `voice-token` 51 | 3. Set the function to `Public`. 52 | - **Heads up! This means that anyone who can guess the URL of your function can issue valid access tokens. You would need to implement authentication on this endpoint for a production use-case!** 53 | 4. Copy and paste the contents of [voice-token.js](/applets/twilio-dialer/code/twilio-functions/voice-token.js) into the function and save 54 | 5. Copy the URL for this function for later use 55 | 56 | ## 7. Create a function to route outbound calls 57 | 58 | In order for Twilio to know what to do when it receives a call from your app, you need a call routing app. To create a call routing function: 59 | 60 | 1. Add a function 61 | 2. Specify a path `twiml-app` 62 | 3. Set the function to `Protected` 63 | 4. Copy and paste the contents of [twiml-app.js](/applets/twilio-dialer/code/twilio-functions/twiml-app.js) into the function and save 64 | 5. Copy the URL for this function for later use 65 | 66 | ## 8. Deploy your Twilio Service! 67 | 68 | This finalizes all of the changes you've made. 69 | 70 | ## 9. Finish configuring your TwiML app 71 | 72 | Remember that empty TwiML app we created in step one? Edit the app settings and provide the URL for your `twiml-app` function as the value of the **Voice Configuration Request URL**. Specify a **Request Method** of `HTTP POST` and save your changes. 73 | 74 | This step connects inbound requests from your Retool app to the call routing function. 75 | 76 | ## Next steps 77 | Next, set up the Retool app to make calls using your Twilio service! 78 | 79 | [Retool Setup](./retool-setup.md) -------------------------------------------------------------------------------- /applets/twilio-dialer/images/twilio-dialer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/applets/twilio-dialer/images/twilio-dialer.png -------------------------------------------------------------------------------- /embed-url-generator/README.md: -------------------------------------------------------------------------------- 1 | # Embed URL Generator / Embedded App Tester 2 | 3 | If you are using [Retool Embed](https://docs.retool.com/docs/retool-embed), we've created a Retool application that lets you preview how your embedded Retool app will look and behave within your parent application. 4 | 5 | **Note**: We recommend using this application in *Incognito Mode* in your browser, to prevent conflicts between the sample embedded user and your original Retool login. We also recommend reading the docs linked above before using, especially the parts about passing in [metadata.](https://docs.retool.com/docs/embed-retool-apps#control-access-and-app-behavior-with-metadata) 6 | 7 | 8 | 9 | ## Setup 10 | The instructions below are outlined within the Retool application. You can import the app and continue in Retool, or follow along below. Once set up, skip to Step 6 to begin using the application. 11 | 12 | ### 0. Make sure you have the proper flags enabled 13 | 14 | This is only for customers who have [Retool Embed](https://retool.com/products/embed) enabled. If you don't have it, but are interested in learning more, use [this link](https://retool.com/products/embed) to book a demo with our team. 15 | 16 | ### 1. Set up API with Embed Scope 17 | 18 | You'll need to create a Retool API token with the `Embed ` Scope. You can access this page by going to your `settings`. Note that you must be an Admin on your instance to set this up. See screenshot below and link to our documentation [here](https://docs.retool.com/docs/embed-retool-apps#1-generate-an-access-token). Save the token to use in the next step. 19 | 20 | 21 | 22 | ## 2. Connect the Retool API as a Resource 23 | 24 | 1. Navigate to the `/resources` page in your instance and click "Create a new resource" 25 | 2. Select REST API as the resource type 26 | 3. Name the resource “Embed URL Generator” 27 | 4. Insert per the below screenshot, updating `` with your Retool domain (eg retool.yourdomain.com) and `` with the token from the prior step: 28 | 29 | 30 | 31 | Save the resource, and it should be available for use by your apps. 32 | 33 | ### 3a. Download app code 34 | Download the app code from the `/code` directory in this repository. 35 | 36 | To do this, clone this Github repo: `git clone https://github.com/tryretool/retool-app-exchange.git` 37 | 38 | (Alternatively, you can manually download `embed_url_generator.json` from the [`code/`](../code) directory.) 39 | 40 | ### 3b. Import the app code 41 | On the [Retool main page](https://docs.retool.com/docs/protected-applications-getting-started#importing-the-application), click `Create new` and select `Import an app`. Upload the JSON file containing the app code, and name the app. 42 | 43 | ### 3c. Rewire the Resource for the postEmbedURL query 44 | Within the app, update the `postEmbedURL` query to point at the resource you created in Step 2. See screenshot below: 45 | 46 | 47 | 48 | ### 4. Get App UUID 49 | Now the Embed URL generator application is set up. To test out embedding an app, we need to find another application within your instance you'd like to use. Navigate to that other application and grab the UUID by either copying it from your app URL or by using the [command palette](https://docs.retool.com/docs/command-palette). See screenshot below for the command palette: 50 | 51 | 52 | 53 | ### 5. Get Group ID 54 | Now that we have an app to embed, we need to pass in a Retool permission group to the API call. You can either use an existing group or create a new group. To learn more about permission groups and how they work with embedded apps, see our docs [here](https://docs.retool.com/docs/embed-retool-apps#2-create-permission-groups-for-your-users). 55 | 56 | 57 | 58 | ### 6. Use the app! 59 | Now we are ready to test out embedded apps! You can use the form on the left of the app under `Submit Request Form` to input different parameters, the JSON on the right under `Submit Request Body` is a preview of what is getting passed to the API in the body, and below the form you can toggle "Preview in Iframe" on to show the app in an iframe below the page, or keep it off to provide the generated embed URL as a link. 60 | 61 | **Note**: Once you have gone through these steps, toggle in the `Setup Complete` tab in the top right of your applciation to navigate to that testing step automatically in the future. 62 | 63 | 64 | ## How to contribute 65 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 66 | -------------------------------------------------------------------------------- /embed-url-generator/images/1_create_api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/embed-url-generator/images/1_create_api_key.png -------------------------------------------------------------------------------- /embed-url-generator/images/2_add_api_generator_as_resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/embed-url-generator/images/2_add_api_generator_as_resource.png -------------------------------------------------------------------------------- /embed-url-generator/images/3_update_app_with_new_resource.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/embed-url-generator/images/3_update_app_with_new_resource.jpeg -------------------------------------------------------------------------------- /embed-url-generator/images/4_copy_app_uuid.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/embed-url-generator/images/4_copy_app_uuid.jpeg -------------------------------------------------------------------------------- /embed-url-generator/images/5_copy_group_id.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/embed-url-generator/images/5_copy_group_id.gif -------------------------------------------------------------------------------- /embed-url-generator/images/6_embed_url_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/embed-url-generator/images/6_embed_url_app.png -------------------------------------------------------------------------------- /incident-central/README.md: -------------------------------------------------------------------------------- 1 | # Incident Central - An incident response hub for engineering teams 2 | 3 |

4 | Incident Central demo gif 5 |

6 | 7 | ## Why you need this 8 | In any engineering organization, things will go wrong. No matter what kind of product you're building, what size you are, or how hard you try, something will break. 9 | 10 | You'll want to develop a robust incident response process, as well as supporting tools to empower people to use the process. For example, a tool can make it easy to report an incident, alert the right people to respond, and give responders a communication channel to collaborate to solve the problem. 11 | 12 | In addition, you want to be able to get a bird's-eye view of incidents—one place you can go to see a list of all open incidents. You also want to be able to find past incidents, in order to review them, reflect on them, and improve. 13 | 14 | ## What does Incident Central do? 15 | Incident Central is a ready-made app that lets you do these things: 16 | 1. Anyone at your company (engineer or not) can report an incident via a simple user interface. When an incident is reported through Incident Central, the following things will happen: 17 | - A unique human-readable incident name will be generated for the incident. 18 | - The PagerDuty on-call rotation for the problematic service will be triggered, thus alerting the on-call. 19 | - A Slack channel for incident response will be created. 20 | - The information above about the incident will be written to a database that you provide, so that you have a means to look up current and past incidents. 21 | 22 | 2. You can see all open incidents, and quickly navigate to the relevant incident Slack channel. 23 | 3. You can drill into a specific incident, and edit its metadata. 24 | 25 | In addition, you can build out any other incident-related functionality you want. (For example, you might decide that you want to also create a Jira ticket every time an incident is opened.) 26 | 27 | In the future, we will likely add a page that displays aggregate incident metrics. Stay tuned. 28 | 29 | ## Screenshots 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | ### Who is this tool for? 42 | You likely want everyone (or most people) at your company to be able to report an incident. Thus, the Report Incident UI within Incident Central is designed to be used by anyone at your company. 43 | 44 | Members of the engineering team will likely use the other functionality: looking up, editing, and reviewing incident data. 45 | 46 | ### Where does this tool fits into the incident response process? 47 | There are five main steps in an incident management process [0]: 48 | 1. Triage 49 | 2. Coordinate 50 | 3. Mitigate 51 | 4. Resolve 52 | 5. Follow-Up 53 | 54 | In Incident Central, the "Report Incident" UI will help kick off Step 1: "Triage", by alerting the right people. It will also set up a communication channel for people to do Step 2: "Coordinate." 55 | 56 | During an incident ("Coordinate"-"Resolve"), people can go to Incident Central to find the link to the incident communication channel and other metadata. 57 | 58 | After an incident, you can use Incident Central to look up information about the incident, as part of Step 5: "Follow-Up." 59 | 60 | 61 | ## Technical Details and Setup 62 | ### Dependencies - backends 63 | The initial version of Incident Central depends on the following backend services: 64 | * Slack 65 | * PagerDuty 66 | * A database (e.g. PostgreSQL or MySQL) 67 | 68 | You are welcome to adapt Incident Central to an alternative backend (for example, perhaps Microsoft Teams instead of Slack). We encourage you to submit your modified version of the app here for others to use. See the **"How to contribute"** section below. 69 | 70 | ### How to set up Incident Central in your Retool instance 71 | See the detailed setup guides in the [Setup Guides folder](./setup-guides). 72 | 73 | As an overview, these guides will take you through: 74 | 75 | 1. Setting up backend dependencies. 76 | - Configuring your Slack and PagerDuty instances. 77 | - Creating a relational database, if you don't have one already. 78 | 2. Setting up these backends as Resources in Retool. 79 | 3. Downloading and configuring the Retool app files. 80 | 81 | 82 | ## What's next? 83 | ### Ideas for extending this app 84 | * Do you use a different chat application instead of Slack? You can swap the Slack REST API calls in this app for calls to your chat application instead. 85 | * Do you want other things to happen when someone reports an incident? (Perhaps, you want a Jira ticket to be created, and a Google Doc to be spun up?) You can extend the Report Incident page to also do these things (e.g. by adding calls to Jira and Google APIs.) 86 | 87 | ### How to contribute 88 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 89 | 90 | ## Appendix 91 | 92 | ### Learn more about incident response process and tooling 93 | As part of the research in building this app, we interviewed an engineer who built incident response tools at Stripe. Check out the blog post: [From checklist to service: scaling Stripe’s incident response](https://retool.com/blog/incident-response-tools-stripe/) 94 | 95 | ### Citations 96 | [0] Five main steps in an incident management process: [Increment magazine](https://increment.com/on-call/when-the-pager-goes-off/) 97 | -------------------------------------------------------------------------------- /incident-central/code/Incident-Central-Home.json: -------------------------------------------------------------------------------- 1 | {"uuid":"d2b883fc-4fc0-11ec-8282-5311b8afe0de","page":{"id":55925865,"data":{"appState":"[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"combinedIncidents\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"combinedIncidents\",\"type\",\"function\",\"subtype\",\"Function\",\"resourceName\",null,\"resourceDisplayName\",null,\"template\",[\"^3\",[\"funcBody\",\"// Combine the PagerDuty and DB data. These should contain the same incidents.\\nconst pdIncidents = {{getPDIncidents.data}};\\nconst dbIncidents = {{getDBIncidents.data}};\\n\\nconst dbIncidentsIdToData = {};\\nformatDataAsArray(dbIncidents).forEach((dbIncident) => {\\n dbIncidentsIdToData[dbIncident.id] = dbIncident;\\n});\\n\\nconst combinedIdToData = {};\\npdIncidents.forEach((pdIncident) => {\\n combinedIdToData[pdIncident.incident_key] = {\\n pd: pdIncident,\\n db: dbIncidentsIdToData[pdIncident.incident_key], \\n }\\n});\\nreturn combinedIdToData;\",\"value\",\"\"]],\"style\",null,\"position2\",null,\"mobilePosition2\",null,\"mobileAppPosition\",null,\"tabIndex\",null,\"createdAt\",\"~m1639619882296\",\"updatedAt\",\"~m1639620866859\",\"container\",\"\",\"folder\",\"filtered_data\",\"screen\",null]]],\"selectedStart\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"selectedStart\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"// Returns the start date in YYYY-MM-DD format.\\nconst shouldUseCustomDateRange = {{useCustomDatesToggle.value}};\\nif (shouldUseCustomDateRange) {\\n return {{dateRangeInput.value.start}};\\n} else {\\n const end = moment();\\n const numDays = parseInt({{daysSelect.value}});\\n const start = moment().subtract(numDays, 'd');\\n return start;\\n}\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639606388980\",\"^?\",\"~m1643594693993\",\"^@\",\"\",\"^A\",\"user_filters\",\"^B\",null]]],\"displayedIncidents\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"displayedIncidents\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"const allIncidents = {{combinedIncidents.value}};\\n\\n// Date range filtering is done in the original data fetch.\\n// We need to do the Status and Priority filters ourselves, because PagerDuty's List Incidents API does \\n// not let us filter by these properties.\\nconst showActive = {{statusCheckboxes.value.includes('Active')}};\\nconst showResolved = {{statusCheckboxes.value.includes('Resolved')}};\\nconst shownPriorities = new Set({{selectedPriorities.value}});\\n\\nreturn Object.values(allIncidents).filter((incident, i) => {\\n if (!showActive && PAGERDUTY.hasActiveStatus(incident.pd)) {\\n return false;\\n }\\n if (!showResolved && !PAGERDUTY.hasActiveStatus(incident.pd)) {\\n return false;\\n }\\n if (incident.pd.priority?.id && !shownPriorities.has(incident.pd.priority.id)) {\\n return false;\\n }\\n return true;\\n});\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639063060804\",\"^?\",\"~m1641854246752\",\"^@\",\"\",\"^A\",\"filtered_data\",\"^B\",null]]],\"getDBIncidents\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"getDBIncidents\",\"^4\",\"datasource\",\"^5\",\"SqlQueryUnified\",\"^6\",\"d9ddb8f4-0a90-45a2-aac2-3185c7341752\",\"^7\",\"Incident Database\",\"^8\",[\"^3\",[\"queryRefreshTime\",\"\",\"records\",\"\",\"lastReceivedFromResourceAt\",null,\"databasePasswordOverride\",\"\",\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",true,\"showFailureToaster\",true,\"query\",\"select\\n\\tid,\\n\\tcreated_at,\\n pagerduty_id,\\n\\tpagerduty_url,\\n\\tslack_channel_name,\\n slack_channel_url,\\n slack_channel_id\\nfrom incidents\\nwhere created_at >= {{selectedStart.value}}\\n and created_at <= {{selectedEnd.value}}\\norder by created_at desc;\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"privateParams\",[\"~#iL\",[]],\"runWhenPageLoadsDelay\",\"\",\"warningCodes\",[\"^C\",[]],\"data\",null,\"recordId\",\"\",\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",false,\"dataArray\",[\"^C\",[]],\"cacheKeyTtl\",\"\",\"filterBy\",\"\",\"databaseHostOverride\",\"\",\"metadata\",null,\"editorMode\",\"sql\",\"actionType\",\"\",\"changesetObject\",\"\",\"shouldUseLegacySql\",false,\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"databaseNameOverride\",\"\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"watchedParams\",[\"^C\",[]],\"enableErrorTransformer\",false,\"enableBulkUpdates\",false,\"showLatestVersionUpdatedWarning\",false,\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"bulkUpdatePrimaryKey\",\"\",\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^C\",[[\"^3\",[\"event\",\"success\",\"type\",\"datasource\",\"method\",\"trigger\",\"pluginId\",\"getPDIncidents\",\"params\",[\"^3\",[]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"tableName\",\"\",\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^C\",[]],\"databaseUsernameOverride\",\"\",\"shouldEnableBatchQuerying\",false,\"doNotThrowOnNoOp\",false,\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639606259563\",\"^?\",\"~m1643594911891\",\"^@\",\"\",\"^A\",\"database\",\"^B\",null]]],\"helper_getOnePageOfPDIncidents\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"helper_getOnePageOfPDIncidents\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",\"8ac5f7df-7b87-4df1-b6f0-e622b75a8bdd\",\"^7\",\"PagerDuty API for Incident Central\",\"^8\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"paginationPaginationField\",\"\",\"headers\",\"[]\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"incidents?limit={{limit}}&offset={{offset}}&sort_by=created_at:desc&since={{sinceDate}}&until={{untilDate}}\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"privateParams\",[\"^C\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",false,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"watchedParams\",[\"^C\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"\",\"events\",[\"^C\",[]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"GET\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^C\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638971262353\",\"^?\",\"~m1643587771167\",\"^@\",\"\",\"^A\",\"pagerduty\",\"^B\",null]]],\"getPagerDutyIncidentPriorities\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"getPagerDutyIncidentPriorities\",\"^4\",\"datasource\",\"^5\",\"RESTQuery\",\"^6\",\"8ac5f7df-7b87-4df1-b6f0-e622b75a8bdd\",\"^7\",\"PagerDuty API for Incident Central\",\"^8\",[\"^3\",[\"queryRefreshTime\",\"\",\"paginationLimit\",\"\",\"body\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",true,\"paginationPaginationField\",\"\",\"headers\",\"[]\",\"showFailureToaster\",true,\"paginationEnabled\",false,\"query\",\"priorities\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"privateParams\",[\"^C\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",false,\"cacheKeyTtl\",\"\",\"cookies\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"watchedParams\",[\"^C\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"paginationDataField\",\"\",\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",true,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"return data.priorities;\",\"events\",[\"^C\",[]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"type\",\"GET\",\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^C\",[]],\"bodyType\",\"json\",\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1643587695025\",\"^?\",\"~m1643587712140\",\"^@\",\"\",\"^A\",\"pagerduty\",\"^B\",null]]],\"listView1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"listView1\",\"^4\",\"widget\",\"^5\",\"ListViewWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"showBorder\",false,\"data\",\"\",\"showDropShadow\",false,\"scroll\",true,\"rowKeys\",\"{{displayedIncidents.value.map(i => i.db.id)}}\",\"formDataKey\",\"{{ self.id }}\",\"style\",[\"^3\",[\"accent-background\",\"hsl(208, 70%, 55%)\",\"primary-surface\",\"\"]],\"dynamicHeightsEnabled\",true,\"itemHeight\",\"3\",\"instances\",\"{{displayedIncidents.value.length}}\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"rowGroup\",\"body\",\"subcontainer\",\"\",\"row\",-8.215650382226158e-15,\"col\",3,\"height\",22.600000000000005,\"width\",9,\"tabNum\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638473334211\",\"^?\",\"~m1639621504177\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"container1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"container1\",\"^4\",\"widget\",\"^5\",\"ContainerWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"_disabledByIndex\",[\"^C\",[\"\"]],\"selectedView\",\"View 1\",\"heightType\",\"auto\",\"currentViewKey\",\"\",\"iconByIndex\",[],\"clickable\",false,\"_viewLabels\",[\"^C\",[]],\"_iconByIndex\",[\"^C\",[\"\"]],\"viewLabels\",[],\"hidden\",false,\"showHeader\",false,\"hoistFetching\",true,\"views\",[],\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"background\",\"{{COLOR.bkgrnd_gray}}\",\"border\",\"{{COLOR.border_green}}\",\"borderRadius\",\"5px\"]],\"hiddenByIndex\",[],\"_hiddenByIndex\",[\"^C\",[\"\"]],\"currentViewIndex\",null,\"_hasMigratedNestedItems\",true,\"transition\",\"none\",\"itemMode\",\"static\",\"_tooltipByIndex\",[\"^C\",[\"\"]],\"tooltipByIndex\",[],\"showFooter\",false,\"_viewKeys\",[\"^C\",[\"View 1\"]],\"events\",[\"^3\",[]],\"_ids\",[\"^C\",[\"\"]],\"viewKeys\",[],\"iconPositionByIndex\",[],\"_iconPositionByIndex\",[\"^C\",[\"\"]],\"loading\",false,\"overflowType\",\"scroll\",\"_labels\",[\"^C\",[\"\"]],\"styleContext\",[\"^3\",[\"textDark\",\"\"]],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"labels\",[]]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"listView1\",\"^D\",\"body\",\"^E\",\"\",\"row\",0,\"col\",0,\"^F\",6,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638473393858\",\"^?\",\"~m1639348307890\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"containerTitle3\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"containerTitle3\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"###### Container title\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"header\",\"^E\",\"\",\"row\",0,\"col\",0,\"^F\",0.6,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638473394207\",\"^?\",\"~m1638909430659\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text3\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text3\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"#### [{{displayedIncidents.value[i].pd?.priority?.name}}] {{displayedIncidents.value[i].db.id}}\",\"style\",[\"^3\",[\"color\",\"\"]],\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",0.8,\"col\",2,\"^F\",0.6,\"^G\",9,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638970252749\",\"^?\",\"~m1639623910265\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text4\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text4\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"###### {{displayedIncidents.value[i].pd.title}}\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",1.8000000000000003,\"col\",2,\"^F\",0.6,\"^G\",5,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638970293894\",\"^?\",\"~m1639175029435\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"tags1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"tags1\",\"^4\",\"widget\",\"^5\",\"TagsWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"hidden\",false,\"showInEditor\",false,\"colors\",\"[{{TAGS[displayedIncidents.value[i].pd.status].color}}]\",\"tooltipText\",\"\",\"value\",\"[{{TAGS[displayedIncidents.value[i].pd.status].text}}]\",\"allowWrap\",true,\"hashColors\",true,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",0.7999999999999996,\"col\",0,\"^F\",0.8,\"^G\",2,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638970331419\",\"^?\",\"~m1639067584109\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text6\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text6\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"**Assigned to** {{displayedIncidents.value[i].pd.assignments.map(a => a.assignee.summary).join(', ')}}\",\"style\",[\"^3\",[\"color\",\"rgba(106, 106, 106, 0.9)\"]],\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",3.000000000000003,\"col\",2,\"^F\",0.6,\"^G\",5,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638970439276\",\"^?\",\"~m1639321829190\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"detailsLink\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"detailsLink\",\"^4\",\"widget\",\"^5\",\"LinkWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"clickable\",false,\"iconAfter\",\"\",\"hidden\",false,\"text\",\"View details\",\"showUnderline\",\"always\",\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"text\",\"{{COLOR.link_green}}\"]],\"allowWrap\",false,\"iconBefore\",\"bold/interface-link-square-alternate\",\"events\",[\"^C\",[[\"^3\",[\"event\",\"click\",\"type\",\"util\",\"method\",\"openApp\",\"pluginId\",\"\",\"params\",[\"^3\",[\"options\",[\"^3\",[\"newTab\",true,\"queryParams\",[\"^C\",[[\"^3\",[\"key\",\"\",\"value\",\"\"]]]],\"hashParams\",[\"^C\",[[\"^3\",[\"key\",\"incident_name\",\"value\",\"{{displayedIncidents.value[i].pd.incident_key}}\"]]]]]],\"uuid\",\"f601490a-5913-11ec-9083-a700d6721666\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",3.5999999999999974,\"col\",8,\"^F\",0.6,\"^G\",4,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638970508087\",\"^?\",\"~m1639321105708\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text8\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text8\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"right\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",1.8,\"col\",7,\"^F\",0.6,\"^G\",1,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638980352132\",\"^?\",\"~m1639518201445\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text9\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text9\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"right\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",2.4,\"col\",7,\"^F\",0.6,\"^G\",1,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638980549104\",\"^?\",\"~m1638980630605\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text11\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text11\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"**Created** {{moment(displayedIncidents.value[i].pd.created_at).format('ddd, MMM Do YYYY, h:mm a')}}\",\"style\",[\"^3\",[\"color\",\"#6a6a6a\"]],\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",2.4000000000000017,\"col\",2,\"^F\",0.6,\"^G\",5,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638980784910\",\"^?\",\"~m1639321825570\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"incidentNavbar1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"incidentNavbar1\",\"^4\",\"widget\",\"^5\",\"GlobalWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"showFetchingIndicator\",false,\"name\",\"Incident-Navbar\",\"pageUuid\",\"cd581076-585d-11ec-a633-936fd80bda50\",\"childNamespace\",\"incidentNavbar1\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^D\",\"body\",\"^E\",\"header\",\"row\",0,\"col\",0,\"^F\",1,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638992365312\",\"^?\",\"~m1638992365312\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"container6\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"container6\",\"^4\",\"widget\",\"^5\",\"ContainerWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"_disabledByIndex\",[\"^C\",[\"\"]],\"selectedView\",\"View 1\",\"heightType\",\"auto\",\"currentViewKey\",\"\",\"iconByIndex\",[],\"clickable\",false,\"_viewLabels\",[\"^C\",[]],\"_iconByIndex\",[\"^C\",[\"\"]],\"viewLabels\",[],\"hidden\",false,\"showHeader\",false,\"hoistFetching\",true,\"views\",[],\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"background\",\"{{COLOR.bkgrnd_gray}}\",\"border\",\"#ffffff\",\"borderRadius\",\"5px\"]],\"hiddenByIndex\",[],\"_hiddenByIndex\",[\"^C\",[\"\"]],\"currentViewIndex\",null,\"_hasMigratedNestedItems\",true,\"transition\",\"none\",\"itemMode\",\"static\",\"_tooltipByIndex\",[\"^C\",[\"\"]],\"tooltipByIndex\",[],\"showFooter\",false,\"_viewKeys\",[\"^C\",[\"View 1\"]],\"events\",[\"^3\",[]],\"_ids\",[\"^C\",[\"\"]],\"viewKeys\",[],\"iconPositionByIndex\",[],\"_iconPositionByIndex\",[\"^C\",[\"\"]],\"loading\",false,\"overflowType\",\"scroll\",\"_labels\",[\"^C\",[\"\"]],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"labels\",[]]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"\",\"^D\",\"body\",\"^E\",\"\",\"row\",1.609823385706477e-15,\"col\",0,\"^F\",13.600000000000001,\"^G\",3,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639056008585\",\"^?\",\"~m1639583561968\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"containerTitle8\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"containerTitle8\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"#### Container title\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"header\",\"^E\",\"\",\"row\",0,\"col\",0,\"^F\",0.6,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639056008793\",\"^?\",\"~m1639056008793\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"daysSelect\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"daysSelect\",\"^4\",\"widget\",\"^5\",\"SelectWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"_disabledByIndex\",[\"^C\",[]],\"showSelectionIndicator\",true,\"_values\",[\"^C\",[]],\"values\",\"['7', '14', '30', '60']\",\"readOnly\",false,\"iconAfter\",\"\",\"inputValue\",\"\",\"_data\",[\"^C\",[]],\"hidden\",false,\"customValidation\",\"\",\"data\",[],\"searchMode\",\"fuzzy\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"selectedItem\",null,\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"showClear\",false,\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"'30'\",\"hiddenByIndex\",[],\"labelCaption\",\"\",\"hideLabel\",true,\"labelWidth\",\"33\",\"deprecatedLabels\",[],\"_hiddenByIndex\",[\"^C\",[]],\"placeholder\",\"Select an option\",\"_captionByIndex\",[\"^C\",[]],\"label\",\"Time period\",\"_hasMigratedNestedItems\",false,\"captionByIndex\",[],\"_validate\",false,\"itemMode\",\"dynamic\",\"labelWidthUnit\",\"%\",\"allowCustomValue\",false,\"invalid\",false,\"selectedIndex\",null,\"_tooltipByIndex\",[\"^C\",[]],\"tooltipByIndex\",[],\"iconBefore\",\"\",\"optionMode\",\"dynamic\",\"selectedLabel\",\"\",\"events\",[\"^3\",[]],\"_ids\",[\"^C\",[]],\"emptyMessage\",\"\",\"overlayMaxHeight\",null,\"loading\",false,\"disabled\",\"{{useCustomDatesToggle.value}}\",\"labelPosition\",\"top\",\"_labels\",[\"^C\",[]],\"labelWrap\",false,\"disabledValues\",[],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"required\",false,\"labels\",\"['Last 7 days', 'Last 14 days', 'Last 30 days', 'Last 60 days']\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",3.4000000000000004,\"col\",1,\"^F\",1,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639056199817\",\"^?\",\"~m1639584385297\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"spacer1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"spacer1\",\"^4\",\"widget\",\"^5\",\"SpacerWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"hidden\",false,\"maintainSpaceWhenHidden\",false,\"showInEditor\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",4.600000000000001,\"col\",0,\"^F\",0.6000000000000001,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639056586466\",\"^?\",\"~m1639056586466\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"spacer2\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"spacer2\",\"^4\",\"widget\",\"^5\",\"SpacerWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"hidden\",false,\"maintainSpaceWhenHidden\",false,\"showInEditor\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",0.2,\"col\",0,\"^F\",0.6000000000000001,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639056671973\",\"^?\",\"~m1639056671973\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"statusCheckboxes\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"statusCheckboxes\",\"^4\",\"widget\",\"^5\",\"CheckboxGroupWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"_disabledByIndex\",[\"^C\",[]],\"_values\",[\"^C\",[]],\"values\",\"['Active', 'Resolved']\",\"selectedIndexes\",[],\"selectedItems\",[],\"_data\",[\"^C\",[]],\"groupLayout\",\"multiColumn\",\"hidden\",false,\"customValidation\",\"\",\"data\",[],\"maxCount\",null,\"hideValidationMessage\",false,\"validationMessage\",\"\",\"showInEditor\",false,\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"['Active']\",\"style\",[\"^3\",[\"checkedBackground\",\"{{COLOR.link_green}}\"]],\"hiddenByIndex\",[],\"labelCaption\",\"\",\"labelWidth\",\"33\",\"deprecatedLabels\",[],\"_hiddenByIndex\",[\"^C\",[]],\"_captionByIndex\",[\"^C\",[]],\"label\",\"\",\"_hasMigratedNestedItems\",false,\"captionByIndex\",[],\"_validate\",false,\"itemMode\",\"dynamic\",\"labelWidthUnit\",\"%\",\"invalid\",false,\"_tooltipByIndex\",[\"^C\",[]],\"tooltipByIndex\",[],\"optionMode\",\"dynamic\",\"events\",[\"^3\",[]],\"_ids\",[\"^C\",[]],\"selectedLabels\",[],\"disabled\",false,\"labelPosition\",\"top\",\"minCount\",null,\"_labels\",[\"^C\",[]],\"labelWrap\",false,\"disabledValues\",[],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"required\",false,\"labels\",\"\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",0.8,\"col\",1,\"^F\",0.6,\"^G\",11,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639057118430\",\"^?\",\"~m1639323123983\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"spacer3\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"spacer3\",\"^4\",\"widget\",\"^5\",\"SpacerWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"hidden\",false,\"maintainSpaceWhenHidden\",false,\"showInEditor\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"listView1\",\"^D\",\"body\",\"^E\",\"\",\"row\",6,\"col\",0,\"^F\",0.4,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639061881071\",\"^?\",\"~m1639061881071\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"slackLink\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"slackLink\",\"^4\",\"widget\",\"^5\",\"LinkWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"clickable\",false,\"iconAfter\",\"\",\"hidden\",false,\"text\",\"#{{displayedIncidents.value[i].db?.slack_channel_name}}\",\"showUnderline\",\"always\",\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"text\",\"{{COLOR.link_green}}\"]],\"allowWrap\",false,\"iconBefore\",\"\",\"events\",[\"^C\",[[\"^3\",[\"event\",\"click\",\"type\",\"util\",\"method\",\"openUrl\",\"pluginId\",\"\",\"params\",[\"^3\",[\"options\",[\"^3\",[\"newTab\",true]],\"url\",\"{{displayedIncidents.value[i].db?.slack_channel_url}}\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",1.9999999999999998,\"col\",8,\"^F\",0.6,\"^G\",4,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639065899723\",\"^?\",\"~m1639320677796\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"pdLink\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"pdLink\",\"^4\",\"widget\",\"^5\",\"LinkWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"clickable\",false,\"iconAfter\",\"\",\"hidden\",false,\"text\",\"Pagerduty profile\",\"showUnderline\",\"always\",\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"text\",\"{{COLOR.link_green}}\"]],\"allowWrap\",false,\"iconBefore\",\"\",\"events\",[\"^C\",[[\"^3\",[\"event\",\"click\",\"type\",\"util\",\"method\",\"openUrl\",\"pluginId\",\"\",\"params\",[\"^3\",[\"url\",\"{{displayedIncidents.value[i].db?.pagerduty_url}}\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",2.8000000000000007,\"col\",8,\"^F\",0.6,\"^G\",4,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639065912486\",\"^?\",\"~m1639321096245\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"$main\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"$main\",\"^4\",\"frame\",\"^5\",\"Frame\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"type\",\"main\",\"sticky\",false,\"style\",[\"^3\",[\"canvas\",\"#ffffff\"]]]],\"^9\",[\"^3\",[]],\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639236967805\",\"^?\",\"~m1639236971888\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"divider1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"divider1\",\"^4\",\"widget\",\"^5\",\"DividerWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"hidden\",false,\"maintainSpaceWhenHidden\",false,\"showInEditor\",false,\"tooltipText\",\"\",\"horizontalAlign\",\"center\",\"text\",\"\",\"textSize\",\"default\",\"style\",[\"^3\",[\"color\",\"{{COLOR.purple}}\"]]]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container1\",\"^D\",\"body\",\"^E\",\"\",\"row\",0,\"col\",0,\"^F\",0.2,\"^G\",12,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639322649174\",\"^?\",\"~m1639322667169\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text12\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text12\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"###### Status\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",0.2,\"col\",1,\"^F\",0.6,\"^G\",11,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639323142313\",\"^?\",\"~m1639323156363\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text13\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text13\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"###### Created\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",2.8000000000000007,\"col\",1,\"^F\",0.6,\"^G\",11,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639323167363\",\"^?\",\"~m1639323175923\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"useCustomDatesToggle\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"useCustomDatesToggle\",\"^4\",\"widget\",\"^5\",\"ToggleLinkWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"iconPosition\",\"before\",\"hidden\",false,\"text\",\"{{ self.value ? 'Use presets' : 'Set custom date range' }}\",\"showUnderline\",\"always\",\"showInEditor\",false,\"tooltipText\",\"\",\"value\",false,\"style\",[\"^3\",[\"text\",\"{{COLOR.link_gray}}\"]],\"allowWrap\",false,\"iconType\",\"caret\",\"events\",[\"^3\",[]],\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",4.3999999999999995,\"col\",1,\"^F\",0.6,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639582639052\",\"^?\",\"~m1639584501920\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"priorityMultiSelect\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"priorityMultiSelect\",\"^4\",\"widget\",\"^5\",\"MultiselectWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"_disabledByIndex\",[\"^C\",[]],\"showSelectionIndicator\",true,\"_values\",[\"^C\",[]],\"values\",\"{{ getPagerDutyIncidentPriorities.data.map(priority => priority.id) }}\",\"readOnly\",false,\"iconAfter\",\"\",\"wrapTags\",true,\"selectedIndexes\",[],\"selectedItems\",[],\"inputValue\",\"\",\"_data\",[\"^C\",[]],\"hidden\",\"{{!useCustomPrioritiesToggle.value}}\",\"customValidation\",\"\",\"data\",[],\"maxCount\",null,\"searchMode\",\"fuzzy\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"showClear\",false,\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"{{ getPagerDutyIncidentPriorities.data.map(priority => priority.id) }}\",\"hiddenByIndex\",[],\"labelCaption\",\"\",\"hideLabel\",true,\"labelWidth\",\"33\",\"deprecatedLabels\",[],\"_hiddenByIndex\",[\"^C\",[]],\"placeholder\",\"Select multiple options\",\"_captionByIndex\",[\"^C\",[]],\"label\",\"Priority\",\"_hasMigratedNestedItems\",false,\"captionByIndex\",[],\"_validate\",false,\"itemMode\",\"dynamic\",\"labelWidthUnit\",\"%\",\"allowCustomValue\",false,\"invalid\",false,\"_tooltipByIndex\",[\"^C\",[]],\"tooltipByIndex\",[],\"iconBefore\",\"\",\"optionMode\",\"dynamic\",\"events\",[\"^3\",[]],\"_ids\",[\"^C\",[]],\"emptyMessage\",\"\",\"selectedLabels\",[],\"overlayMaxHeight\",null,\"loading\",false,\"disabled\",false,\"labelPosition\",\"top\",\"minCount\",null,\"_labels\",[\"^C\",[]],\"labelWrap\",false,\"disabledValues\",[],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"required\",false,\"labels\",\"{{ getPagerDutyIncidentPriorities.data.map(priority => (priority.description ? `${priority.name} - ${priority.description}` : priority.name)) }}\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",9.000000000000004,\"col\",1,\"^F\",1,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639583372179\",\"^?\",\"~m1639584403990\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"text15\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"text15\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"auto\",\"horizontalAlign\",\"left\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"###### Priority\",\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",6.799999999999999,\"col\",1,\"^F\",0.6,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639583388887\",\"^?\",\"~m1639583419304\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"spacer4\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"spacer4\",\"^4\",\"widget\",\"^5\",\"SpacerWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"hidden\",false,\"maintainSpaceWhenHidden\",false,\"showInEditor\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",10.000000000000009,\"col\",1,\"^F\",1,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639583583328\",\"^?\",\"~m1639583583328\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"prioritySelect\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"prioritySelect\",\"^4\",\"widget\",\"^5\",\"SelectWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"_disabledByIndex\",[\"^C\",[]],\"showSelectionIndicator\",true,\"_values\",[\"^C\",[]],\"values\",\"{{ getPagerDutyIncidentPriorities.data.map(priority => priority.id).concat(['Any']) }}\",\"readOnly\",false,\"iconAfter\",\"\",\"inputValue\",\"\",\"_data\",[\"^C\",[]],\"hidden\",false,\"customValidation\",\"\",\"data\",[],\"searchMode\",\"fuzzy\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"selectedItem\",null,\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"showClear\",false,\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",\"Any\",\"hiddenByIndex\",[],\"labelCaption\",\"\",\"hideLabel\",true,\"labelWidth\",\"33\",\"deprecatedLabels\",[],\"_hiddenByIndex\",[\"^C\",[]],\"placeholder\",\"Select an option\",\"_captionByIndex\",[\"^C\",[]],\"label\",\"Priority\",\"_hasMigratedNestedItems\",false,\"captionByIndex\",[],\"_validate\",false,\"itemMode\",\"dynamic\",\"labelWidthUnit\",\"%\",\"allowCustomValue\",false,\"invalid\",false,\"selectedIndex\",null,\"_tooltipByIndex\",[\"^C\",[]],\"tooltipByIndex\",[],\"iconBefore\",\"\",\"selectedLabel\",\"\",\"events\",[\"^3\",[]],\"_ids\",[\"^C\",[]],\"emptyMessage\",\"\",\"overlayMaxHeight\",null,\"loading\",false,\"disabled\",\"{{useCustomPrioritiesToggle.value}}\",\"labelPosition\",\"left\",\"_labels\",[\"^C\",[]],\"labelWrap\",false,\"disabledValues\",[],\"disabledByIndex\",[],\"maintainSpaceWhenHidden\",false,\"required\",false,\"labels\",\"{{ getPagerDutyIncidentPriorities.data.map(priority => (priority.description ? `${priority.name} - ${priority.description}` : priority.name)) }}\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",7.400000000000001,\"col\",1,\"^F\",1,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639583684566\",\"^?\",\"~m1639602849548\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"useCustomPrioritiesToggle\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"useCustomPrioritiesToggle\",\"^4\",\"widget\",\"^5\",\"ToggleLinkWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"iconPosition\",\"before\",\"hidden\",false,\"text\",\"{{ self.value ? 'Use presets' : 'Set custom filters' }}\",\"showUnderline\",\"always\",\"showInEditor\",false,\"tooltipText\",\"\",\"value\",false,\"style\",[\"^3\",[\"text\",\"{{COLOR.link_gray}}\"]],\"allowWrap\",false,\"iconType\",\"caret\",\"events\",[\"^3\",[]],\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",8.400000000000004,\"col\",1,\"^F\",0.6,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639583917681\",\"^?\",\"~m1639584510997\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"getPDIncidents\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"getPDIncidents\",\"^4\",\"datasource\",\"^5\",\"JavascriptQuery\",\"^6\",\"JavascriptQuery\",\"^7\",null,\"^8\",[\"^3\",[\"queryRefreshTime\",\"\",\"lastReceivedFromResourceAt\",null,\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"showFailureToaster\",true,\"query\",\"/*\\n * Get incidents from PagerDuty.\\n */\\n// The logic below will fetch 50 incidents at a time from PagerDuty.\\nconst PAGE_SIZE = 50;\\n// This Query won't return more than 1000 incidents. You can change this upper bound.\\nconst MAX = 1000;\\n\\nlet allPDIncidents = [];\\nlet offset = 0;\\nlet hasMore = true;\\n\\nwhile (hasMore && allPDIncidents.length <= MAX - PAGE_SIZE) {\\n\\tconst resp = await helper_getOnePageOfPDIncidents.trigger({\\n additionalScope: {\\n \\toffset: offset,\\n limit: PAGE_SIZE,\\n sinceDate: selectedStart.value,\\n untilDate: selectedEnd.value,\\n \\t}\\n });\\n if (!resp) {\\n hasMore = false;\\n break;\\n }\\n \\n allPDIncidents = allPDIncidents.concat(resp.incidents);\\n \\n offset += PAGE_SIZE;\\n hasMore = resp.more;\\n}\\n\\nreturn allPDIncidents;\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"privateParams\",[\"^C\",[]],\"runWhenPageLoadsDelay\",\"\",\"data\",null,\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",false,\"cacheKeyTtl\",\"\",\"metadata\",null,\"changesetObject\",\"\",\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"watchedParams\",[\"^C\",[]],\"enableErrorTransformer\",false,\"showLatestVersionUpdatedWarning\",false,\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^C\",[]],\"queryTimeout\",\"10000\",\"requireConfirmation\",false,\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^C\",[]],\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1638976882574\",\"^?\",\"~m1643594794207\",\"^@\",\"\",\"^A\",\"pagerduty\",\"^B\",null]]],\"dateRangeInput\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"dateRangeInput\",\"^4\",\"widget\",\"^5\",\"DateRangeWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"minDate\",\"\",\"readOnly\",false,\"iconAfter\",\"\",\"dateFormat\",\"MMM d, yyyy\",\"hidden\",\"{{!useCustomDatesToggle.value}}\",\"customValidation\",\"{{moment(self.value.end).diff(self.value.start, 'd') > 60 ? 'Date range must be at most 60 days' : null}}\",\"hideValidationMessage\",false,\"textBefore\",\"\",\"validationMessage\",\"\",\"textAfter\",\"\",\"showInEditor\",false,\"showClear\",false,\"tooltipText\",\"\",\"labelAlign\",\"left\",\"formDataKey\",\"{{ self.id }}\",\"value\",[\"^3\",[\"start\",\"{{ moment().subtract(30, 'd') }}\",\"end\",\"{{moment()}}\"]],\"startPlaceholder\",\"Start date\",\"labelCaption\",\"\",\"hideLabel\",true,\"maxDate\",\"\",\"labelWidth\",\"33\",\"label\",\"Date range\",\"formattedValue\",\"\",\"_validate\",false,\"labelWidthUnit\",\"%\",\"invalid\",false,\"iconBefore\",\"bold/interface-calendar-remove\",\"endPlaceholder\",\"End date\",\"events\",[\"^3\",[]],\"textBetween\",\"-\",\"disabled\",false,\"labelPosition\",\"left\",\"labelWrap\",false,\"maintainSpaceWhenHidden\",false,\"required\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^@\",\"container6\",\"^D\",\"body\",\"^E\",\"\",\"row\",4.999999999999998,\"col\",1,\"^F\",1,\"^G\",10,\"^H\",0]]],\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639602605697\",\"^?\",\"~m1643594739379\",\"^@\",\"\",\"^A\",\"\",\"^B\",null]]],\"selectedEnd\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"selectedEnd\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"// Returns the end date in YYYY-MM-DD format.\\nconst shouldUseCustomDateRange = {{useCustomDatesToggle.value}};\\nif (shouldUseCustomDateRange) {\\n return {{dateRangeInput.value.end}};\\n} else {\\n return moment();\\n}\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639606902007\",\"^?\",\"~m1643594709100\",\"^@\",\"\",\"^A\",\"user_filters\",\"^B\",null]]],\"selectedPriorities\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"selectedPriorities\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"// Returns an array of PagerDuty priority IDs\\nconst shouldUseCustomPriorities = {{useCustomPrioritiesToggle.value}};\\nif (shouldUseCustomPriorities) {\\n return {{priorityMultiSelect.value}};\\n} else {\\n const selectedPriority = {{prioritySelect.value}};\\n if (selectedPriority === 'Any') {\\n return {{getPagerDutyIncidentPriorities.data.map(p => p.id)}};\\n }\\n return [selectedPriority];\\n}\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639607346737\",\"^?\",\"~m1639625669571\",\"^@\",\"\",\"^A\",\"user_filters\",\"^B\",null]]],\"pdPriorityIdToName\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"pdPriorityIdToName\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"const idToName = {};\\nconst priorities = {{getPagerDutyIncidentPriorities.data}};\\npriorities.forEach(p => {\\n idToName[p.id] = p.name;\\n});\\n\\nreturn idToName;\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",\"~m1639622080150\",\"^?\",\"~m1639622149776\",\"^@\",\"\",\"^A\",\"pagerduty\",\"^B\",null]]]]],\"^>\",null,\"version\",\"2.84.7\",\"appThemeId\",null,\"preloadedAppJavaScript\",\"window.SLACK = {\\n TEAM_ID: 'T02NCPCBJUS',\\n createDeepLink: (channelId) => {\\n return `slack://channel?team=${window.SLACK.TEAM_ID}&id=${channelId}`\\n },\\n};\\n\\nwindow.PAGERDUTY = {\\n hasActiveStatus: (pdIncident) => {\\n return pdIncident.status != 'resolved';\\n }\\n};\\n\\nconst SALMON = '#EF4E4E';\\nconst SLATE = '#243B53';\\n\\nwindow.TAGS = {\\n // These status names correspond to statuses in PagerDuty.\\n triggered: {\\n color: SALMON,\\n text: 'Triggered'\\n },\\n acknowledged: {\\n color: SALMON,\\n text: 'Acknowledged',\\n },\\n resolved: {\\n color: SLATE,\\n text: 'Resolved',\\n }\\n}\\n\\nwindow.COLOR = {\\n purple: '#0C008C',\\n link_green: '#199473',\\n link_gray: '#627D98',\\n bkgrnd_gray: '#F0F4F8',\\n border_green: '#EFFCF6',\\n};\",\"preloadedAppJSLinks\",[],\"appStyles\",\".retool-canvas {\\n max-width: none !important;\\n}\\n\\n._retool-container-listView1 {\\n margin-top: -30px;\\n}\",\"testEntities\",[],\"tests\",[],\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^C\",[]],\"pageLoadValueOverrides\",[\"^C\",[]],\"isGlobalWidget\",false,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"instrumentationEnabled\",false,\"customDocumentTitleEnabled\",false,\"customDocumentTitle\",\"\",\"customShortcuts\",[],\"folders\",[\"^C\",[\"pagerduty\",\"database\",\"filtered_data\",\"user_filters\"]],\"markdownLinkBehavior\",\"never\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null]]]"},"changesRecord":[{"type":"DATASOURCE_TYPE_CHANGE","payload":{"newType":"SqlQueryUnified","pluginId":"getDBIncidents","resourceName":"d9ddb8f4-0a90-45a2-aac2-3185c7341752"}},{"type":"WIDGET_TEMPLATE_UPDATE","payload":{"plugin":{"id":"getDBIncidents","type":"datasource","style":null,"folder":"database","screen":null,"subtype":"SqlQueryUnified","tabIndex":null,"template":{"data":null,"query":"","events":[],"rawData":null,"records":"","filterBy":"","metadata":null,"recordId":"","changeset":"","dataArray":[],"tableName":"","timestamp":0,"actionType":"","editorMode":"sql","isFetching":false,"isImported":false,"cacheKeyTtl":"","transformer":"// type your code here\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\nreturn data","queryTimeout":"10000","allowedGroups":[],"enableCaching":false,"privateParams":[],"queryDisabled":"","watchedParams":[],"successMessage":"","changesetObject":"","doNotThrowOnNoOp":false,"errorTransformer":"// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error","queryRefreshTime":"","runWhenPageLoads":false,"changesetIsObject":false,"enableBulkUpdates":false,"enableTransformer":false,"queryThrottleTime":"750","queryTriggerDelay":"0","shouldUseLegacySql":false,"showFailureToaster":true,"showSuccessToaster":true,"importedQueryInputs":{},"playgroundQueryUuid":"","requireConfirmation":false,"runWhenModelUpdates":true,"bulkUpdatePrimaryKey":"","databaseHostOverride":"","databaseNameOverride":"","notificationDuration":"","queryDisabledMessage":"","resourceNameOverride":"","importedQueryDefaults":{},"playgroundQuerySaveId":"latest","runWhenPageLoadsDelay":"","enableErrorTransformer":false,"queryFailureConditions":"","databasePasswordOverride":"","databaseUsernameOverride":"","shouldEnableBatchQuerying":false,"updateSetValueDynamically":false,"showLatestVersionUpdatedWarning":false,"showUpdateSetValueDynamicallyToggle":true},"container":"","createdAt":"2021-12-15T22:10:59.563Z","namespace":null,"position2":null,"updatedAt":"2022-01-31T02:03:37.976Z","resourceName":"d9ddb8f4-0a90-45a2-aac2-3185c7341752","mobilePosition2":null,"mobileAppPosition":null},"update":{"data":null,"query":"select\n\tid,\n\tcreated_at,\n pagerduty_id,\n\tpagerduty_url,\n\tslack_channel_name,\n slack_channel_url,\n slack_channel_id\nfrom incidents\nwhere created_at >= {{selectedStart.value}}\n and created_at <= {{selectedEnd.value}}\norder by created_at desc;","events":[{"type":"datasource","event":"success","method":"trigger","params":{},"waitMs":"0","pluginId":"getPDIncidents","waitType":"debounce"}],"rawData":null,"records":"","filterBy":"","metadata":null,"recordId":"","changeset":"","dataArray":[],"tableName":"","timestamp":0,"actionType":"","editorMode":"sql","isFetching":false,"isImported":false,"cacheKeyTtl":"","transformer":"// type your code here\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\nreturn data","queryTimeout":"10000","warningCodes":[],"allowedGroups":[],"enableCaching":false,"privateParams":[],"queryDisabled":"","watchedParams":[],"successMessage":"","changesetObject":"","doNotThrowOnNoOp":false,"errorTransformer":"// The variable 'data' allows you to reference the request's data in the transformer. \n// example: return data.find(element => element.isError)\nreturn data.error","queryRefreshTime":"","runWhenPageLoads":false,"changesetIsObject":false,"enableBulkUpdates":false,"enableTransformer":false,"playgroundQueryId":null,"queryThrottleTime":"750","queryTriggerDelay":"0","shouldUseLegacySql":false,"showFailureToaster":true,"showSuccessToaster":false,"confirmationMessage":null,"importedQueryInputs":{},"playgroundQueryUuid":"","requireConfirmation":false,"runWhenModelUpdates":true,"bulkUpdatePrimaryKey":"","databaseHostOverride":"","databaseNameOverride":"","notificationDuration":"","queryDisabledMessage":"","resourceNameOverride":"","importedQueryDefaults":{},"playgroundQuerySaveId":"latest","runWhenPageLoadsDelay":"","enableErrorTransformer":false,"queryFailureConditions":"","databasePasswordOverride":"","databaseUsernameOverride":"","shouldEnableBatchQuerying":false,"updateSetValueDynamically":false,"lastReceivedFromResourceAt":null,"showLatestVersionUpdatedWarning":false,"showUpdateSetValueDynamicallyToggle":true},"widgetId":"getDBIncidents"},"isUserTriggered":true}],"gitSha":null,"checksum":null,"createdAt":"2022-01-31T02:08:32.362Z","updatedAt":"2022-01-31T02:08:32.362Z","pageId":692536,"userId":66039,"branchId":null},"modules":{"Incident-Navbar":{"moduleSaveId":55923533,"moduleName":"Incident-Navbar","moduleUuid":"cd581076-585d-11ec-a633-936fd80bda50","data":{"appState":"[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"moduleContainer\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"moduleContainer\",\"type\",\"widget\",\"subtype\",\"ModuleContainerWidget\",\"resourceName\",\"\",\"resourceDisplayName\",null,\"template\",[\"^3\",[\"heightType\",\"fixed\",\"spinWhenChildrenAreFetching\",true,\"showBorder\",true,\"showDropShadow\",true,\"isGlobalWidgetContainer\",true,\"style\",[\"^3\",[]],\"backgroundColor\",\"white\",\"title\",\"\",\"overflowType\",\"hidden\",\"disabled\",\"\"]],\"style\",[\"^3\",[]],\"position2\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"container\",\"\",\"rowGroup\",\"body\",\"subcontainer\",\"\",\"row\",0,\"col\",0,\"height\",12,\"width\",12,\"tabNum\",0]]],\"mobilePosition2\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"\",\"^<\",\"body\",\"^=\",\"\",\"row\",0,\"col\",0,\"^>\",12,\"^?\",12,\"^@\",0]]],\"mobileAppPosition\",null,\"tabIndex\",null,\"createdAt\",\"~m1638992045069\",\"updatedAt\",\"~m1638992045069\",\"^;\",\"\",\"folder\",\"\",\"screen\",null]]],\"navigation1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"navigation1\",\"^4\",\"widget\",\"^5\",\"NavigationWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"valueToCopy\",\"\",\"menuItems\",[\"~#iL\",[[\"^3\",[\"title\",\"Home\",\"hidden\",\"\",\"clickable\",[\"^3\",[\"valueToCopy\",\"\",\"internalUrlHashParams\",\"\",\"exportFileType\",\"csv\",\"buttonType\",\"internal-url\",\"exportTemplate\",null,\"url\",\"\",\"newWindow\",false,\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"d2b883fc-4fc0-11ec-8282-5311b8afe0de\",\"exportFileName\",\"\",\"disabled\",\"\"]],\"menuItems\",[\"^H\",[]]]],[\"^3\",[\"title\",\"Incident Details\",\"clickable\",[\"^3\",[\"valueToCopy\",\"\",\"internalUrlHashParams\",\"\",\"exportFileType\",\"csv\",\"buttonType\",\"internal-url\",\"exportTemplate\",null,\"url\",\"\",\"newWindow\",false,\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"f601490a-5913-11ec-9083-a700d6721666\",\"exportFileName\",\"\",\"disabled\",\"\"]],\"hidden\",\"\",\"menuItems\",[\"^H\",[]]]]]],\"srcFromUrl\",true,\"internalUrlHashParams\",\"\",\"exportFileType\",\"csv\",\"logoImageFile\",\"\",\"buttonType\",\"action\",\"exportTemplate\",null,\"url\",\"\",\"newWindow\",false,\"style\",[\"^3\",[\"primary-surface\",\"\",\"primary-text\",\"rgba(9, 9, 9, 0.7)\",\"accent-background\",\"rgba(54, 60, 244, 1)\"]],\"logoImageSrc\",\"\",\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"\",\"exportFileName\",\"\",\"disabled\",\"\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"moduleContainer\",\"^<\",\"body\",\"^=\",\"\",\"row\",0,\"col\",2,\"^>\",1,\"^?\",8,\"^@\",0]]],\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1638992064464\",\"^E\",\"~m1643591902637\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]],\"reportIncidentBtn\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"reportIncidentBtn\",\"^4\",\"widget\",\"^5\",\"ButtonWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"stretch\",\"clickable\",false,\"iconAfter\",\"\",\"submitTargetId\",null,\"hidden\",false,\"text\",\"Report Incident\",\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"background\",\"{{reportIncidentBtnColor.value}}\",\"border\",\"#ffffff\",\"borderRadius\",\"3px\"]],\"styleVariant\",\"solid\",\"submit\",false,\"iconBefore\",\"\",\"events\",[\"^H\",[[\"^3\",[\"event\",\"click\",\"type\",\"util\",\"method\",\"openApp\",\"pluginId\",\"\",\"params\",[\"^3\",[\"uuid\",\"a961f07e-4bf6-11ec-a646-27b92ee07218\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"moduleContainer\",\"^<\",\"body\",\"^=\",\"\",\"row\",0,\"col\",10,\"^>\",1,\"^?\",2,\"^@\",0]]],\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1638992292834\",\"^E\",\"~m1640527161639\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]],\"appTitleText\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"appTitleText\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"fixed\",\"horizontalAlign\",\"center\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"#### Incident Central\",\"style\",[\"^3\",[\"background\",\"\",\"color\",\"rgb(12, 0, 140)\"]],\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"moduleContainer\",\"^<\",\"body\",\"^=\",\"\",\"row\",5.551115123125783e-17,\"col\",0,\"^>\",1,\"^?\",2,\"^@\",0]]],\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1639319524508\",\"^E\",\"~m1639320166341\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]],\"reportIncidentBtnColor\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"reportIncidentBtnColor\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"const SALMON = '#EF4E4E';\\nconst SLATE_GRAY = '#627D98';\\n\\n// IMPORTANT: Edit this if you rename the Report Incident app.\\nconst REPORT_INCIDENT_APP_NAME = 'Report-Incident';\\nconst retoolContext = {{retoolContext}};\\n\\n// The retoolContext.appName is the whole path to the app: folder/app_name\\nconst appPathParts = retoolContext.appName.split('/');\\nconst appNameOnly = appPathParts[appPathParts.length - 1];\\n\\nif (appNameOnly.endsWith(REPORT_INCIDENT_APP_NAME)) {\\n return SLATE_GRAY;\\n} else {\\n return SALMON;\\n}\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1640526820496\",\"^E\",\"~m1640527474281\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]]]],\"^D\",null,\"version\",\"2.84.7\",\"appThemeId\",null,\"preloadedAppJavaScript\",\"// Warning: As of Dec 2021, the JS here will not be imported into the app \\n// where this module is used. Instead, in each app where you use this \\n// module, you must define a window.COLOR with a `salmon` field, like \\n// below, in that app's own JavaScript.\\nconst SALMON = '#EF4E4E';\\n\\nwindow.COLOR = {\\n salmon: SALMON,\\n}\",\"preloadedAppJSLinks\",[],\"appStyles\",\"\",\"testEntities\",[],\"tests\",[],\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^H\",[]],\"pageLoadValueOverrides\",[\"^H\",[]],\"isGlobalWidget\",true,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"instrumentationEnabled\",false,\"customDocumentTitleEnabled\",false,\"customDocumentTitle\",\"\",\"customShortcuts\",[],\"folders\",[\"^H\",[]],\"markdownLinkBehavior\",\"never\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null]]]"}}}} -------------------------------------------------------------------------------- /incident-central/code/Incident-Navbar.json: -------------------------------------------------------------------------------- 1 | {"uuid":"cd581076-585d-11ec-a633-936fd80bda50","page":{"id":55923533,"data":{"appState":"[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"moduleContainer\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"moduleContainer\",\"type\",\"widget\",\"subtype\",\"ModuleContainerWidget\",\"resourceName\",\"\",\"resourceDisplayName\",null,\"template\",[\"^3\",[\"heightType\",\"fixed\",\"spinWhenChildrenAreFetching\",true,\"showBorder\",true,\"showDropShadow\",true,\"isGlobalWidgetContainer\",true,\"style\",[\"^3\",[]],\"backgroundColor\",\"white\",\"title\",\"\",\"overflowType\",\"hidden\",\"disabled\",\"\"]],\"style\",[\"^3\",[]],\"position2\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"container\",\"\",\"rowGroup\",\"body\",\"subcontainer\",\"\",\"row\",0,\"col\",0,\"height\",12,\"width\",12,\"tabNum\",0]]],\"mobilePosition2\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"\",\"^<\",\"body\",\"^=\",\"\",\"row\",0,\"col\",0,\"^>\",12,\"^?\",12,\"^@\",0]]],\"mobileAppPosition\",null,\"tabIndex\",null,\"createdAt\",\"~m1638992045069\",\"updatedAt\",\"~m1638992045069\",\"^;\",\"\",\"folder\",\"\",\"screen\",null]]],\"navigation1\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"navigation1\",\"^4\",\"widget\",\"^5\",\"NavigationWidget\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"left\",\"valueToCopy\",\"\",\"menuItems\",[\"~#iL\",[[\"^3\",[\"title\",\"Home\",\"hidden\",\"\",\"clickable\",[\"^3\",[\"valueToCopy\",\"\",\"internalUrlHashParams\",\"\",\"exportFileType\",\"csv\",\"buttonType\",\"internal-url\",\"exportTemplate\",null,\"url\",\"\",\"newWindow\",false,\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"d2b883fc-4fc0-11ec-8282-5311b8afe0de\",\"exportFileName\",\"\",\"disabled\",\"\"]],\"menuItems\",[\"^H\",[]]]],[\"^3\",[\"title\",\"Incident Details\",\"clickable\",[\"^3\",[\"valueToCopy\",\"\",\"internalUrlHashParams\",\"\",\"exportFileType\",\"csv\",\"buttonType\",\"internal-url\",\"exportTemplate\",null,\"url\",\"\",\"newWindow\",false,\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"f601490a-5913-11ec-9083-a700d6721666\",\"exportFileName\",\"\",\"disabled\",\"\"]],\"hidden\",\"\",\"menuItems\",[\"^H\",[]]]]]],\"srcFromUrl\",true,\"internalUrlHashParams\",\"\",\"exportFileType\",\"csv\",\"logoImageFile\",\"\",\"buttonType\",\"action\",\"exportTemplate\",null,\"url\",\"\",\"newWindow\",false,\"style\",[\"^3\",[\"primary-surface\",\"\",\"primary-text\",\"rgba(9, 9, 9, 0.7)\",\"accent-background\",\"rgba(54, 60, 244, 1)\"]],\"logoImageSrc\",\"\",\"exportQuery\",\"\",\"action\",\"\",\"internalUrlQuery\",\"\",\"internalUrlPath\",\"\",\"exportFileName\",\"\",\"disabled\",\"\"]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"moduleContainer\",\"^<\",\"body\",\"^=\",\"\",\"row\",0,\"col\",2,\"^>\",1,\"^?\",8,\"^@\",0]]],\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1638992064464\",\"^E\",\"~m1643591902637\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]],\"reportIncidentBtn\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"reportIncidentBtn\",\"^4\",\"widget\",\"^5\",\"ButtonWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"horizontalAlign\",\"stretch\",\"clickable\",false,\"iconAfter\",\"\",\"submitTargetId\",null,\"hidden\",false,\"text\",\"Report Incident\",\"showInEditor\",false,\"tooltipText\",\"\",\"style\",[\"^3\",[\"background\",\"{{reportIncidentBtnColor.value}}\",\"border\",\"#ffffff\",\"borderRadius\",\"3px\"]],\"styleVariant\",\"solid\",\"submit\",false,\"iconBefore\",\"\",\"events\",[\"^H\",[[\"^3\",[\"event\",\"click\",\"type\",\"util\",\"method\",\"openApp\",\"pluginId\",\"\",\"params\",[\"^3\",[\"uuid\",\"a961f07e-4bf6-11ec-a646-27b92ee07218\"]],\"waitType\",\"debounce\",\"waitMs\",\"0\"]]]],\"loading\",false,\"loaderPosition\",\"auto\",\"disabled\",false,\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"moduleContainer\",\"^<\",\"body\",\"^=\",\"\",\"row\",0,\"col\",10,\"^>\",1,\"^?\",2,\"^@\",0]]],\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1638992292834\",\"^E\",\"~m1640527161639\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]],\"appTitleText\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"appTitleText\",\"^4\",\"widget\",\"^5\",\"TextWidget2\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"heightType\",\"fixed\",\"horizontalAlign\",\"center\",\"hidden\",false,\"imageWidth\",\"fit\",\"showInEditor\",false,\"verticalAlign\",\"center\",\"tooltipText\",\"\",\"value\",\"#### Incident Central\",\"style\",[\"^3\",[\"background\",\"\",\"color\",\"rgb(12, 0, 140)\"]],\"disableMarkdown\",false,\"overflowType\",\"scroll\",\"maintainSpaceWhenHidden\",false]],\"^9\",[\"^3\",[]],\"^:\",[\"^0\",[\"^ \",\"n\",\"position2\",\"v\",[\"^ \",\"^;\",\"moduleContainer\",\"^<\",\"body\",\"^=\",\"\",\"row\",5.551115123125783e-17,\"col\",0,\"^>\",1,\"^?\",2,\"^@\",0]]],\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1639319524508\",\"^E\",\"~m1639320166341\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]],\"reportIncidentBtnColor\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"reportIncidentBtnColor\",\"^4\",\"function\",\"^5\",\"Function\",\"^6\",null,\"^7\",null,\"^8\",[\"^3\",[\"funcBody\",\"const SALMON = '#EF4E4E';\\nconst SLATE_GRAY = '#627D98';\\n\\n// IMPORTANT: Edit this if you rename the Report Incident app.\\nconst REPORT_INCIDENT_APP_NAME = 'Report-Incident';\\nconst retoolContext = {{retoolContext}};\\n\\n// The retoolContext.appName is the whole path to the app: folder/app_name\\nconst appPathParts = retoolContext.appName.split('/');\\nconst appNameOnly = appPathParts[appPathParts.length - 1];\\n\\nif (appNameOnly.endsWith(REPORT_INCIDENT_APP_NAME)) {\\n return SLATE_GRAY;\\n} else {\\n return SALMON;\\n}\",\"value\",\"\"]],\"^9\",null,\"^:\",null,\"^A\",null,\"^B\",null,\"^C\",null,\"^D\",\"~m1640526820496\",\"^E\",\"~m1640527474281\",\"^;\",\"\",\"^F\",\"\",\"^G\",null]]]]],\"^D\",null,\"version\",\"2.84.7\",\"appThemeId\",null,\"preloadedAppJavaScript\",\"// Warning: As of Dec 2021, the JS here will not be imported into the app \\n// where this module is used. Instead, in each app where you use this \\n// module, you must define a window.COLOR with a `salmon` field, like \\n// below, in that app's own JavaScript.\\nconst SALMON = '#EF4E4E';\\n\\nwindow.COLOR = {\\n salmon: SALMON,\\n}\",\"preloadedAppJSLinks\",[],\"appStyles\",\"\",\"testEntities\",[],\"tests\",[],\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^H\",[]],\"pageLoadValueOverrides\",[\"^H\",[]],\"isGlobalWidget\",true,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"instrumentationEnabled\",false,\"customDocumentTitleEnabled\",false,\"customDocumentTitle\",\"\",\"customShortcuts\",[],\"folders\",[\"^H\",[]],\"markdownLinkBehavior\",\"never\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null]]]"},"changesRecord":[{"type":"WIDGET_TEMPLATE_UPDATE","payload":{"plugin":{"id":"navigation1","type":"widget","style":{},"folder":"","screen":null,"subtype":"NavigationWidget","tabIndex":null,"template":{"url":"","style":{"primary-text":"rgba(9, 9, 9, 0.7)","primary-surface":"","accent-background":"rgba(54, 60, 244, 1)"},"action":"","disabled":"","menuItems":[{"title":"Home","hidden":"","clickable":{"url":"","action":"","disabled":"","newWindow":false,"buttonType":"internal-url","exportQuery":"","valueToCopy":"","exportFileName":"","exportFileType":"csv","exportTemplate":null,"internalUrlPath":"d2b883fc-4fc0-11ec-8282-5311b8afe0de","internalUrlQuery":"","internalUrlHashParams":""},"menuItems":[]},{"title":"Incident Details","hidden":"","clickable":{"url":"","action":"","disabled":"","newWindow":false,"buttonType":"internal-url","exportQuery":"","valueToCopy":"","exportFileName":"","exportFileType":"csv","exportTemplate":null,"internalUrlPath":"f601490a-5913-11ec-9083-a700d6721666","internalUrlQuery":"","internalUrlHashParams":""},"menuItems":[]},{"title":"Statistics","hidden":"","clickable":{"url":"","action":"","disabled":"","newWindow":false,"buttonType":"internal-url","exportQuery":"","valueToCopy":"","exportFileName":"","exportFileType":"csv","exportTemplate":null,"internalUrlPath":"885f8bbc-5929-11ec-b6d0-bbe59e0a4d46","internalUrlQuery":"","internalUrlHashParams":""},"menuItems":[]}],"newWindow":false,"buttonType":"action","srcFromUrl":true,"exportQuery":"","valueToCopy":"","logoImageSrc":"","logoImageFile":"","exportFileName":"","exportFileType":"csv","exportTemplate":null,"horizontalAlign":"left","internalUrlPath":"","internalUrlQuery":"","internalUrlHashParams":""},"container":"","createdAt":"2021-12-08T19:34:24.464Z","namespace":null,"position2":{"col":2,"row":0,"width":8,"height":1,"tabNum":0,"rowGroup":"body","container":"moduleContainer","subcontainer":""},"updatedAt":"2021-12-15T14:29:37.700Z","resourceName":null,"mobilePosition2":null,"mobileAppPosition":null},"update":{"menuItems":[{"title":"Home","hidden":"","clickable":{"url":"","action":"","disabled":"","newWindow":false,"buttonType":"internal-url","exportQuery":"","valueToCopy":"","exportFileName":"","exportFileType":"csv","exportTemplate":null,"internalUrlPath":"d2b883fc-4fc0-11ec-8282-5311b8afe0de","internalUrlQuery":"","internalUrlHashParams":""},"menuItems":[]},{"title":"Incident Details","hidden":"","clickable":{"url":"","action":"","disabled":"","newWindow":false,"buttonType":"internal-url","exportQuery":"","valueToCopy":"","exportFileName":"","exportFileType":"csv","exportTemplate":null,"internalUrlPath":"f601490a-5913-11ec-9083-a700d6721666","internalUrlQuery":"","internalUrlHashParams":""},"menuItems":[]}]},"widgetId":"navigation1"},"isUserTriggered":true}],"gitSha":null,"checksum":null,"createdAt":"2022-01-31T01:18:24.902Z","updatedAt":"2022-01-31T01:18:24.902Z","pageId":719152,"userId":66039,"branchId":null},"modules":{}} -------------------------------------------------------------------------------- /incident-central/code/extras/Database-Setup.json: -------------------------------------------------------------------------------- 1 | {"uuid":"8b631cb8-56d9-11ec-b334-37497cb0e233","page":{"id":55923151,"data":{"appState":"[\"~#iR\",[\"^ \",\"n\",\"appTemplate\",\"v\",[\"^ \",\"isFetching\",false,\"plugins\",[\"~#iOM\",[\"q1_createUpdateTimestampTrigger\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"q1_createUpdateTimestampTrigger\",\"type\",\"datasource\",\"subtype\",\"SqlQueryUnified\",\"resourceName\",\"d9ddb8f4-0a90-45a2-aac2-3185c7341752\",\"resourceDisplayName\",\"Incident Database\",\"template\",[\"^3\",[\"queryRefreshTime\",\"\",\"records\",\"\",\"lastReceivedFromResourceAt\",null,\"databasePasswordOverride\",\"\",\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"showFailureToaster\",true,\"query\",\"CREATE OR REPLACE FUNCTION trigger_set_timestamp()\\nRETURNS TRIGGER AS $$\\nBEGIN\\n NEW.updated_at = NOW();\\n RETURN NEW;\\nEND;\\n$$ LANGUAGE plpgsql;\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"privateParams\",[\"~#iL\",[]],\"runWhenPageLoadsDelay\",\"\",\"warningCodes\",[\"^9\",[]],\"data\",null,\"recordId\",\"\",\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"dataArray\",[\"^9\",[]],\"cacheKeyTtl\",\"\",\"filterBy\",\"\",\"databaseHostOverride\",\"\",\"metadata\",null,\"editorMode\",\"sql\",\"actionType\",\"\",\"changesetObject\",\"\",\"shouldUseLegacySql\",false,\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"databaseNameOverride\",\"\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"watchedParams\",[\"^9\",[]],\"enableErrorTransformer\",false,\"enableBulkUpdates\",false,\"showLatestVersionUpdatedWarning\",false,\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"bulkUpdatePrimaryKey\",\"\",\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^9\",[]],\"tableName\",\"\",\"queryTimeout\",\"10001\",\"requireConfirmation\",false,\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^9\",[]],\"databaseUsernameOverride\",\"\",\"shouldEnableBatchQuerying\",false,\"doNotThrowOnNoOp\",false,\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"style\",null,\"position2\",null,\"mobilePosition2\",null,\"mobileAppPosition\",null,\"tabIndex\",null,\"createdAt\",\"~m1638825289256\",\"updatedAt\",\"~m1638825576497\",\"container\",\"\",\"folder\",\"\",\"screen\",null]]],\"q2_createIncidentsTable\",[\"^0\",[\"^ \",\"n\",\"pluginTemplate\",\"v\",[\"^ \",\"id\",\"q2_createIncidentsTable\",\"^4\",\"datasource\",\"^5\",\"SqlQueryUnified\",\"^6\",\"d9ddb8f4-0a90-45a2-aac2-3185c7341752\",\"^7\",\"Incident Database\",\"^8\",[\"^3\",[\"queryRefreshTime\",\"\",\"records\",\"\",\"lastReceivedFromResourceAt\",null,\"databasePasswordOverride\",\"\",\"queryDisabledMessage\",\"\",\"successMessage\",\"\",\"queryDisabled\",\"\",\"playgroundQuerySaveId\",\"latest\",\"resourceNameOverride\",\"\",\"runWhenModelUpdates\",false,\"showFailureToaster\",true,\"query\",\"CREATE TABLE IF NOT EXISTS incidents (\\n id TEXT PRIMARY KEY,\\n created_at TIMESTAMP NOT NULL DEFAULT NOW(),\\n updated_at TIMESTAMP NOT NULL DEFAULT NOW(),\\n \\n pagerduty_id TEXT,\\n pagerduty_url TEXT,\\n \\n slack_channel_id TEXT,\\n slack_channel_name TEXT,\\n slack_channel_url TEXT,\\n \\n reporter_email TEXT,\\n incident_commander_email TEXT\\n);\\n\\nCREATE TRIGGER set_updated_at_on_incidents\\nBEFORE UPDATE ON incidents\\nFOR EACH ROW\\nEXECUTE PROCEDURE trigger_set_timestamp();\",\"playgroundQueryUuid\",\"\",\"playgroundQueryId\",null,\"privateParams\",[\"^9\",[]],\"runWhenPageLoadsDelay\",\"\",\"warningCodes\",[\"^9\",[]],\"data\",null,\"recordId\",\"\",\"importedQueryInputs\",[\"^3\",[]],\"isImported\",false,\"showSuccessToaster\",true,\"dataArray\",[\"^9\",[]],\"cacheKeyTtl\",\"\",\"filterBy\",\"\",\"databaseHostOverride\",\"\",\"metadata\",null,\"editorMode\",\"sql\",\"actionType\",\"\",\"changesetObject\",\"\",\"shouldUseLegacySql\",false,\"errorTransformer\",\"// The variable 'data' allows you to reference the request's data in the transformer. \\n// example: return data.find(element => element.isError)\\nreturn data.error\",\"databaseNameOverride\",\"\",\"confirmationMessage\",null,\"isFetching\",false,\"changeset\",\"\",\"rawData\",null,\"queryTriggerDelay\",\"0\",\"watchedParams\",[\"^9\",[]],\"enableErrorTransformer\",false,\"enableBulkUpdates\",false,\"showLatestVersionUpdatedWarning\",false,\"timestamp\",0,\"importedQueryDefaults\",[\"^3\",[]],\"enableTransformer\",false,\"showUpdateSetValueDynamicallyToggle\",true,\"bulkUpdatePrimaryKey\",\"\",\"runWhenPageLoads\",false,\"transformer\",\"// type your code here\\n// example: return formatDataAsArray(data).filter(row => row.quantity > 20)\\nreturn data\",\"events\",[\"^9\",[]],\"tableName\",\"\",\"queryTimeout\",\"10001\",\"requireConfirmation\",false,\"queryFailureConditions\",\"\",\"changesetIsObject\",false,\"enableCaching\",false,\"allowedGroups\",[\"^9\",[]],\"databaseUsernameOverride\",\"\",\"shouldEnableBatchQuerying\",false,\"doNotThrowOnNoOp\",false,\"queryThrottleTime\",\"750\",\"updateSetValueDynamically\",false,\"notificationDuration\",\"\"]],\"^:\",null,\"^;\",null,\"^<\",null,\"^=\",null,\"^>\",null,\"^?\",\"~m1638825581496\",\"^@\",\"~m1638914697107\",\"^A\",\"\",\"^B\",\"\",\"^C\",null]]]]],\"^?\",null,\"version\",\"2.84.7\",\"appThemeId\",null,\"preloadedAppJavaScript\",null,\"preloadedAppJSLinks\",[],\"appStyles\",\"\",\"testEntities\",[],\"tests\",[],\"responsiveLayoutDisabled\",false,\"loadingIndicatorsDisabled\",false,\"urlFragmentDefinitions\",[\"^9\",[]],\"pageLoadValueOverrides\",[\"^9\",[]],\"isGlobalWidget\",false,\"isMobileApp\",false,\"multiScreenMobileApp\",false,\"instrumentationEnabled\",false,\"customDocumentTitleEnabled\",false,\"customDocumentTitle\",\"\",\"customShortcuts\",[],\"folders\",[\"^9\",[]],\"markdownLinkBehavior\",\"never\",\"inAppRetoolPillAppearance\",\"NO_OVERRIDE\",\"rootScreen\",null]]]"},"changesRecord":[{"type":"PLUGIN_DELETE","payload":["table1"],"hideChangelogEntry":false}],"gitSha":null,"checksum":null,"createdAt":"2022-01-31T01:10:00.193Z","updatedAt":"2022-01-31T01:10:00.193Z","pageId":713828,"userId":66039,"branchId":null},"modules":{}} -------------------------------------------------------------------------------- /incident-central/images/home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/images/home-page.png -------------------------------------------------------------------------------- /incident-central/images/incident-central-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/images/incident-central-demo.gif -------------------------------------------------------------------------------- /incident-central/images/incident-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/images/incident-details.png -------------------------------------------------------------------------------- /incident-central/images/report-incident-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/images/report-incident-created.png -------------------------------------------------------------------------------- /incident-central/images/report-incident.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/images/report-incident.png -------------------------------------------------------------------------------- /incident-central/setup-guides/README.md: -------------------------------------------------------------------------------- 1 | # Incident Central - Setup guide 2 | 3 | This guide helps you set up Incident Central inside your Retool instance. (If you're wondering "What's Incident Central?", see [the main README](../README.md).) 4 | 5 | ## Overview 6 | As an overview, we'll go through the following steps: 7 | 8 | * Set up backend dependencies. (Steps 1-3) 9 | - Configuring your Slack and PagerDuty instances. 10 | - Creating a relational database, if you don't have one already. 11 | * Set up these backends as Resources in Retool. (Step 4) 12 | * Download and configure the Retool app files in Retool. (Step 5) 13 | 14 | ## Important assumptions 15 | Incident Central works off the following assumptions about your setup: 16 | * The email address of people in your PagerDuty account matches their email address within your Slack account. 17 | 18 | ## Step 1 - Set up PagerDuty 19 | [Step-by-step: Set up PagerDuty](./set-up-pagerduty.md) 20 | 21 | ## Step 2 - Set up Slack 22 | [Step-by-step: Set up Slack](./set-up-slack.md) 23 | 24 | ## Step 3 - Set up a relational database for incident data 25 | [Step-by-step: Set up a database](./set-up-database.md) 26 | 27 | ## Step 4 - Set up these backends as Resources in Retool 28 | [Step-by-step: Set up Retool Resources](./set-up-retool-resources.md) 29 | 30 | ## Step 5 - Set up the Retool app files 31 | [Step-by-step: Set up Retool Apps](./set-up-retool-apps.md) 32 | -------------------------------------------------------------------------------- /incident-central/setup-guides/images/adjust-navbar-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/setup-guides/images/adjust-navbar-link.png -------------------------------------------------------------------------------- /incident-central/setup-guides/images/adjust-report-incident-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/setup-guides/images/adjust-report-incident-link.png -------------------------------------------------------------------------------- /incident-central/setup-guides/images/fill-in-slack-team-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/setup-guides/images/fill-in-slack-team-id.png -------------------------------------------------------------------------------- /incident-central/setup-guides/images/pagerduty-api-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/setup-guides/images/pagerduty-api-resource.png -------------------------------------------------------------------------------- /incident-central/setup-guides/images/slack-api-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/incident-central/setup-guides/images/slack-api-resource.png -------------------------------------------------------------------------------- /incident-central/setup-guides/set-up-database.md: -------------------------------------------------------------------------------- 1 | # Incident Central Setup - Step 3: Database 2 | 3 | ## What's this database for? 4 | Incident Central will write each new triggered incident to a database table. This table will be the one place where you keep track of all the different metadata associated with each incident. For example, each incident has a name, a start time, a PagerDuty profile, a Slack channel, and so on. 5 | 6 | In the future, you may choose to add some other information (e.g. a Google doc, or a Jira ticket) to each incident, in which case you can add additional columns to this database table, and modify Incident Central to write this additional information. 7 | 8 | ## Output 9 | The goal of this section is to create: 10 | * A database, if you don't already have one 11 | * A database table called `incidents` with the schema that Incident Central expects 12 | 13 | ## Step 3.1 - Create a database instance 14 | If you already have a database that you want to use for Incident Central, you are all set. (The example code we provide will specifically be suited for PostgreSQL, but you can adapt it to whatever database you are using, including a non-relational database if you want.) 15 | 16 | Otherwise, you can spin up a PostgreSQL database on a variety of platforms. [Heroku's managed PostgreSQL offering](https://www.heroku.com/postgres) is one example, and it has a free tier. 17 | 18 | ## Step 3.2 - Create a database table called `incidents` and define its schema 19 | Run the following command on your database: 20 | ``` 21 | CREATE OR REPLACE FUNCTION trigger_set_timestamp() 22 | RETURNS TRIGGER AS $$ 23 | BEGIN 24 | NEW.updated_at = NOW(); 25 | RETURN NEW; 26 | END; 27 | $$ LANGUAGE plpgsql; 28 | ``` 29 | 30 | Then run: 31 | ``` 32 | CREATE TABLE IF NOT EXISTS incidents ( 33 | id TEXT PRIMARY KEY, 34 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 35 | updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 36 | 37 | pagerduty_id TEXT, 38 | pagerduty_url TEXT, 39 | 40 | slack_channel_id TEXT, 41 | slack_channel_name TEXT, 42 | slack_channel_url TEXT, 43 | 44 | reporter_email TEXT, 45 | incident_commander_email TEXT 46 | ); 47 | 48 | CREATE TRIGGER set_updated_at_on_incidents 49 | BEFORE UPDATE ON incidents 50 | FOR EACH ROW 51 | EXECUTE PROCEDURE trigger_set_timestamp(); 52 | ``` 53 | ### Optional way to run these commands 54 | An easy way to run these commands on your database is to connect your database to Retool first, and then run the command through Retool. 55 | 56 | If you would like to do this: 57 | * First, go through Step 4: Retool Resources to get this database connected to Retool (as the "Incident Database" resource). 58 | * Upload the `Database-Setup` Retool app provided in [the `code/extras` subdirectory](../code/extras/) into your Retool instance. To do this, download the code, and go to the homepage of Retool and click "Create new" → "Import an app". 59 | * Open the `Database-Setup` app in **_Edit_** mode, and manually click to run the two Queries (shown in the bottom panel of the Editor view) in the app. 60 | 61 | ## Summary 62 | You now have a database with an `incidents` table that Incident Central can write to. 63 | 64 | ## Next step 65 | [Step-by-step: Set up Retool Resources](./set-up-retool-resources.md) 66 | -------------------------------------------------------------------------------- /incident-central/setup-guides/set-up-pagerduty.md: -------------------------------------------------------------------------------- 1 | # Incident Central Setup - Step 1: PagerDuty 2 | 3 | ## What's PagerDuty? 4 | PagerDuty is the tool that Incident Central will use to trigger incidents for your services. PagerDuty will handle alerting the oncall staff you have configured within it. 5 | 6 | Note, you can define "services" however you like—this is just the terminology in PagerDuty for a thing that an oncall supports. 7 | 8 | ## Output 9 | The goal of this section is to create: 10 | * A PagerDuty account, if you don't have one already. (A free trial works.) 11 | * A PagerDuty API key. 12 | * Inside PagerDuty, you should configure at least 1 service, and configure its "escalation policy" to alert the people who are oncall for that service. 13 | 14 | ## Step 1.1 - PagerDuty account 15 | Head on over to [pagerduty.com](https://pagerduty.com/) and sign up, or sign in. 16 | 17 | ## Step 1.2 - PagerDuty API key 18 | Follow the instructions in the [PagerDuty API Access Keys docs](https://support.pagerduty.com/docs/api-access-keys) to create a "General Access REST API key." 19 | 20 | Note: We want this key to have "write" access so that we can create incidents. So, do not make the key "read-only". 21 | 22 | ## Step 1.3 - Invite oncalls to PagerDuty 23 | If you already have existing oncall people/rotations in PagerDuty, you are all set. 24 | 25 | If this is your first time setting up PagerDuty, you'll want to invite the people you want to be oncall. To do this, follow the instructions in [PagerDuty's Manage Users docs](https://support.pagerduty.com/docs/users), under "Add a User". (You may also consider grouping people onto [Teams](https://support.pagerduty.com/docs/teams).) 26 | 27 | ### Important assumption 28 | Incident Central works off the following assumptions about your setup: 29 | * The email address of people in your PagerDuty account matches their email address within your Slack account. 30 | 31 | ## Step 1.4 - PagerDuty service with escalation policy 32 | If you already have existing services and escalation policies defined in PagerDuty, you are all set. 33 | 34 | If you don't, follow the instructions in the [PagerDuty Services and Integrations docs](https://support.pagerduty.com/docs/services-and-integrations) under "Create a Service". This will take you through creating a service along with its "escalation policy." The escalation policy is the set of rules that defines who to notify, in what order, when the service has an incident. 35 | 36 | If you are feeling fun, create at least 2 services! This will be more realistic. 37 | 38 | Note: It's possible to define an oncall schedules—a rotation that consists of multiple people—in PagerDuty as well. You can then define an escalation policy that triggers a specific oncall schedule. To find out how to set up an oncall schedule, see [PagerDuty's Schedule Basics docs](https://support.pagerduty.com/docs/schedule-basics). 39 | 40 | ## Summary 41 | You now have the following items that you can plug into a later step: 42 | * PagerDuty API key 43 | 44 | You also have a "service" within PagerDuty that you want to be able to trigger an incident on, and this service has an "escalation policy" that defines who will get notified when an incident starts. 45 | 46 | ## Next step 47 | [Step-by-step: Set up Slack](./set-up-slack.md) 48 | -------------------------------------------------------------------------------- /incident-central/setup-guides/set-up-retool-apps.md: -------------------------------------------------------------------------------- 1 | # Incident Central Setup - Step 5: Apps in Retool 2 | 3 | ## What's an App in Retool? 4 | As the name suggests, an App is a single "view" (or "page") that you can build in Retool. An App is built from a combination of Components to construct the UI, and Queries that define calls to your Resources. Queries can do reads or writes. 5 | 6 | ## Why Incident Central is 3 Retool Apps 7 | 8 | To the end-user, Incident Central looks like a single, unified website. However, under the hood, it is 3 distinct Retool Apps. 9 | 10 | This is because Incident Central consists of three distinct "views", and I have chosen to make these views independent of each other, so that it's easy for anyone (including me!) to mix and match them and refactor them. Decoupling unrelated pieces of functionality is in general a good practice in programming. 11 | 12 | The three distinct Retool Apps are: a home page, an incident details view, and a Report Incident view. 13 | 14 | These three Apps are united by one common Navigation bar (implemented as a reusable [Module](https://docs.retool.com/docs/modules)) that sits in the header. This Navigation bar lets the end-user experience Incident Central as a single, unified website. 15 | 16 | ## Output 17 | The goal of this section is: 18 | * To get Incident Central into your Retool instance. 🚀 19 | 20 | 21 | ## Step 5.1 - Download the app "code" 22 | In this step, you will obtain the code files in the `/code` directory in this Github repo. 23 | 24 | To do this, clone this Github repo: 25 | `git clone https://github.com/tryretool/retool-app-exchange.git` 26 | 27 | (Alternatively, you can manually click-to-download each file in the [`code/`](../code) directory.) 28 | 29 | You will have the following files: 30 | 31 | ``` 32 | - Incident-Navbar.json 33 | - Incident-Central-Home.json 34 | - Incident-Central-Details.json 35 | - Report-Incident.json 36 | ``` 37 | 38 | ## Step 5.2 - Import the app "code" 39 | First, let's create a folder to put all of this code into, to keep things organized. 40 | 1. On your Retool home page, click "Create new" → "Create a new folder". Give this folder a name you like, e.g. "Incident-Central". 41 | 42 | Now, let's import the Navigation bar Module. 43 | 2. Click "Create new" → "Import an app". Upload the `Incident-Navbar.json` file, and select the folder you just created ("Incident-Central") as the destination. 44 | 45 | Now, let's import each app. 46 | 3. For each of the three app files (`Incident-Central-Home.json`, `Incident-Central-Details.json`, `Report-Incident.json`), do these steps: 47 | * Click "Create new" → "Import an app". 48 | * Upload the file, and select the folder you just created ("Incident-Central") as the destination. 49 | 50 | 51 | ## Step 5.3 - Adjust links in the navigation bar 52 | Finally, let's hook up the links to our apps in the `Incident-Navbar`. 53 | 4. Go to the "Incident-Central" folder. Click the "Edit" button to the right of the `Incident-Navbar` Module to open the editor view. 54 | 5. Within the UI editor canvas, click on the 'navigation' component on the screen. You should see the details of this component appear in the right-hand sidebar. 55 | 6. Look in the right-hand sidebar for the "Menu items" detail, with "Home" and "Incident Details" listed. Click on each of these menu items, and adjust its "App to Open" to be the appropriate app. 56 |
Adjusting the navigation bar menu items 57 | 58 | 7. Now, within the UI editor canvas, click on the "Report Incident" button component. 59 | 8. Look in the right-hand sidebar for the "Event handlers" detail. Click on the one event handler, and adjust the "App" that will be opened. 60 |
Adjusting the app to be opened 61 | 62 | ## Step 5.4 - Fill in your Slack Team ID 63 | There is one place in Incident Central where we use your Slack Team ID as a hardcoded value. This is the simplest way to get this value (which in most cases stays the same). 64 | 65 | 9. Open the "Report-Incident" app in Editor mode. 66 | 10. Click the "..." button in the upper right. Select "Scripts and styles" from the dropdown. 67 | 11. A modal will open. Select the "JavaScript" tab. 68 | 12. At the top of the code here, you'll see a place to fill in your Slack Team ID as a constant. 69 |
Filling in the Slack Team ID 70 | 13. Click "Save". 71 | 72 | ## Step 5.5 - Test it out 73 | In this section, we're going to test out the functionality in your new apps. Ideally, put yourself oncall for whichever service you want to send a test "incident" to in PagerDuty, so that you don't surprise someone else! (Or else, give the person who's oncall a heads-up that you are testing.) 74 | 75 | Navigate to your "Incident-Central" folder, and click on the "Report-Incident" app. Choose a service to create a "test" incident for, and fill out the form. This should: 76 | - Page the person who is oncall via PagerDuty. 77 | - Create a Slack channel and add you and the oncall person to it. 78 | 79 | Now, click on the "Home" menu item in the navbar to open the Incident Central "Home" page. You should see the new incident listed. 80 | 81 | Now, click on "View details" for this incident. You should see the details view load for this incident. 82 | 83 | 84 | ## Summary 85 | 86 | You should now have Incident Central within your Retool instance! 🥳 87 | 88 | **Next steps:** You can choose to extend or adjust this app in any way. (For example, maybe you'd like Report Incident to also create a Jira ticket.) 89 | 90 | Please consider contributing back to this Github repo if you make something you think other people would like to use! Get in touch by opening a Github Issue if you have an idea you want to run by us. 91 | 92 | **Questions, comments, feedback?** Please open a Github Issue and let us know! 93 | -------------------------------------------------------------------------------- /incident-central/setup-guides/set-up-retool-resources.md: -------------------------------------------------------------------------------- 1 | # Incident Central Setup - Step 4: Resources in Retool 2 | 3 | ## What's a Resource in Retool? 4 | A Resource is a core concept in Retool. A Resource is a config that defines a backend data source that your Retool apps can talk to. Retool supports a variety of different kinds of backends, from databases to REST API calls, to special integrations like Stripe. 5 | 6 | You can define a Resource once, then use it again and again in your Retool apps. 7 | 8 | Read more about Resources in [the Retool docs](https://docs.retool.com/docs/integrations-overview). 9 | 10 | ## Output 11 | The goal of this section is to create: 12 | * A Resource called `PagerDuty API for Incident Central`. This will be a reusable way to call the PagerDuty API that you set up in Step 1. 13 | * A Resource called `Slack API for Incident Central`. This will be a reusable way to call the Slack API that you set up in Step 2. 14 | * A Resource called `Incident Database`. This will be a reusable way to query the database that you set up in Step 3. 15 | 16 | Note: The names here are important! The app code we provide expects the Resources to have these names. (You can choose to rename them, after you've finished setting everything up.) 17 | 18 | ## Step 4.1 - Create the "PagerDuty API for Incident Central" Resource 19 | 1. On the Resources page (`/resources`) in Retool, click "Create New" in the upper right. 20 | 2. Select the "REST API" resource type. 21 | 3. Fill out the resource information as follows: 22 | 23 | ``` 24 | Name: PagerDuty API for Incident Central 25 | Base URL: https://api.pagerduty.com 26 | ``` 27 | 28 | ``` 29 | = Headers = 30 | Accept: application/vnd.pagerduty+json;version=2 31 | Authorization: Token token= 32 | Content-Type: application/json 33 | ``` 34 | 4. Click "Save changes". 35 | 36 | Setting up the PagerDuty resource 37 | 38 | 39 | ## Step 4.2 - Create the "Slack API for Incident Central" Resource 40 | 1. On the Resources page (`/resources`) in Retool, click "Create New" in the upper right. 41 | 2. Select the "REST API" resource type. 42 | 3. Fill out the resource information as follows: 43 | 44 | ``` 45 | Name: Slack API for Incident Central 46 | Base URL: https://slack.com/api 47 | ``` 48 | 49 | ``` 50 | = Headers = 51 | Content-Type: application/json; charset=utf-8 52 | Authorization: Bearer 53 | ``` 54 | 4. Click "Save changes". 55 | 56 | Setting up the Slack resource 57 | 58 | 59 | ## Step 4.3 - Create the "Incident Database" Resource 60 | 1. On the Resources page (`/resources`) in Retool, click "Create New" in the upper right. 61 | 2. Select the "Postgres" resource type. (Or, if you've used a different type of database, select the appropriate type.) 62 | 3. Fill out the resource information as follows. (For more info, see [our docs on Postgres integration](https://docs.retool.com/docs/postgresql-integration).) 63 | 64 | ``` 65 | Name: Incident Database 66 | 67 | 68 | Host: 69 | Port: 70 | Database name: 71 | Database username: 72 | Database password: 73 | ``` 74 | 75 | 4. Click "Save changes". 76 | 77 | 78 | ## Summary 79 | You now have three Resources defined in Retool, with the following names: 80 | * `PagerDuty API for Incident Central` 81 | * `Slack API for Incident Central` 82 | * `Incident Database` 83 | 84 | Reminder: The names here are important! The app code we provide expects the Resources to have these names. (You can choose to rename them, after you've finished setting everything up.) 85 | 86 | Now, we are ready to import our apps! 87 | 88 | ## Next step 89 | [Step-by-step: Set up Retool Apps](./set-up-retool-apps.md) 90 | -------------------------------------------------------------------------------- /incident-central/setup-guides/set-up-slack.md: -------------------------------------------------------------------------------- 1 | # Incident Central Setup - Step 2: Slack 2 | 3 | ## What's Slack? 4 | Slack is the tool that Incident Central will use to create a real-time communication channel when an incident is triggered. The channel is meant to be a central place for people to collaborate to solve the incident. 5 | 6 | ## Output 7 | The goal of this section is to create: 8 | * A Slack account (aka "workspace"), if you don't have one already. (A free trial works.) 9 | * A Slack API key. 10 | * Inside Slack, you should invite (at least) all the people in your organization who may help out in incident response. 11 | 12 | ## Step 2.1 - Slack workspace 13 | Head on over to [slack.com](https://slack.com/) and sign up, or sign in. 14 | 15 | ## Step 2.2 - Slack API key 16 | In order to get a Slack API key, you'll first need to create a Slack app. To do this, follow the instructions in [Slack's Basic app setup docs](https://api.slack.com/authentication/basics): in particular the steps "Creating an app", "Requesting scopes", and "Installing the app to a workspace". (You don't need to do the rest.) 17 | 18 | For the "Requesting scopes" step, you should specify the following scopes: 19 | ``` 20 | Bot Token Scopes: 21 | - channels:manage 22 | - users:read 23 | - users:read.email 24 | ``` 25 | 26 | After you install the app to your Slack workspace, you will be able to make API requests using the "Bot User OAuth Token" associated with your new Slack app. 27 | 28 | ## Step 2.3 - Invite oncalls to Slack 29 | If you already have everyone in Slack, you are all set. 30 | 31 | If this is your first time setting up Slack, you'll want to invite ideally your whole team to Slack, but definitely anyone who may help out in incident response (whether or not they are oncall first-responders). 32 | 33 | ### Important assumption 34 | Incident Central works off the following assumptions about your setup: 35 | * The email address of people in your PagerDuty account matches their email address within your Slack account. 36 | 37 | ## Summary 38 | You now have the following items that you can plug into a later step: 39 | * Slack API key - this is the "Bot User OAuth Token" associated with your Slack app 40 | 41 | You also have a Slack workspace in which a new Slack channel will be opened whenever an incident is triggered through Incident Central. Incident Central will autogenerate a unique, human-readable name for the incident, and give the Slack channel the same name. Incident Central will also add the oncall person (as defined by PagerDuty) and incident reporter to this new channel. 42 | 43 | ## Next step 44 | [Step-by-step: Set up a database](./set-up-database.md) 45 | -------------------------------------------------------------------------------- /snowflake-resource-optimization/README.md: -------------------------------------------------------------------------------- 1 | # Snowflake Resource Optimization: Setup & Configuration 2 | 3 |

4 | Snowflake Resource Optimization: Setup & Configuration Optimization gif 5 |

6 | 7 | ## Why you need this 8 | This Retool app extends [Snowflake's Resource Optimization: Setup & Configuration Quickstart Guide](https://quickstarts.snowflake.com/guide/resource_optimization_setup/index.html?index=..%2F..index#0). If you use Snowflake, it will help you better monitor and manage your credit consumption, and avoid spending more than you need to. 9 | 10 | This Retool app makes it easy to put the Quickstart Guide into practice, by enabling you to take action from right within an easy-to-use UI. Without having to remember or type any commands, you can set auto-suspend or auto-resume for your warehouses, suspend or drop idle warehouses, disable or drop idle users, etc. 11 | 12 | ## What does Snowflake Resource Optimization: Setup & Configuration do? 13 | The Snowflake Resource Optimization: Setup & Configuration app lets you do the following things with the click of a button: 14 | 15 | ### Warehouses 16 | - Identify warehouses without auto-resume enabled and enable it 17 | - Identify warehouses without auto-suspend enabled and enable it for your chosen duration 18 | - Identify warehouses with long suspension and modify the auto-suspend setting 19 | - Identify warehouses without resource monitors and assign them where needed 20 | - Identify idle warehouses and suspend or drop them 21 | ### Users 22 | - Review warehouses used by multiple roles and investigate relevant query metrics 23 | - Identify idle users and disable or drop them 24 | - Identify users who have never logged in and disable or drop them 25 | - Identify idle roles and drop them 26 | ### Tasks 27 | - Identify failed tasks and show query history or drop them 28 | - Identify long running tasks and show query history or drop them 29 | ### Else 30 | - Review account, warehouse, or user statement timeouts and modify them 31 | - Identify stale table streams to potentially recreate 32 | 33 | 34 | ## Screenshots 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | ## Who is this tool for? 47 | This tool will enable users to make irreversible changes, such as dropping warehouses, users, roles, or tasks. It is set up to leverage the [ACCOUNTADMIN](https://docs.snowflake.com/en/user-guide/security-access-control-considerations.html#using-the-accountadmin-role) role, and as such should only be used by Snowflake administrators who are trusted to make such changes. 48 | 49 | ## Technical Details and Setup 50 | ### Dependencies - backends 51 | The Snowflake Resource Optimization: Setup & Configuration app depends on the following backend services: 52 | - Snowflake 53 | 54 | ### How to set up Snowflake Resource Optimization: Setup & Configuration in your Retool instance 55 | See the detailed setup guides in the [Setup Guides folder](./setup-guides). 56 | 57 | As an overview, these guides will take you through: 58 | 59 | 1. Setting up Snowflake. 60 | 2. Setting up your Snowflake database as a Resource in Retool. 61 | 3. Downloading and configuring the Retool app file. 62 | 63 | ### Application notes 64 | Please find some relevant considerations below: 65 | - In [Snowflake's Resource Optimization: Setup & Configuration Quickstart Guide](https://quickstarts.snowflake.com/guide/resource_optimization_setup/index.html?index=..%2F..index#0), the Idle Users, Users Never Logged In, and Idle Roles queries leverage the [ACCOUNT_USAGE](https://docs.snowflake.com/en/sql-reference/account-usage.html) schema. The Retool app, however, uses Snowflake DDL (e.g. SHOW USERS) to avoid the latency associated with the ACCOUNT_USAGE schema. This means that you are able to review and update users and roles as soon as they are created in Snowflake. It also means that these queries do not show deleted users or roles. 66 | - The Failed Tasks and Long Running Tasks queries can take a long time to run. Feel free to modify them using [Snowflake's recommendations](https://docs.snowflake.com/en/sql-reference/account-usage/task_history.html#usage-notes) as needed. 67 | 68 | 69 | ## What's next? 70 | ### How to contribute 71 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 72 | -------------------------------------------------------------------------------- /snowflake-resource-optimization/images/home-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-resource-optimization/images/home-page.png -------------------------------------------------------------------------------- /snowflake-resource-optimization/images/setup-config-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-resource-optimization/images/setup-config-demo.gif -------------------------------------------------------------------------------- /snowflake-resource-optimization/images/statement-timeouts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-resource-optimization/images/statement-timeouts.png -------------------------------------------------------------------------------- /snowflake-resource-optimization/images/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-resource-optimization/images/users.png -------------------------------------------------------------------------------- /snowflake-resource-optimization/images/warehouses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-resource-optimization/images/warehouses.png -------------------------------------------------------------------------------- /snowflake-resource-optimization/setup-guides/README.md: -------------------------------------------------------------------------------- 1 | # App Setup guide 2 | 3 | This guide helps you set up Snowflake Resource Optimization: Setup & Configuration inside your Retool instance. (If you're wondering "What's Snowflake Resource Optimization: Setup & Configuration?", see [the main README](../README.md).) 4 | 5 | ## Overview 6 | As an overview, we'll go through the following steps: 7 | 8 | * Set up Snowflake. (Step 1) 9 | * Set up your Snowflake database as a Resource in Retool. (Step 2) 10 | * Download and configure the Retool app file in Retool. (Step 3) 11 | 12 | ## Step 1 - Set up Snowflake 13 | [Step-by-step: Set up Snowflake](./set-up-snowflake.md) 14 | 15 | ## Step 2 - Set up your Snowflake database as a Resource in Retool 16 | [Step-by-step: Set up Retool Resource](./set-up-retool-resource.md) 17 | 18 | ## Step 3 - Set up the Retool app file 19 | [Step-by-step: Set up Retool App](./set-up-retool-app.md) 20 | -------------------------------------------------------------------------------- /snowflake-resource-optimization/setup-guides/images/snowflake-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-resource-optimization/setup-guides/images/snowflake-resource.png -------------------------------------------------------------------------------- /snowflake-resource-optimization/setup-guides/set-up-retool-app.md: -------------------------------------------------------------------------------- 1 | # App Setup - Step 3: Apps in Retool 2 | 3 | ## What's an App in Retool? 4 | As the name suggests, an App is a single "view" (or "page") that you can build in Retool. An App is built from a combination of Components to construct the UI, and Queries that define calls to your Resources. Queries can do reads or writes. 5 | 6 | ## Output 7 | The goal of this section is: 8 | * To get Snowflake Resource Optimization: Setup & Configuration into your Retool instance. 🚀 9 | 10 | 11 | ## Step 3.1 - Download the app "code" 12 | In this step, you will obtain the code file in the `/code` directory in this Github repo. 13 | 14 | To do this, clone this Github repo: 15 | `git clone https://github.com/tryretool/retool-app-exchange.git` 16 | 17 | (Alternatively, you can manually click-to-download the file in the [`/code`](../code) directory.) 18 | 19 | You will have the following file: `Snowflake-Resource-Optimization-Setup-Configuration.json`. 20 | 21 | ## Step 3.2 - Import the app "code" 22 | First, let's create a folder to put this code into, to keep things organized. 23 | 1. On your Retool home page, click "Create new" → "Create a new folder". Give this folder a name you like, e.g. "Snowflake Resource Optimization". 24 | 25 | 2. Now, let's import the app. 26 | * Click "Create new" → "Import an app". 27 | * Upload the `Snowflake-Resource-Optimization-Setup-Configuration.json` file, and select the folder you just created ("Snowflake Resource Optimization") as the destination. 28 | 29 | ## Step 3.3 - Test it out 30 | In this section, we're going to test out the functionality in your new app. Please be aware that you can use this app to make irreversible changes (e.g. drop a warehouse or user). 31 | 32 | Navigate to your "Snowflake Resource Optimization" folder, and click on the "Snowflake Resource Optimization: Setup & Configuration" app. Toggle through the application tabs (Warehouses, Users, Tasks, Else), review query results, and try out a couple of actions, e.g.: 33 | - Set auto-resume for a warehouse without it 34 | - Suspend an idle warehouse 35 | - Disable a user who has never logged in 36 | - etc. 37 | 38 | ## Summary 39 | 40 | You should now have Snowflake Resource Optimization: Setup & Configuration within your Retool instance! 🥳 41 | 42 | **Next steps:** You can choose to extend or adjust this app in any way. 43 | 44 | Please consider contributing back to this Github repo if you make something you think other people would like to use! Get in touch by opening a Github Issue if you have an idea you want to run by us. 45 | 46 | **Questions, comments, feedback?** Please open a Github Issue and let us know! 47 | -------------------------------------------------------------------------------- /snowflake-resource-optimization/setup-guides/set-up-retool-resource.md: -------------------------------------------------------------------------------- 1 | # App Setup - Step 2: Resources in Retool 2 | 3 | ## What's a Resource in Retool? 4 | A Resource is a core concept in Retool. A Resource is a config that defines a backend data source that your Retool apps can talk to. Retool supports a variety of different kinds of backends, from databases to REST API calls, to special integrations like Stripe. 5 | 6 | You can define a Resource once, then use it again and again in your Retool apps. 7 | 8 | Read more about Resources in [the Retool docs](https://docs.retool.com/docs/integrations-overview). 9 | 10 | ## Output 11 | The goal of this section is to create: 12 | * A Resource called `Snowflake DB - Account Admin`. This will be a reusable way to hit Snowflake as the user from Step 1. 13 | 14 | Note: The name here is important! The app code we provide expects the Resource to have this name. (You can choose to rename it, after you've finished setting everything up.) 15 | 16 | ## Step 3.1 - Create the "Snowflake DB - Account Admin" Resource 17 | 1. On the Resources page (`/resources`) in Retool, click "Create New" in the upper right. 18 | 2. Select the "Snowflake" resource type. 19 | 3. Fill out the resource information as follows. (For more info, see [our docs on Snowflake integration](https://docs.retool.com/docs/snowflake-integration).) 20 | 21 | ``` 22 | Name: Snowflake DB - Account Admin 23 | 24 | Account name: 25 | Database name: SNOWFLAKE 26 | Database schema: [leave blank] 27 | Database warehouse: [leave blank] 28 | User role: ACCOUNTADMIN (this role is required) 29 | 30 | Authentication: Password (you must use password authentication) 31 | Database username: 32 | Database password: 33 | 34 | [X] Disable converting queries to prepared statements (you must check this box) 35 | ``` 36 | 37 | Note: You must check the 'Disable converting queries to prepared statements' box (see more info [here](https://docs.retool.com/docs/sql-in-retool#prepared-statements)). 38 | 39 | 4. Click "Save changes". 40 | 41 | Setting up the Snowflake resource 42 | 43 | 44 | ## Summary 45 | You now have a Snowflake resource defined in Retool, named `Snowflake DB - Account Admin`. 46 | 47 | Reminder: The name here is important! The app code we provide expects the Resource to have this name. (You can choose to rename it, after you've finished setting everything up.) 48 | 49 | Now, we are ready to import our app! 50 | 51 | ## Next step 52 | [Step-by-step: Set up Retool App](./set-up-retool-app.md) 53 | -------------------------------------------------------------------------------- /snowflake-resource-optimization/setup-guides/set-up-snowflake.md: -------------------------------------------------------------------------------- 1 | # App Setup - Step 1: Snowflake 2 | 3 | ## What's Snowflake? 4 | Snowflake provides cloud-based data storage and analytics services, and if you're here, you're likely already using it! The Snowflake Resource Optimization: Setup & Configuration app is designed to help you manage your Snowflake resources. 5 | 6 | ## Output 7 | The goal of this section is to: 8 | * Ensure you have a Snowflake user with the ACCOUNTADMIN role 9 | 10 | ## Step 1.1 - Snowflake user 11 | The Snowflake Resource Optimization: Setup & Configuration app requires a Snowflake user with the ACCOUNTADMIN role. 12 | 13 | If you would like to create a new Snowflake user for Retool, please follow Snowflake's instructions on [creating a user](https://docs.snowflake.com/en/user-guide/admin-user-management.html#creating-users). Again, please ensure that this user has the [ACCOUNTADMIN](https://docs.snowflake.com/en/user-guide/security-access-control-considerations.html#using-the-accountadmin-role) role. 14 | 15 | ## Summary 16 | You now have a Snowflake user with the ACCOUNTADMIN role, which you can plug into the next step. 17 | 18 | ## Next step 19 | [Step-by-step: Set up Retool Resource](./set-up-retool-resource.md) 20 | -------------------------------------------------------------------------------- /snowflake-uar-reviews/README.md: -------------------------------------------------------------------------------- 1 | # User Access Reviews by Snowflake 2 | 3 | ## Why you need this 4 | This Retool app makes it easy to perform periodic reviews of employee access rights. Navigate the process of preparing data, reviewing access rights, and remediating within an easy-to-use GUI. 5 | 6 | 7 | 8 | ## What does the User Access Review app do? 9 | Provides an interface to re-evaluate user roles, access privileges, and credentials to mitigate insider threats, prevent insider mistakes, reduce excessive access, and meet compliance requirements such as HIPAA, FedRAMP, SOX, and more. Take actions such as approving or revoking privileges and enter your reasons for doing so. Submit individual changes or update multiple records in bulk. 10 | 11 | This repo includes all code needed to source user roles within your Snowflake instance and generate the table schemas that the application attaches to. Once you load this application, you have the ability to modify the source code as you need to align to your own organization’s processes. This repo: 12 | 13 | * Provides a schema to use on Snowflake 14 | * Provides a manager review application that implements filtering at the manager level 15 | * An admin application to get started with user and role entry 16 | * A review application to audit manager submissions for super-users and compliance teams 17 | 18 | ## Caveats 19 | * If you need row-level security reads and writes, talk to your Retool account team 20 | * The template assumes a simple hierarchy (reviewer to employee), however you can customize this by modifying the schema 21 | * These applications are meant to be configurable to meet your needs 22 | ## Screenshots 23 | Manager Review Portal 24 |

25 | Manager Review Portal 26 |

27 | Compliance Review Portal 28 |

29 | Manager Review Portal 30 |

31 | Create Review Cycle Admin Application 32 |

33 | Manager Review Portal 34 |

35 |

36 | Manager Review Portal 37 |

38 |

39 | Manager Review Portal 40 |

41 | 42 | ## Technical Details and Setup 43 | ### Dependencies - backends 44 | The Snowflake User Access Roles Review app depends on the following backend services: 45 | - Snowflake 46 | 47 | ### How to set up Snowflake User Access Roles Review in your Retool instance 48 | See the detailed setup guides in the [Setup Guides folder](./setup-guides). 49 | 50 | As an overview, these guides will take you through: 51 | 52 | 1. Set up Snowflake 53 | [Step-by-step: Set up Snowflake](./setup-guides/set-up-snowflake.md) 54 | 55 | 2. Set up your Snowflake database as a Resource in Retool 56 | [Step-by-step: Set up Retool Resource](./setup-guides/set-up-retool-resource.md) 57 | 58 | 3. Set up the Retool app file 59 | [Step-by-step: Set up Retool App](./setup-guides/set-up-retool-app.md) 60 | 61 | 62 | 63 | ## How to contribute 64 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 65 | -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/compliance_review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/compliance_review.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/create_cycle_employees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/create_cycle_employees.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/create_cycle_name_cycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/create_cycle_name_cycle.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/create_cycle_system_roles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/create_cycle_system_roles.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/manager_review_bulk_approve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/manager_review_bulk_approve.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/manager_review_bulk_revoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/manager_review_bulk_revoke.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/images/manager_review_landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/images/manager_review_landing.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/setup-guides/images/snowflake_resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/snowflake-uar-reviews/setup-guides/images/snowflake_resource.png -------------------------------------------------------------------------------- /snowflake-uar-reviews/setup-guides/set-up-retool-app.md: -------------------------------------------------------------------------------- 1 | # App Setup - Step 3: Apps in Retool 2 | 3 | ## What's an App in Retool? 4 | As the name suggests, an App is a single "view" (or "page") that you can build in Retool. An App is built from a combination of Components to construct the UI, and Queries that define calls to your Resources. Queries can do reads or writes. 5 | 6 | ## Output 7 | The goal of this section is: 8 | * To get the 3 applications that make up Snowflake User Access Roles Review into your Retool instance. 🚀 9 | 10 | 11 | ## Step 3.1 - Download the app "code" 12 | In this step, you will obtain the code file in the `/code` directory in this Github repo. 13 | 14 | To do this, clone this Github repo: 15 | `git clone https://github.com/tryretool/retool-app-exchange.git` 16 | 17 | (Alternatively, you can manually click-to-download the file in the [`/code`](../code) directory.) 18 | 19 | You will have the following 3 files: 20 | - `Compliance_Review_Portal.json` 21 | - `Create_Review_Cycle.json` 22 | - `Manager_Review_Portal.json` 23 | 24 | ## Step 3.2 - Import the app "code" 25 | First, let's create a folder to put this code into, to keep things organized. 26 | 1. On your Retool home page, click "Create new" → "Create a new folder". Give this folder a name you like, e.g. "Snowflake User Access Roles Review". 27 | 28 | 2. Now, let's import the app. 29 | * Click "Create new" → "Import an app". 30 | * Upload the `Compliance_Review_Portal.json`, `Create_Review_Cycle.json`, `Manager_Review_Portal.json` files, and select the folder you just created ("Snowflake User Access Roles Review") as the destination. 31 | 32 | ## Step 3.3 - Test it out 33 | In this section, we're going to test out the functionality in your new app. 34 | 35 | 1. Identify the employees and system roles for which we want to review: 36 | * The Create_Review_Cycle Retool application will allow you to manually input all the _EMPLOYEE_ and _SYSTEM_ROLES_ data required to generate the _ACCESS_REVIEWS_ table 37 | * If this is your first time using the app, create a couple example employees, system roles, and then create a review cycle. Make sure that you're email address used to login to Retool is one of the employees and you are listed as the reviewer email in the employees tab of the Create_Review_Cycle application. 38 | * Alternatively, you can replace the _EMPLOYEE_ and _SYSTEM_ROLE_ tables with records from your existing ETL or database processes. Simply modify the createAccessReviews SQL in the Create_Review_Cycle application to point to the correct tables. 39 | 2. In the Create_Review_Cycle admin application, define a new review cycle 40 | * This will first save a new record in the _REVIEW_CYCLE_ table and then create the _ACCESS_REVIEWS_ table that will be used in the Manager_Review_Portal and Compliance_Review_Portal applications. 41 | 42 | 43 | ## Summary 44 | 45 | You should now have Snowflake User Access Roles Review app suite within your Retool instance! 🥳 46 | 47 | **Next steps:** You can choose to customize or extend the application any way you like. Introduce any necessary automations to the Compliance_Review_Portal application. Examples include: 48 | * Making an API request to create/update JIRA tickets 49 | * Making an API request to send a Slack notification in the compliance channel that the data is ready to be reviewed. 50 | 51 | Please consider contributing back to this Github repo if you make something you think other people would like to use! Get in touch by opening a Github Issue if you have an idea you want to run by us. 52 | 53 | **Questions, comments, feedback?** Please open a Github Issue and let us know! 54 | -------------------------------------------------------------------------------- /snowflake-uar-reviews/setup-guides/set-up-retool-resource.md: -------------------------------------------------------------------------------- 1 | # App Setup - Step 2: Resources in Retool 2 | 3 | ## What's a Resource in Retool? 4 | A Resource is a core concept in Retool. A Resource is a config that defines a backend data source that your Retool apps can talk to. Retool supports a variety of different kinds of backends, from databases to REST API calls, to special integrations like Stripe. 5 | 6 | You can define a Resource once, then use it again and again in your Retool apps. 7 | 8 | Read more about Resources in [the Retool docs](https://docs.retool.com/docs/integrations-overview). 9 | 10 | ## Output 11 | The goal of this section is to create: 12 | * A Resource called `Snowflake UAR`. This will be a reusable way to hit Snowflake as the user from Step 1. 13 | 14 | Note: The name here is important! The app code we provide expects the Resource to have this name. (You can choose to rename it, after you've finished setting everything up.) 15 | 16 | ## Step 3.1 - Create the "Snowflake UAR" Resource 17 | 1. On the Resources page (`/resources`) in Retool, click "Create New" in the upper right. 18 | 2. Select the "Snowflake" resource type. 19 | 3. Fill out the resource information as follows. (For more info, see [our docs on Snowflake integration](https://docs.retool.com/docs/snowflake-integration).) 20 | 21 | ``` 22 | Name: Snowflake UAR 23 | 24 | Account name: 25 | Database name: SNOWFLAKE 26 | Database schema: 27 | Database warehouse: 28 | User role: ACCOUNTADMIN (this role is required) 29 | 30 | Authentication: Password (you must use password authentication) 31 | Database username: 32 | Database password: 33 | ``` 34 | 35 | 4. Click "Save changes". 36 | 37 | Setting up the Snowflake resource 38 | 39 | 40 | ## Summary 41 | You now have a Snowflake resource defined in Retool, named `Snowflake UAR`. 42 | 43 | Reminder: The name here is important! The app code we provide expects the Resource to have this name. (You can choose to rename it, after you've finished setting everything up.) 44 | 45 | Now, we are ready to import our app! 46 | 47 | ## Next step 48 | [Step-by-step: Set up Retool App](./set-up-retool-app.md) 49 | -------------------------------------------------------------------------------- /snowflake-uar-reviews/setup-guides/set-up-snowflake.md: -------------------------------------------------------------------------------- 1 | # App Setup - Step 1: Snowflake 2 | 3 | ## What's Snowflake? 4 | Snowflake provides cloud-based data storage and analytics services, and if you're here, you're likely already using it! The Snowflake User Access Roles Review app is designed to help you conduct regular UAR reviews defined in your Snowflake resources. 5 | 6 | ## Output 7 | The goal of this section is to: 8 | * Ensure you have a Snowflake user with the ACCOUNTADMIN role 9 | * Ensure you create the necessary tables used in the Create Review Cycle, Manager Review Portal, and Compliance Review Portal Retool applications 10 | 11 | ## Step 1 - Snowflake user 12 | The Snowflake User Access Roles Review app requires a Snowflake user with the ACCOUNTADMIN role. 13 | 14 | If you would like to create a new Snowflake user for Retool, please follow Snowflake's instructions on [creating a user](https://docs.snowflake.com/en/user-guide/admin-user-management.html#creating-users). Again, please ensure that this user has the [ACCOUNTADMIN](https://docs.snowflake.com/en/user-guide/security-access-control-considerations.html#using-the-accountadmin-role) role. 15 | 16 | 17 | ## Step 2 - Create Schema 18 | 19 | Create the 4 tables used across the applications in your database: 20 | - _ACCESS_REVIEWS_ 21 | - _EMPLOYEES_ 22 | - _SYSTEM_ROLES_ 23 | - _REVIEW_CYCLES_ 24 | 25 | Please follow Snowflake's instructions on [creating a table](https://docs.snowflake.com/en/sql-reference/sql/create-table.html). 26 | 27 | ### Access_Reviews 28 | ``` 29 | create or replace TABLE ..ACCESS_REVIEWS ( 30 | ID NUMBER(38,0) NOT NULL DEFAULT ..ACCESS_REVIEWS_SEQ.NEXTVAL, 31 | STATUS VARCHAR(16777216), 32 | EMPLOYEE_EMAIL VARCHAR(16777216), 33 | REVIEW_CYCLE_ID NUMBER(38,0), 34 | SYSTEMROLE_ID VARCHAR(16777216), 35 | INITIALDECISIONREASON VARCHAR(16777216), 36 | UPDATED_AT TIMESTAMP_NTZ(9), 37 | SUBMITTED_AT TIMESTAMP_NTZ(9), 38 | ACTION VARCHAR(16777216), 39 | REASON VARCHAR(16777216), 40 | CLOSED_BY_EMAIL VARCHAR(16777216), 41 | CLOSED_AT TIMESTAMP_NTZ(9) 42 | ); 43 | ``` 44 | ### Employees 45 | 46 | ``` 47 | create or replace TABLE ..EMPLOYEES ( 48 | EMPLOYEE_ID NUMBER(38,0), 49 | EMAIL VARCHAR(16777216), 50 | NAME VARCHAR(16777216), 51 | REVIEWER_EMAIL VARCHAR(16777216) 52 | ); 53 | ``` 54 | 55 | ### System Roles 56 | ``` 57 | create or replace TABLE ..SYSTEM_ROLES ( 58 | ID NUMBER(38,0) NOT NULL DEFAULT ..SYSTEM_ROLES_SEQ.NEXTVAL, 59 | NAME VARCHAR(16777216) 60 | ); 61 | ``` 62 | ### Review Cycles 63 | ``` 64 | create or replace TABLE ..REVIEW_CYCLES ( 65 | ID NUMBER(38,0) NOT NULL DEFAULT ..REVIEW_CYCLES_SEQ.NEXTVAL, 66 | NAME VARCHAR(16777216), 67 | START_DATE DATE, 68 | END_DATE DATE, 69 | PREVIOUS_CYCLE_ID NUMBER(38,0) 70 | ); 71 | ``` 72 | ## Summary 73 | You now have a Snowflake user with the ACCOUNTADMIN role, which you can plug into the next step. You have also created the tables that will be used in the Retool applications. 74 | 75 | ## Next step 76 | [Step-by-step: Set up Retool Resource](./set-up-retool-resource.md) -------------------------------------------------------------------------------- /usage-viewer/README.md: -------------------------------------------------------------------------------- 1 | # Importable Usage Analytics Retool App 2 | 3 | If you're self-hosting Retool, we’ve created a Retool app you can import to surface usage and adoption metrics for your users and apps. 4 | 5 | This data will help you properly allocate developer resources toward app-building and understand how your teams are using Retool. 6 | 7 | **Note**: This app surfaces *directional* usage metrics – not the exact # of users you'll be invoiced for next month. 8 | 9 | 10 | 11 | ## Setup 12 | This app needs read access to [Retool's storage database](https://docs.retool.com/docs/configuring-retools-storage-database). Therefore, it's only suitable for use with self-hosted instances of Retool. 13 | 14 | > :warning: **For a smooth app import experience, we recommend upgrading to Retool v2.116 or later. If you are using [Spaces](https://docs.retool.com/org-users/guides/configure-spaces), you must be on version 3.18 or above.** 15 | 16 | ### 0. Make sure you have access! 17 | 18 | You should have the right approvals and know where to look to find the `POSTGRES_HOST`, `POSTGRES_PORT`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` environment variables in your deployment configuration. 19 | 20 | ### 1. Create a read-only database user 21 | 22 | Since you'll need to connect to Retool's storage database, which contains all your apps, users, and settings, we recommend creating a read-only user, with access to the following tables in the Retool storage database: 23 | 24 | - `page_saves` 25 | - `pages` 26 | - `folders` 27 | - `users` 28 | - `audit_trail_events` 29 | - `groups` 30 | - `resources` 31 | - `workflow_save` 32 | 33 | To create a user and set a password, use the following command when running `psql` in your `postgres` container. 34 | 35 | ```CREATE USER WITH PASSWORD ;``` 36 | 37 | To assign the required permissions, connect to the Retool storage database and use the following commands: 38 | 39 | ``` 40 | GRANT CONNECT ON DATABASE `` TO ; 41 | GRANT USAGE ON SCHEMA public TO ; 42 | GRANT SELECT ON page_saves TO ; 43 | GRANT SELECT ON pages TO ; 44 | GRANT SELECT ON folders TO ; 45 | GRANT SELECT ON users TO ; 46 | GRANT SELECT ON audit_trail_events TO ; 47 | GRANT SELECT ON groups to ; 48 | GRANT SELECT ON resources to ; 49 | GRANT SELECT ON groups to ; 50 | GRANT SELECT ON resources to ; 51 | GRANT SELECT ON organizations to ; 52 | GRANT SELECT ON workflow_save to ; 53 | GRANT SELECT ON workflow to ; 54 | GRANT SELECT ON workflow_run to ; 55 | ``` 56 | 57 | ## 2. Connect the Retool Postgres DB as a Resource 58 | 59 | 1. Navigate to the `/resources` page in your instance and click "Create a new resource" 60 | 2. Select Postgres as the resource type 61 | 3. Name the resource “Retool Audit Log” 62 | 4. To fill out the following details, depending on your deployment, check your secrets file in your deployment configuration. The relevant environment variables are: 63 | - Host: `POSTGRES_HOST` 64 | - Port: `POSTGRES_PORT` 65 | - Database name: `POSTGRES_DB` 66 | - Database username: `` 67 | - Database password: `` 68 | 69 | Save the resource, and it should be available for use by your apps. 70 | 71 | **Note**: If you have multiple instances, you’ll have a Postgres DB for each instance, which means you can only view usage analytics for that instance only. 72 | 73 | ### 3. Download the app code 74 | Download the app code from the `/code` directory in this repository. 75 | 76 | To do this, clone this Github repo: `git clone https://github.com/tryretool/retool-app-exchange.git` 77 | 78 | **Note**: If you are using Spaces and are on version v3.18 and above, download `usage_analytics_spaces.json` from the ['code/](..code) directory. 79 | 80 | ### 4. Import the app code 81 | On the [Retool main page](https://docs.retool.com/docs/protected-applications-getting-started#importing-the-application), click `Create new` and select `Import an app`. Upload the JSON file containing the app code, and name the app. 82 | 83 | ### 5. Rewire the Resource for each query 84 | If the Usage Analytics app doesn't populate with data, it's likely you'll need to update the Resource for each query. 85 | 86 | To update the queries to use the correct Resource, load the Usage Analytics app in editor mode and open the bottom panel to view the queries. Edit each query and select your Retool Postgres database from the Resource dropdown. (Don't forget to save your queries after editing them!) 87 | 88 | ## How to contribute 89 | Please open a Github Issue on this repo, and let us know about your interest in contributing! We encourage you to reach out before you get started building to get early feedback. 90 | -------------------------------------------------------------------------------- /usage-viewer/images/usage_analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryretool/retool-app-exchange/ea48efde500e831b0435a0e7ae1e28d1e5021e83/usage-viewer/images/usage_analytics.png --------------------------------------------------------------------------------