Kickstart your Digital Projects with a Custom App (a 25 min tutorial)
Jae Myong Wilson
Big Data | Business Intelligence Implementation | Freelance Domo Consultant
In this tutorial, I'll do exactly what it says on the tin: show you how to use Domo to host and build a custom app with a natural language processing workflow built into it.
Disclaimer: My name is Jae Wilson. I am a Sr. Technical Consultant and Technical Success Manager at Domo, but moreover, I like to think of myself as a tinkerer. I enjoy using data and technology to solve business problems and design digital products. My writing is my own and does not necessarily reflect the views of Domo.
The custom app we're going to build takes user input, translates it via Microsoft's Cognitive Services, then saves it as a dataset in Domo. To be honest, I wrote this tutorial AFTER I built the same workflow as a writeback connector; because I wanted to highlight how easy it is to design interactive workflows with data through the custom app framework.
After proudly showing off the app to my colleague, he snidely remarked, "So what? I can do that on my phone in Google." To which I proudly replied, "yes, but my app is powered by Domo." He still seemed nonplussed — simple minds— but it raised an interesting point. With Domo, clients can create, develop, and deploy new digital products and services (complete with data collection, governance, and reporting tooling) in hours.
So what all does it take to build a custom app? I could lie and say you need extensive HTML, JavaScript, and CSS skills, or you could do what I did — just lift a Microsoft tutorial and rewrite it using vanilla JavaScript. You don't even need any Data Science skills to implement natural language processing just leverage Microsoft's Translator Text API.
You might be wondering, "Jae, why are you publishing this? Why do should people re-skin a Microsoft tutorial in Domo?"
Why you should read this article (and complete the tutorial)
- Use this tutorial as inspiration for digital products, services or business workflows your company could develop AND
- Demonstrate how you can enrich data and workflows with input from 3rd-party APIs.
Nerd Note: Not-so-secretly, I have a third objective in this article, but I wanted to present it separately because it's a pretty academic (but vitally important). By storing and facilitating governed access to unstructured data, Domo delivers on providing a comprehensive integrated big-data platform.
- Custom apps use Domo's AppDb engine to store and access unstructured data in collections and documents—schema-on-read. For big-data enthusiasts, this welcome feature complements Domo's schema-on-write Connector framework.
- Collections in the AppDb database can be configured with near-real-time sync to Domo datasets—enabling up-to-minute analysis, visualization or ETL workflows.
For more details about my thoughts on Domo's Data Lake offering, check out the introduction to this article. Technical jargon aside, my goal is to work with clients to
Make Data More Interesting
For years, market leaders have described data as an asset more valuable than oil, but by the same coin, many organizations don't know what to do with their data. Consider these opportunities:
- In this tutorial, we'll translate text, but you could use this code to supercharge data scraped off your website, news feeds or social media using any number of natural language processing workflows.
- Domo won't replace enterprise resource planning systems, but interactive apps like this open up opportunities for automating multi-step business processes and workflows. Imagine blending an automated customer churn analysis with a customer retention workflow managed and actioned via a custom app.
- Poor data quality is one of the most challenging hurdles stymieing advanced analytics projects. What if you used apps to create a feedback loop that integrated fuzzy matching algorithms with data cleansing efforts.
Digital transformation is a phrase thrown about so haphazardly it's achieved meme status.
What if digital transformation required just the smallest small shift in how organizations thought about data? Instead of the status-quo fascination with historical reporting, what if we used data to design new (high-value) service or product.
Let's do this.
— Domo Tutorial Starts Here —
Custom Apps = HTML + JavaScript + CSS
If you pop over to developer.domo.com, the documentation references 'dev-studio' apps. These are synonymous with custom apps -- implemented via HTML, CSS, and JavaScript. You know... the same tools that underpin virtually ALL apps on the internet.
Before we can start developing the app, there are are a few foundational steps. Read the documentation carefully, but the highlights are:
Read the Dev-Studio Overview
- Install node.js
- Install the domo CLI by typing npm install -g ryuu in terminal.
Read the Quickstart
Initialize your project by typing domo init in the terminal.
Give your app a name then choose the hello world starter option. This will initialize your project with a library of template files for your app.
- app.css — style sheet for your app. (Pro tip: to give your apps a consistent look and feel, find out if your company's marketing team has a branded CSS template.)
- app.js — script the actions your app can take.
- domo.js — templated utility function for interacting with Domo's APIs (do not alter).
- index.html — define how the app is rendered on screen.
- manifest.json — stores metadata about the app including AppDb collections and dataset schemas.
Note: your starter option (manifest only or hello world) determines whether your template files are encapsulated in a new subfolder. You can re-organize or rename files or subfolders (except index.html) as you like, just update the paths to the files in your documents as appropriate. The name and default size of your app in Domo be determined by the contents of manifest.json.
Create a Live View of your app
Previously we used the NPM module domo to initialize our app. Next, we'll:
- run domo login to authenticate against your Domo instance.
- run domo dev to launch your browser with a rendered version of index.html. (at this point you should see your app name rendered in H2 style).
Moving forward, every time you save, your app preview should refresh in the browser. Note: the app will not be visible in your instance's asset library until you've published it via domo publish.
You can use any text editor (Notepad, Sublime, or Visual Studio Code for modifying your template files). I use Visual Studio Code.
Define the App form by updating index.html
As aforementioned, this tutorial was adapted from a longer Microsoft Tutorial. For today, we'll only implement the translator element; however, you could integrate the rest of the original tutorial to include sentiment analysis and text-to-speech conversion.
- remove the placeholders for the sentiment analysis and text-to-speech code blocks and remove the <form> tags.
- add app.js and domo.js as script sources. If you renamed or relocated the files, make sure to update accordingly.
<html lang="en"> <head> <!-- Required metadata tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="description" content="Translate and analyze text with Azure Cognitive Services."> <!-- Bootstrap CSS --> <link rel="stylesheet" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <title>Translate and analyze text with Azure Cognitive Services</title> </head> <body> <div class="container"> <h1>Translate, synthesize, and analyze text with Azure</h1> <p>This simple web app uses Azure for text translation, text-to-speech conversion, and sentiment analysis of input text and translations. </p> <div class="row"> <div class="col"> <!-- Enter text to translate. --> <div class="form-group"> <label for="text-to-translate"><strong>Enter the text you'd like to translate:</strong></label> <textarea class="form-control" id="text-to-translate" rows="5"></textarea> </div> <!-- Select output language. --> <div class="form-group"> <label for="select-language"><strong>Translate to:</strong></label> <select class="form-control" id="select-language"> <option value="ar">Arabic</option> <option value="ca">Catalan</option> <option value="zh-Hans">Chinese (Simplified)</option> <option value="zh-Hant">Chinese (Traditional)</option> <option value="hr">Croatian</option> <option value="en">English</option> <option value="fr">French</option> <option value="de">German</option> <option value="el">Greek</option> <option value="he">Hebrew</option> <option value="hi">Hindi</option> <option value="it">Italian</option> <option value="ja">Japanese</option> <option value="ko">Korean</option> <option value="pt">Portuguese</option> <option value="ru">Russian</option> <option value="es">Spanish</option> <option value="th">Thai</option> <option value="tr">Turkish</option> <option value="vi">Vietnamese</option> </select> </div> <button class="btn btn-primary mb-2" id="translate">Translate text</button> </br> <div id="detected-language" style="display: none"> <strong>Detected language:</strong> <span id="detected-language-result"></span> <br /> <strong>Detection confidence:</strong> <span id="confidence"></span> <br /> <br /> </div> <!-- Required Javascript for this tutorial --> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> <script src ?="domo.js"></script> <script src="app.js"></script> </body> </html>
Once you save index.html, notice that your app refreshes in the browser.
Pro Tip: if the app looks squashed, toggle 'fullscreen'. To avoid futzing with the resize toggle; modify the width and height nodes in manifest.json.
Our app layout is beautiful (thank you bootstrap css — see line 9) but the 'Translate text' button doesn't do anything. We'll need to add some JavaScript.
Capture the Click event with JavaScript
index.html describes how the page looks, css determines formatting and styling, now we're going to add interaction with JavaScript functions.
Disclaimer, I am not a JavaScript developer, I lifted a lot of my code from the Tutorial and took liberal advantage of Stack Overflow, but let's break this down into digestible steps.
To capture app interactions, add the following code to apps.js:
let but_translate = document.querySelector('#translate'); console.log(but_translate);
- We used the document .querySelector() method to return the first HTML element where id = "translate" and assigned it to a variable but_translate.
- console.log() facilities troubleshooting and debugging by printing to the browser. To see the log in your browser: Right Click > Inspect > Console.
- For now, we'll primarily use Console to debug our code. Later as we interact with APIs, we'll use the Network page to monitor traffic, and Sources to see failure points in our code.
To capture interactions with the app, we add an .addEventListener() to the 'Translate text' button. When the app registers a 'click' event, the listener triggers the execution of the translate() function.
function translate() { const translateRequest = { 'text_input': document.querySelector("#text-to-translate").value, 'translation_output': document.querySelector("#select-language").value }; console.log(translateRequest); } // setup the page and page interactions let but_translate = document.querySelector('#translate'); but_translate.addEventListener('click', (event) => { translate() });
Setup your Azure Account
Before we can call Microsoft Cognitive Services APIs, we'll need to configure an Azure account with access to the appropriate services.
Create your free Cognitive Services resource using Azure Portal.
- Azure allocates different keys for different API services. Take care if you're setting up multiple services at once.
Hold on to your seats, the code is gonna get dense real fast.
In addition to the translate() function, we'll define a separate function to get_data() from the Microsoft API.
// configuration --- must change ------------------ // const translator_key = 'key from azure portal' //query the API async function get_translation(text_input, language_output) { const base_url = '<https://api.cognitive.microsofttranslator.com/translate?api-version=3.0>' const params = '&to=' + language_output const constructed_url = base_url + params const req_data = [ { 'text': text_input } ]; const content = { "method": 'POST', "headers": { 'Ocp-Apim-Subscription-Key': translator_key, 'Content-type': 'application/json' }, "body": JSON.stringify(req_data) } const response = await fetch(constructed_url, content); console.log(response); return (response.json()) } async function translate() { const translateRequest = { 'text_input': document.querySelector("#text-to-translate").value, 'translation_output': document.querySelector("#select-language").value }; data = await get_translation(translateRequest.text_input, translateRequest.translation_output); } // setup the page and page interactions let but_translate = document.querySelector('#translate'); but_translate.addEventListener('click', (event) => { translate() });
At the bottom of get_data(), we use the fetch() function to request data from the API.
- Fetch() takes two arguments, URL and content. In this case, content will describe the request method (POST), include authentication headers (translator_key from Azure Portal), and provide the body of the request (text_input from the HTML form).
- With API requests, the required body format and headers are unique to each implementation, so check the documentation.
As you continue to read backward, you can see the constructed_url has been parameterized to accept the language_output, which was also taken from the dropdown button.
Check the browser for the results of console.log(response). Hopefully, you have a 200 response with a nice translation. If not, verify your Azure service key, header, and body.
Nerd Note: In the updated code blocks, we introduce asynchronous functions (async / await). Developers implemented fetch() as an asynchronous function to prevent blocking (waiting for extended chunks of time before the rest of the code finishes running). Asynchronous code allows multiple
Historically asynchronous functions were coded with promise chains, .then(), **async/await was developed to write asynchronous code that reads like synchronous code.
... yeah, that reads like Greek to me too ... check out some other articles for more information.
Print results to the HTML Document
Having retrieved a translation from the Microsoft API, we're now ready to update the user interface. We'll create a function fn_print() to update document objects.
- Instead of setting variables equal to .querySelector().value— as in the start of translate()— do the reverse and update the .textContent of HTML objects.
- Alter the visibility of the confidence <span> by setting the CSS .style based on whether a detected_language was returned.
... function fn_print(data) { res = data[0]; const translation_result = res.translations[0].text; const detected_language = res.detectedLanguage.language; const translation_confidence = res.detectedLanguage.score; document.querySelector("#translation-result").textContent = translation_result; document.querySelector("#detected-language-result").textContent = detected_language; if (document.querySelector("#detected-language-result").textContent !== "") { document.querySelector("#detected-language").style.display = "block"; } document.querySelector("#confidence").textContent = translation_confidence; return (true); }; async function translate() { const translateRequest = { 'text_input': document.querySelector("#text-to-translate").value, 'translation_output': document.querySelector("#select-language").value }; data = await get_translation(translateRequest.text_input, translateRequest.translation_output); prn = fn_print(data); return (true); } ...
Write the App contents to a collection in AppDb
We're on the home stretch! All we have to do is capture our results as a document in an AppDb collection and sync it to Domo as a dataset!
- Note: a database in AppDb is called a collection, and records in the database are referred to as documents. We can perform standard CRUD (create, replace, update, and delete) operations through the API.
Before we can access the AppDb API via domo dev, we must first configure proxy settings as defined here.
Use domo publish to publish your app.
- Note: To publish, you'll need to include a 300x300 pixel thumbnail.png.
- Once you've published your app, manifest.json should include an id GUID.
Go to your instance of Domo and deploy your app via the Asset Library.
To setup the proxy in the dev environment, you'll need to identify the deployed app's appId.
- Inspect the page and find the <iframe> that contains your app's code. The URL should be of the form //{HASH}.domoapps.prodX.domo.com?userId=...
- The App ID can be found between // and .domoapps.
Update manifest.json with the proxyId / appId and define the schema of your dataset as it should appear in Domo.
- Note: documents in an AppDb collection do not necessarily have to follow the same schema; however, the schema defined in manifest.json must conform to the same rules all connectors adhere to—no nested key-value pairs and no nested arrays.
{ ... "mapping": [], "collections": [ { "name": "translation", "schema": { "columns": [ { "name": "translation_result", "type": "STRING" }, { "name": "detected_language", "type": "STRING" }, { "name": "translation_confidence", "type": "DECIMAL" }, { "name": "translation_input", "type": "STRING" } ] }, "syncEnabled": true } ], "id": "definedDuringPublish", "proxyId": "yourAppId" }
Update app.js to write documents to AppDb and sync with Domo
We'll finish off this app by adding two actions: POST a new document to AppDb, then POST a manual export to sync the collection to a Domo dataset.
- define a function write_domo() that performs the POST.
- extend fn_print() to call write_domo() and end with a manual export request.
// configuration --- must change ------------------ // const translator_key = 'your Azure Key here' const collection = 'translation' const collection_url = `/domo/datastores/v1/collections/${collection}/documents/` async function write_domo(new_document) { response = await domo.post(collection_url, new_document); console.log('response: ' + JSON.stringify(response)); } //query the API async function get_translation(text_input, language_output) { const base_url = 'https://api.cognitive.microsofttranslator.com/translate?api-version=3.0' const params = '&to=' + language_output const constructed_url = base_url + params const req_data = [ { 'text': text_input } ]; const content = { "method": 'POST', "headers": { 'Ocp-Apim-Subscription-Key': translator_key, 'Content-type': 'application/json' }, "body": JSON.stringify(req_data) } const response = await fetch(constructed_url, content); return ( response.json() ) } async function fn_print(data, text_input) { res = data[0]; const translation_result = res.translations[0].text; const detected_language = res.detectedLanguage.language; const translation_confidence = res.detectedLanguage.score; document.querySelector("#translation-result").textContent = translation_result; document.querySelector("#detected-language-result").textContent = detected_language; if (document.querySelector("#detected-language-result").textContent !== "") { document.querySelector("#detected-language").style.display = "block"; } document.querySelector("#confidence").textContent = translation_confidence; new_document = { "content": { "translation_result": translation_result, "detected_language": detected_language, "translation_confidence": translation_confidence, "translation_input": text_input } }; write_domo(new_document); await domo.post(`/domo/datastores/v1/export`); return(true); }; async function translate() { const translateRequest = { 'text_input': document.querySelector("#text-to-translate").value, 'translation_output': document.querySelector("#select-language").value }; data = await get_translation(translateRequest.text_input, translateRequest.translation_output); prn = fn_print(data, translateRequest.text_input); return(true); } // setup the page and page interactions let but_translate = document.querySelector('#translate'); but_translate.addEventListener('click', (event) => { translate() });?
You made it! Thanks for sticking with me to the bitter end.
Your Digital Transformation Journey begins with a single App!
Hopefully, this is the first of many small steps on an exciting journey into data curiosity with Domo! It may seem trivial, but I don’t want to underemphasize the value this tutorial gives you and your team.
Reporting and Analytics is a monster we'll have to wrestle with from now until the end of time, and there are a multitude of products that can help you build pretty charts and graphs (I'll toss my hat in the ring and say Domo is one of the better data collaboration tools I've worked with.)
When it comes to your company's play at digital disruption or transformation <ugh, sorry for the buzz words > think about the products and services you interact with every day. I'll bet it's Waze or Google Maps powered by crowdsourced data mashed up with geocoding services telling you the fastest way to get to work. Or maybe it's your curated news feed based on tagged articles mashed up with a clustering algorithm occasionally interrupted by ads based on demographics and more clustering.
Either way, custom apps underpinned by Domo's integrated big-data platform are your ticket to becoming 'the next big thing' in Data.
So, if you haven’t written a line of code yet, I hope you’ll do what I did. Crack a beer, grab a few like-minded friends, stop fretting about the boring stuff and just get started with Domo.
— The End —
My name is Jae Wilson. Although my official title is Sr. Technical Consultant and Technical Success Manager at Domo, I like to think of myself as a tinkerer. I enjoy using data and technology to solve business problems and design digital products. My writing is my own and does not necessarily reflect the views of Domo.