├── .gitignore ├── LICENSE ├── Procfile.txt ├── README.md ├── app.js ├── bin └── www ├── package.json ├── public ├── images │ ├── setup-list-test-1.png │ ├── setup-list-test-2.png │ ├── setup-list-test-3.png │ ├── setup-list-variable.png │ ├── setup-lookup-variable.png │ ├── setup-simplejson-datasource.png │ └── setup-template-variables.png └── stylesheets │ └── style.css ├── routes └── index.js ├── server └── data.json └── views ├── error.hbs ├── index.hbs └── layout.hbs /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | 4 | #Config 5 | config/default.json 6 | config/production.json 7 | 8 | #Public Keys 9 | *.pem 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules 42 | jspm_packages 43 | bower_components 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Cymatic Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile.txt: -------------------------------------------------------------------------------- 1 | web: node ./bin/www -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grafana SimpleJson Value Mapper 2 | A simple Node.js/Express application that provides basic template variable aliasing/value mapping. 3 | 4 | ## What Is It For? 5 | [Grafana](https://grafana.com/) is a popular data visualization platform that can query [many popular data sources](https://grafana.com/plugins?type=datasource) and visualize their data sets (most often time series data). 6 | 7 | #### Template Variables 8 | Once configured, [template variables](http://docs.grafana.org/reference/templating/) allow users to dynamically configure dashboards without having to be technically savvy enough to write or rewrite the underlying queries that power them. This is all driven through an intuitive visual interface within the dashboard itself. 9 | 10 |  11 | 12 | #### The Problem of Cryptic Values 13 | Many times the template variables that appear in these drop down lists are themselves the result of a dynamic query. Sometimes the values are human-readable, but othertimes they can be quite cryptic and it's hard for the dashboard user to know exactly what it is they are selecting or looking at. 14 | 15 | For example, imagine that you are querying a list of IDs that happen to be GUIDs/UUIDs. A single ID might look something like this: 16 | 17 | `123e4567-e89b-12d3-a456-426655440000` 18 | 19 | That doesn't tell the dashboard user anything about who the ID belongs to. Even if the user could be taught to memorize and match IDs to the things they belong to, as the list of those IDs grows, keeping track of them becomes even more cumbersome and potentially impossible. 20 | 21 | #### Aliasing Values to Something More Readable 22 | One solution would be to replace the hard-to-read values with something that is much more easy to read and understand for the user. Unfortunately there are two problems to this approach in Grafana: 23 | 24 | 1. Grafana has some aliasing options, but they generally do not work for dynamic template values, such as ones that are the results of a query. 25 | 2. Even if you could replace the unreadable value with something better, the underlying queries usually need the unreadable value, so the human-readable replacement would not work as a substitute when being fed into the query after the user selects it from the dashboard template menu. 26 | 27 | #### Creating a Dynamic Look-Up Service 28 | What is truly needed in these scenarios is a dynamic look-up service that can be given the unreadable values and return readable values for the template UI while also retaining the unreadable values to drive queries dynamically. It turns out that this is actually possible with Grafana, but requires the use of a datasource plugin called [SimpleJson datasource](https://grafana.com/plugins/grafana-simple-json-datasource). 29 | 30 | By using the SimpleJson datasource plugin, it is possible to create the dynamic look-up service that will satisfy the needs of human-readable values in the template variable menus while also retaining the unreadable values that generally drive the queries behind the scenes. The SimpleJson datasource turns a web application into a generic datasource in Grafana (which it communicates with via HTTP). 31 | 32 | #### Human-Readable Template Variables Solved 33 | This web application implements functionality to solve this look-up problem by implementing the contracts needed by the SimpleJson datasource plugin. It can be used as-is if your aliasing needs are simple and editing a JSON file on disk will suffice. You can also use this project as starting point for reference or by extending it yourself to tap into your own data sources for aliasing on the backend. 34 | 35 | ## Installation 36 | The following steps will help you setup a basic template variable look-up service using this web application. 37 | 38 | **Note**: This installation assumes you have Grafana installed and have sufficient privileges and know how to perform basic administrative tasks. 39 | 40 | ### Install the SimpleJson Datasource Plugin 41 | Follow the [instructions here](https://grafana.com/plugins/grafana-simple-json-datasource/installation) to install the SimpleJson datasource plugin. You will have to restart Grafana before the datasource plugin will be accessible. 42 | 43 | ### Install Grafana SimpleJson Value Mapper 44 | Install this project by cloning it or downloading it. By default when run from localhost, it should run on port 3003. 45 | 46 | ### Configure a SimpleJson datasource 47 | Once you've installed the plugin and the the web application, start the web application using node/npm and ensure it is running at http://localhost:3003 (or whever you are hosting it). 48 | 49 | If you've restarted Grafana, login as a user with sufficient privileges and create a new SimpleJson datasource that will talk to the web application. 50 | 51 |  52 | 53 | 54 | ### Configure the Aliasing Data 55 | In the web application you will find the aliasing data file here: `/server/data.json`. You can start out using the test data that already exists in the file just to verify things work or you can edit it to add your own data. 56 | 57 | Here's a quick explanation of the test data and basic schema of the data that the web application expects: 58 | 59 | ``` 60 | { 61 | "list": [ 62 | "value1", 63 | "value2", 64 | "value3" 65 | ], 66 | "lookup": { 67 | "value1": "Value 1", 68 | "value2": "Value 2", 69 | "value3": "Value 3" 70 | } 71 | } 72 | ``` 73 | 74 | Each top-level property (*"list"*, *"lookup"*, etc.) of the JSON object act as a separate dataset that you can reference when defining your template variables. This way if you have more than one set of look-up/aliasing data you don't have to put it all into one big unified list. 75 | 76 | The first entry, *"list"*, is an array that will only provide a non-aliased set of results back to Grafana's template variable query. Mostly it will be used in this example to emulate dynamic values that were received from a query from another datasource in Grafana. It does have limited usefulness in that it can be filtered with a basic string match using a "contains" approach. More on that later. 77 | 78 | Most aliasing datasets will look like the second entry, *"lookup"*, which is a hash/map, where the propery name (*value1*, *value2*, etc.) is the input/unreadable ID and its value is the human-readable alias. You will add as many of these look-up style entries as you need into the data.json file. 79 | 80 | Let's assume you had a couple regions of IoT devices for example that have GUIDs as their identifier: 81 | 82 | ``` 83 | { 84 | "IoT Outside": { 85 | "03e79c66-d624-459d-9a6c-caa40c428c29": "Camera North", 86 | "b61a9967-7363-4d6b-a51c-27b5a539eba5": "Camera South", 87 | "a0caf0b9-e5f0-4a54-b6fe-7eb8a692b3c9": "Camera East", 88 | "4d84b539-e81c-4341-a97d-aa5f133b6ee3": "Camera West" 89 | }, 90 | "IoT Inside": { 91 | "bd683289-ae38-4c09-9826-17c6243b7621": "Temperature Sensor", 92 | "9513dff8-6b32-4450-88ca-33786a147142": "Living Room Lights", 93 | } 94 | } 95 | ``` 96 | 97 | Your template variable queries to the configured SimpleJson datasource will reference *"IoT Outside"* or *"IoT Inside"* when doing aliasing look-ups. You can of course always just create a single, unified list; that is entirely up to you. You just need to create one top-level look-up entry/hash. 98 | 99 | Make sure to restart the web application after making any edits so that they will be live. 100 | 101 | ## Creating Template Variables 102 | Just for the sake of example we'll discuss how to setup a basic test using the demo data in data.json. Create a new dashboard and [configure its templating](http://docs.grafana.org/reference/templating/). 103 | 104 | First lets setup a template variable that will pull the list values into the first drop down menu: 105 | 106 |  107 | 108 | The *Variable Type* should be: `Query` 109 | 110 | The *Data source* property will be the SimpleJson datasource you setup earlier, in this example the `Template Aliasing` datasource. 111 | 112 | The query will be: `{ "data": "list" }`, where the *data* property references the top-level property/dataset in the data.json file (in this case: *list*). 113 | 114 | It's up to you if you want to include multiple values and the "All" option, but it's recommended to see how more advanced aliasing scenarios might work. 115 | 116 | Next setup the look-up template variable: 117 | 118 |  119 | 120 | The setup here is fairly similar except the query looks like this: 121 | 122 | `{ "data": "lookup", "id": "$list" }`. 123 | 124 | In this case, since *lookup* is a hash/map in data.json we can include the *id* property which says which ID it should return an alias for. This can be a hardcoded ID or set of IDs, but in this case we are using the *list* template variable directly/dynamically by using: `$list`. This way one template variable will dynamically drive the other. 125 | 126 | If you want to hardcode IDs it will look like this: 127 | 128 | `{ "data": "lookup", "id": "value1" }` (for single ID) 129 | 130 | `{ "data": "lookup", "id": "(value1|value2|value3)" }` (for multiple IDs) 131 | 132 | You template variables on your dashboard should look like this: 133 | 134 |  135 | 136 | ### Testing Things Out 137 | 138 | Now go back to your dashboard and check out the drop down list for both template variables: 139 | 140 |  141 | 142 |  143 | 144 | Selecting values from the first list should filter values from the second list (which in this example is the unreadable IDs; even though, yes, they are still readable): 145 | 146 |  147 | 148 | Now, if you were to use the template variables *$lookup* in a dashboard query, even though the menu labels are *Value 1* and *Value 3*, the actual value that would be injected into the query via the *$lookup* template variable would be *value1* and *value3*. Substitude *value1* and *value3* with a GUID instead and you'll start to get the idea. 149 | 150 | There is also a *contains* property which can be included in the template variable query that will check to see if the string you supply is contained by the value (case-insensitive, substring match) before returning the result: 151 | 152 | ### "Contains" Filter 153 | 154 | `{ "data":"lookup", "id":"$list", "contains":"2" }` 155 | 156 | That would only return *value2*/*Value 2* because only that entry contains the string "2" in the default test data in data.json. You could of course inject a dynamic value into the contains filter (only a single value supported though) from another template variable by putting its name with a "$" character in front of it, similar to how the two template variables in the above example interact dynamically. 157 | 158 | ### Real World 159 | A more likely scenario is that you will have a hidden template variable (you can opt to hide template variables when creating them) that will query a datasource which returns the unreadable IDs, like the IoT GUIDs in the exampel further up. Then you will pass those IDs over to the aliasing datasource which will have their human-readable names in a hash look-up in data.json. The results of that template variable will be what the user sees, and then you will use that template variable in your dashboard queries to query and visualize data related to those underlying GUIDs, but the user will only ever see the human-readable version of the IDs. 160 | 161 | ## Security 162 | If you want to secure things a bit more, you can enable HTTP BASIC authentication. When you configure your datasource in Grafana, click the checkbox to enable this and enter your desired user name and password. 163 | 164 | When you launch the Node.js web application set the environmental variables `HTTP_AUTH_USERNAME` and `HTTP_AUTH_PASSWORD` to the same values. This way, whenever a request is made to the aliasing service (`/search` in terms of the route), the request will be authenticated against these credentials. 165 | 166 | If you need more security you are encouraged to extend this project with your own solution and/or use more sophisticated proxying via Grafana. 167 | 168 | ## Conclusion 169 | This web application primarily serves as an example only. You might be able to get by with it if your needs are simple and manually updating the data.json file and restarting the web application are not going to be deal-breakers when updates to the look-up data occur. Otherwise you are encouraged to check out the code in `/routes/index.js` and alter it to fit your needs. 170 | 171 | Happy Mapping! 172 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | 8 | var index = require('./routes/index'); 9 | 10 | var app = express(); 11 | 12 | // view engine setup 13 | app.set('views', path.join(__dirname, 'views')); 14 | app.set('view engine', 'hbs'); 15 | 16 | // uncomment after placing your favicon in /public 17 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 18 | app.use(logger('dev')); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(cookieParser()); 22 | app.use(express.static(path.join(__dirname, 'public'))); 23 | 24 | app.use('/', index); 25 | 26 | // catch 404 and forward to error handler 27 | app.use(function(req, res, next) { 28 | var err = new Error('Not Found'); 29 | err.status = 404; 30 | next(err); 31 | }); 32 | 33 | // error handler 34 | app.use(function(err, req, res, next) { 35 | // set locals, only providing error in development 36 | res.locals.message = err.message; 37 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 38 | 39 | // render the error page 40 | res.status(err.status || 500); 41 | res.render('error'); 42 | }); 43 | 44 | module.exports = app; 45 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('grafanasimplejsonvaluemapper:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3003'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grafanasimplejsonvaluemapper", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "basic-auth": "^2.0.0", 10 | "body-parser": "~1.18.2", 11 | "cookie-parser": "~1.4.3", 12 | "debug": "~2.6.9", 13 | "express": "~4.15.5", 14 | "hbs": "~4.0.1", 15 | "morgan": "~1.9.0", 16 | "serve-favicon": "~2.4.5" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/images/setup-list-test-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-list-test-1.png -------------------------------------------------------------------------------- /public/images/setup-list-test-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-list-test-2.png -------------------------------------------------------------------------------- /public/images/setup-list-test-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-list-test-3.png -------------------------------------------------------------------------------- /public/images/setup-list-variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-list-variable.png -------------------------------------------------------------------------------- /public/images/setup-lookup-variable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-lookup-variable.png -------------------------------------------------------------------------------- /public/images/setup-simplejson-datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-simplejson-datasource.png -------------------------------------------------------------------------------- /public/images/setup-template-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CymaticLabs/GrafanaSimpleJsonValueMapper/ce0999b342f4e8e861509e7ce3def5d9a7debf10/public/images/setup-template-variables.png -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var auth = require('basic-auth'); 4 | var data = require('../server/data.json'); 5 | 6 | /** 7 | * Uses HTTP BASIC authentication to autheticate the request. 8 | * @param req The request object. 9 | * @param res The response object. 10 | * @param next The middleware callback. 11 | */ 12 | function isAuthorized(req, res, next) { 13 | // If there's no configured HTTP BASIC authentication user name or password, don't worry about authorization 14 | if (!process.env.HTTP_AUTH_USERNAME || !process.env.HTTP_AUTH_PASSWORD) { 15 | next(); 16 | } 17 | // Otherwise authorize the user 18 | else { 19 | var user = auth(req); 20 | 21 | // Ensure the user name and password match 22 | if (!user || user.name !== process.env.HTTP_AUTH_USERNAME || user.pass !== process.env.HTTP_AUTH_PASSWORD) { 23 | res.status(401); 24 | res.json({ error: 'Access Denied' }); 25 | res.end(); 26 | } 27 | else { 28 | next(); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Index page. 35 | */ 36 | router.get('/', function(req, res, next) { 37 | res.render('index', { title: 'Grafana SimpleJSON Value Mapper' }); 38 | }); 39 | 40 | /** 41 | * This is the API endpoint that Grafana Simple JSON Datasources uses for metrics and template variables. 42 | */ 43 | router.post('/search', isAuthorized, function(req, res, next) { 44 | // If there was no target to the search, return an empty result 45 | if (!req.body || !req.body.target || req.body.target.length === 0) { 46 | res.json([]); 47 | } 48 | // Otherwise, parse the target further 49 | else { 50 | // If the body isn't JSON, it's a metric search which is unsupported for this implementation 51 | if (req.body.target[0] !== "{") { 52 | res.status(400).json({ error: 'query should be a JSON object'}); 53 | return; 54 | } 55 | 56 | // Try parsing the query 57 | try { 58 | // Parse the query 59 | var query = JSON.parse(req.body.target); 60 | 61 | // Get the contains filter if one exists 62 | var containsFilter = query.contains ? query.contains.toString().toLowerCase() : null; 63 | 64 | // Prepare the results 65 | var results = []; 66 | 67 | // Validate 68 | if (!query.data || typeof(query.data) !== 'string') { 69 | res.status(400).json({ error: '"data" must be a string' }); 70 | return; 71 | } 72 | 73 | // Get the target data 74 | var values = data[query.data]; 75 | 76 | // Make sure they were found 77 | if (!values) { 78 | res.status(400).json({ error: 'no data found for data target: ' + query.data }); 79 | return; 80 | } 81 | 82 | // Is this an array/list of values? 83 | if (typeof(values.length) === 'number') { 84 | 85 | // Go through each value 86 | for (var i = 0; i < values.length; i++) { 87 | var val = values[i]; 88 | 89 | // If there's a contains filter, apply it 90 | if (!containsFilter || val.toString().toLowerCase().indexOf(containsFilter) !== -1) { 91 | // If so include it in the results. 92 | results.push({ text: val, value: val }); 93 | } 94 | } 95 | } 96 | // Otherwise this is an aliased look-up 97 | else { 98 | // Check to see if there is one or more ID filters 99 | var idFilters = {}; 100 | var hasIdFilters = query.id && query.id.length > 0; 101 | 102 | // If so, construct the ID filters object 103 | if (hasIdFilters) { 104 | // If it starts with parentheses, it's a list of IDs separated by '|' character 105 | if (query.id[0] === "(") { 106 | var rawIds = query.id.substring(1, query.id.length - 1); 107 | var parsedIds = rawIds.split("|"); 108 | 109 | // Add each filter ID 110 | for (var i = 0; i < parsedIds.length; i++) { 111 | idFilters[parsedIds[i]] = true; 112 | } 113 | } 114 | // Otherwise it's a single ID 115 | else { 116 | idFilters[query.id] = true; 117 | } 118 | 119 | // Only select from the ID filters 120 | for (var id in idFilters) { 121 | // Make sure the ID is found 122 | if (!values[id]) continue; 123 | 124 | // If there's a contains filter, apply it 125 | if (!containsFilter || id.toLowerCase().indexOf(containsFilter) !== -1) { 126 | results.push({ text: values[id], value: id }); 127 | } 128 | } 129 | } 130 | // Otherwise there are no ID filters, just go through all values 131 | else { 132 | // Go through each value 133 | for (var id in values) { 134 | // If there's a contains filter, apply it 135 | if (!containsFilter || id.toLowerCase().indexOf(containsFilter) !== -1) { 136 | results.push({ text: values[id], value: id }); 137 | } 138 | } 139 | } 140 | } 141 | 142 | // Return results 143 | res.json(results || []); 144 | } 145 | catch (e) { 146 | res.status(400).json({ error: e.toString() }); 147 | } 148 | } 149 | }); 150 | 151 | module.exports = router; 152 | -------------------------------------------------------------------------------- /server/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "list": [ 3 | "value1", 4 | "value2", 5 | "value3" 6 | ], 7 | "lookup": { 8 | "value1": "Value 1", 9 | "value2": "Value 2", 10 | "value3": "Value 3" 11 | } 12 | } -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 |
{{error.stack}}4 | -------------------------------------------------------------------------------- /views/index.hbs: -------------------------------------------------------------------------------- 1 |
See the github repository for more information.
3 | -------------------------------------------------------------------------------- /views/layout.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |