├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── cli ├── assets.js ├── auth.js ├── builddash.js ├── cli.js ├── constants.js ├── datafns.js ├── env.js ├── exec.js ├── init.js ├── pkgjson.js ├── prompts.js ├── snapshot.js ├── splunkd.js └── vercel.js ├── license_report.txt ├── package.json ├── template ├── .gitignore ├── .prettierrc ├── next.config.js ├── package.json ├── public │ ├── favicon.ico │ └── fonts │ │ ├── inconsolata-regular.woff │ │ ├── proxima-bold-webfont.woff │ │ ├── proxima-regular-webfont.woff │ │ ├── proxima-semibold-webfont.woff │ │ ├── splunkdatasans-bold.woff2 │ │ ├── splunkdatasans-regular.woff2 │ │ └── splunkdatasans-semibold.woff2 ├── src │ ├── _version.json │ ├── autoupdate.js │ ├── components │ │ ├── dashboard.js │ │ ├── home.js │ │ ├── loading.js │ │ ├── nossr.js │ │ └── page.js │ ├── datasource.js │ ├── drilldown.js │ ├── pages │ │ ├── [dashboard].jsx │ │ ├── _document.jsx │ │ ├── api │ │ │ ├── _version.json │ │ │ ├── data │ │ │ │ ├── [dsid].js │ │ │ │ ├── _snapshot.json │ │ │ │ ├── package.json │ │ │ │ └── yarn.lock │ │ │ ├── package.json │ │ │ └── version.js │ │ └── index.jsx │ ├── polyfills.js │ ├── preset.js │ ├── ready.js │ └── styles.js └── yarn.lock └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .vscode 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | /test-project 24 | 25 | /tmp 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020, Splunk Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dashboard Publisher 2 | 3 | **EXPERIMENTAL** tool that creates a Next.js project for a given list of Splunk dashboards, optionally making the dashboards accessible to anyone using [Vercel](https://vercel.com). Search results are proxied through serverless functions, which handle authentication and efficient CDN caching. 4 | 5 | ## Prerequisites 6 | 7 | - Node.js 12+, NPM, Yarn 8 | - [Vercel CLI](https://vercel.com/download) if you want to publish on Vercel 9 | 10 | ## Get started 11 | 12 | 1. Install `dashpub` CLI globally 13 | 14 | ```sh-session 15 | $ npm i -g @splunk/dashpub 16 | ``` 17 | 18 | 2. Initialize a new project 19 | 20 | ```sh-session 21 | $ dashpub init 22 | ``` 23 | 24 | Follow the instructions to create your dashboard project. 25 | 26 | --- 27 | 28 | Copyright 2020 Splunk Inc. 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | http://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License. 41 | -------------------------------------------------------------------------------- /cli/assets.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const { splunkd } = require('./splunkd'); 18 | const { writeFile } = require('fs-extra'); 19 | const sharp = require('sharp'); 20 | const crypto = require('crypto'); 21 | const path = require('path'); 22 | const fetch = require('node-fetch'); 23 | require('dotenv').config(); 24 | 25 | function shortHash(buffer) { 26 | const h = crypto.createHash('sha256'); 27 | h.write(buffer); 28 | h.end(); 29 | return h.digest('hex').slice(0, 20); 30 | } 31 | 32 | function parseDataUri(dataUri) { 33 | if (!dataUri.startsWith('data:')) { 34 | throw new Error('Invalid data URI'); 35 | } 36 | const semiIdx = dataUri.indexOf(';'); 37 | if (semiIdx < 0) { 38 | throw new Error('Invalid data URI'); 39 | } 40 | const mime = dataUri.slice(5, semiIdx); 41 | if (!dataUri.slice(semiIdx + 1, 7) === 'base64,') { 42 | throw new Error('Unsupported data URI encoding'); 43 | } 44 | const data = Buffer.from(dataUri.slice(semiIdx + 8), 'base64'); 45 | return [mime, data]; 46 | } 47 | 48 | const seenImages = {}; 49 | 50 | async function storeImage(data, mimeType, { name = 'img', projectDir }) { 51 | let optimzed = data; 52 | let filename; 53 | 54 | switch (mimeType) { 55 | case 'image/svg+xml': 56 | filename = `${name}.svg`; 57 | break; 58 | case 'image/jpeg': 59 | case 'image/jpg': 60 | filename = `${name}.jpg`; 61 | optimzed = await sharp(data) 62 | .jpeg() 63 | .toBuffer(); 64 | break; 65 | case 'image/png': 66 | filename = `${name}.png`; 67 | optimzed = await sharp(data) 68 | .png() 69 | .toBuffer(); 70 | break; 71 | case 'image/gif': 72 | filename = `${name}.gif`; 73 | break; 74 | default: 75 | throw new Error(`Unsupported mime type: ${mimeType}`); 76 | } 77 | 78 | filename = `${shortHash(optimzed)}_${filename}`; 79 | await writeFile(path.join(projectDir, 'public/assets', filename), optimzed); 80 | 81 | return filename; 82 | } 83 | 84 | async function downloadImage(src, assetType, splunkdInfo, projectDir) { 85 | if (!src) { 86 | return src; 87 | } 88 | if (src in seenImages) { 89 | return seenImages[src]; 90 | } 91 | const [type, id] = src.split('://'); 92 | 93 | if (type === 'https' || type === 'http') { 94 | const res = await fetch(src); 95 | 96 | const data = await res.buffer(); 97 | const mimeType = res.headers.get('Content-Type'); 98 | 99 | const filename = await storeImage(data, mimeType, { projectDir }); 100 | const newUri = `/assets/${filename}`; 101 | seenImages[src] = newUri; 102 | return newUri; 103 | } 104 | 105 | if (type === 'splunk-enterprise-kvstore') { 106 | const imgData = await splunkd( 107 | 'GET', 108 | `/servicesNS/nobody/splunk-dashboard-studio/storage/collections/data/splunk-dashboard-${assetType}/${encodeURIComponent(id)}`, 109 | splunkdInfo 110 | ); 111 | 112 | const [mimeType, data] = parseDataUri(imgData.dataURI); 113 | const filename = await storeImage(data, mimeType, { name: id, projectDir }); 114 | const newUri = `/assets/${filename}`; 115 | seenImages[src] = newUri; 116 | return newUri; 117 | } 118 | 119 | throw new Error(`Unexpected image type: ${type}`); 120 | } 121 | 122 | module.exports = { 123 | downloadImage, 124 | }; 125 | -------------------------------------------------------------------------------- /cli/auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const { getPackageJson } = require('./pkgjson'); 18 | const { writeDotenv } = require('./env'); 19 | const { splunkdPassword } = require('./prompts'); 20 | 21 | async function updatePassword() { 22 | const pkg = await getPackageJson(); 23 | const { user, url } = pkg.dashpub.splunkd; 24 | 25 | console.log(`Enter password for user ${user} at ${url}`); 26 | const password = await splunkdPassword(url, user); 27 | 28 | await writeDotenv({ 29 | splunkdUrl: url, 30 | splunkdUser: user, 31 | splunkdPassword: password, 32 | }); 33 | 34 | return { username: user, password, url }; 35 | } 36 | 37 | async function ensureAuth() { 38 | if (!process.env.SPLUNKD_PASSWORD) { 39 | return await updatePassword(); 40 | } 41 | 42 | const pkg = await getPackageJson(); 43 | const { user, url } = pkg.dashpub.splunkd; 44 | return { username: user, url, password: process.env.SPLUNKD_PASSWORD }; 45 | } 46 | 47 | module.exports = { 48 | updatePassword, 49 | ensureAuth, 50 | }; 51 | -------------------------------------------------------------------------------- /cli/builddash.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const { loadDashboard } = require('./splunkd'); 18 | const { downloadImage } = require('./assets'); 19 | const { generateCdnDataSources } = require('./datafns'); 20 | const { writeFile, mkdirp, remove } = require('fs-extra'); 21 | const { cli } = require('cli-ux'); 22 | const path = require('path'); 23 | 24 | const COMPONENT_CODE = `\ 25 | import React, { lazy, Suspense } from 'react'; 26 | import Loading from '../../components/loading'; 27 | import NoSSR from '../../components/nossr'; 28 | import definition from './definition.json'; 29 | 30 | const Dashboard = lazy(() => import('../../components/dashboard')); 31 | 32 | export default function () { 33 | return ( 34 | 35 | }> 36 | 37 | 38 | 39 | ); 40 | } 41 | `; 42 | 43 | async function generateDashboard({ name, targetName = name, app, projectFolder }, splunkdInfo) { 44 | const dash = await loadDashboard(name, app, splunkdInfo); 45 | const [dsManifest, newDash] = await generateCdnDataSources(dash, projectFolder); 46 | for (const viz of Object.values(newDash.visualizations || {})) { 47 | try { 48 | if (viz.type === 'viz.singlevalueicon') { 49 | viz.options.icon = await downloadImage(viz.options.icon, 'icons', splunkdInfo, projectFolder); 50 | } 51 | if (viz.type === 'splunk.singlevalueicon') { 52 | viz.options.icon = await downloadImage(viz.options.icon, 'icons', splunkdInfo, projectFolder); 53 | } 54 | if (viz.type === 'viz.img') { 55 | if (viz.options.src.match(/\$.*\$/g) ) 56 | console.log(`Skipping image download due to token ${viz.options.src}`) 57 | else{ 58 | viz.options.src = await downloadImage(viz.options.src, 'images', splunkdInfo, projectFolder); 59 | } 60 | } 61 | if (viz.type === 'splunk.image') { 62 | if (viz.options.src.match(/\$.*\$/g) ) 63 | console.log(`Skipping image download due to token ${viz.options.src}`) 64 | else{ 65 | viz.options.src = await downloadImage(viz.options.src, 'images', splunkdInfo, projectFolder); 66 | } 67 | } 68 | } catch (e) { 69 | console.error(`Failed to download image ${viz.options.icon || viz.options.src}`, e); 70 | } 71 | } 72 | 73 | if (newDash.layout.options.backgroundImage) { 74 | newDash.layout.options.backgroundImage.src = await downloadImage( 75 | newDash.layout.options.backgroundImage.src, 76 | 'images', 77 | splunkdInfo, 78 | projectFolder 79 | ); 80 | } 81 | 82 | const dir = path.join(projectFolder, 'src/dashboards', targetName); 83 | await mkdirp(dir); 84 | await writeFile(path.join(dir, 'definition.json'), Buffer.from(JSON.stringify(newDash, null, 2), 'utf-8')); 85 | await writeFile(path.join(dir, 'index.js'), COMPONENT_CODE, 'utf-8'); 86 | 87 | return [dsManifest, { [name]: newDash.title }]; 88 | } 89 | 90 | async function generate(app, dashboards, splunkdInfo, projectFolder) { 91 | console.log(`Generating ${dashboards.length} dashboards...`); 92 | // cleanup 93 | await remove(path.join(projectFolder, 'public/assets')); 94 | await remove(path.join(projectFolder, 'src/pages/api/data/_datasources.json')); 95 | await remove(path.join(projectFolder, 'src/dashboards')); 96 | 97 | // create required dirs 98 | await mkdirp(path.join(projectFolder, 'public/assets')); 99 | await mkdirp(path.join(projectFolder, 'src/pages/api/data')); 100 | 101 | let datasourcesManifest = {}; 102 | let dashboardsManifest = {}; 103 | 104 | for (const dashboard of dashboards) { 105 | const targetName = dashboard; 106 | cli.action.start(`Generating dashboard ${dashboard}`); 107 | const [dsManifest, dashboardInfo] = await generateDashboard( 108 | { 109 | name: dashboard, 110 | targetName, 111 | app, 112 | projectFolder, 113 | }, 114 | splunkdInfo 115 | ); 116 | 117 | datasourcesManifest = Object.assign(datasourcesManifest, dsManifest); 118 | Object.assign(dashboardsManifest, dashboardInfo); 119 | cli.action.stop(); 120 | } 121 | 122 | cli.action.start('Writing manifest files...'); 123 | await writeFile(path.join(projectFolder, 'src/pages/api/data/_datasources.json'), JSON.stringify(datasourcesManifest, null, 4), { 124 | encoding: 'utf-8', 125 | }); 126 | await writeFile(path.join(projectFolder, 'src/_dashboards.json'), JSON.stringify(dashboardsManifest, null, 4), { 127 | encoding: 'utf-8', 128 | }); 129 | cli.action.stop(); 130 | } 131 | 132 | module.exports = { 133 | generate, 134 | }; 135 | -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | Copyright 2020 Splunk Inc. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | const { initNewProject, generateDashboards } = require('./init'); 20 | const { getPackageJson } = require('./pkgjson'); 21 | const { ensureAuth, updatePassword } = require('./auth'); 22 | const { takeDataSnapshot, clearSnapshot } = require('./snapshot'); 23 | 24 | require('dotenv').config(); 25 | 26 | const usage = () => { 27 | console.error(`Usage: dashpub (init|update|auth|snapshot) [...options]`); 28 | process.exit(1); 29 | }; 30 | 31 | async function main([cmd]) { 32 | if (cmd === 'init') { 33 | await initNewProject(); 34 | } else if (cmd === 'auth') { 35 | await loadProject(); 36 | await updatePassword(); 37 | } else if (cmd === 'update') { 38 | const project = await loadProject(); 39 | const splunkdInfo = await ensureAuth(); 40 | await generateDashboards(project.dashboards, splunkdInfo, process.cwd()); 41 | if (project.settings.useDataSnapshots) { 42 | await takeDataSnapshot(process.cwd(), project, splunkdInfo); 43 | } else { 44 | await clearSnapshot(process.cwd()); 45 | } 46 | } else if (cmd === 'snapshot') { 47 | const project = await loadProject(); 48 | const splunkdInfo = await ensureAuth(); 49 | await takeDataSnapshot(process.cwd(), project, splunkdInfo); 50 | } else { 51 | usage(); 52 | } 53 | } 54 | 55 | async function loadProject() { 56 | const pkg = await getPackageJson(); 57 | if (!pkg.dashpub) { 58 | throw new Error('This project does not seem to dashpub-generated. Missing dashpub section in package.json'); 59 | } 60 | return pkg.dashpub; 61 | } 62 | 63 | main(process.argv.slice(2)).catch(e => { 64 | console.error(e); 65 | process.exit(1); 66 | }); 67 | -------------------------------------------------------------------------------- /cli/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const SPLUNK_DASHBOARDS_APP = 'search'; 18 | 19 | module.exports = { 20 | SPLUNK_DASHBOARDS_APP, 21 | }; 22 | -------------------------------------------------------------------------------- /cli/datafns.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const crypto = require('crypto'); 18 | 19 | const makeId = ds => { 20 | const h = crypto.createHash('sha256'); 21 | if (ds.query) { 22 | h.write(ds.query); 23 | } 24 | if (ds.queryParameters) { 25 | if (ds.queryParameters.earliest) { 26 | h.write(ds.queryParameters.earliest); 27 | } 28 | if (ds.queryParameters.latest) { 29 | h.write(ds.queryParameters.latest); 30 | } 31 | } 32 | if (ds.refresh) { 33 | h.write(ds.refresh); 34 | } 35 | if (ds.postprocess) { 36 | h.write(ds.postprocess); 37 | } 38 | h.end(); 39 | const s = h.digest('hex').slice(0, 24); 40 | let res = ''; 41 | for (let i = 0; i < s.length; i += 2) { 42 | res += (parseInt(s.slice(i, i + 2), 16) % 36).toString(36)[0]; 43 | } 44 | return res; 45 | }; 46 | 47 | const units = { 48 | ms: 1, 49 | s: 1000, 50 | m: 60000, 51 | h: 3600000, 52 | }; 53 | 54 | function parseRefreshTime(refresh, defaultValue = 400) { 55 | if (typeof refresh === 'number') { 56 | return refresh; 57 | } 58 | if (typeof refresh === 'string') { 59 | const m = refresh.match(/^(\d+)(ms|s|m|h)$/); 60 | if (m) { 61 | const v = parseInt(m[1]); 62 | if (!isNaN(v)) { 63 | const u = units[m[2]]; 64 | if (u) { 65 | const ms = v * u; 66 | 67 | if (ms < 1000) { 68 | console.log('WARN: Ignoring sub-second refresh time'); 69 | return defaultValue; 70 | } 71 | return Math.floor(ms / 1000); 72 | } 73 | } 74 | } 75 | const n = parseInt(refresh, 10); 76 | if (!isNaN(n)) { 77 | return n; 78 | } 79 | } 80 | return defaultValue; 81 | } 82 | 83 | async function generateCdnDataSource([key, ds], allDataSources) { 84 | let settings = ds.options; 85 | 86 | if (ds.type === 'ds.chain') { 87 | const base = allDataSources[ds.options.extend]; 88 | if (!base) { 89 | throw new Error(`Unable to find base search ${ds.options.extend} for data source ${key}`); 90 | } 91 | settings = { 92 | ...base.options, 93 | postprocess: ds.options.query, 94 | }; 95 | } 96 | 97 | if (!settings.query) { 98 | return null; 99 | } 100 | 101 | const id = makeId(settings); 102 | 103 | const dataSourceManifest = [ 104 | id, 105 | { 106 | search: { ...settings, refresh: parseRefreshTime(ds.options.refresh) }, 107 | app: 'search', 108 | id, 109 | }, 110 | ]; 111 | 112 | const dataSourceDefinition = [ 113 | key, 114 | { 115 | type: 'ds.cdn', 116 | name: ds.name, 117 | options: { 118 | uri: `/api/data/${id}`, 119 | "enableSmartSources": true 120 | }, 121 | }, 122 | ]; 123 | 124 | return [dataSourceManifest, dataSourceDefinition]; 125 | } 126 | 127 | async function generateCdnDataSources(def, projectDir) { 128 | const results = []; //await Promise.all(Object.entries(def.dataSources || {}).map(e => generateCdnDataSource(e, def.dataSources))); 129 | for (const e of Object.entries(def.dataSources || {})) { 130 | const res = await generateCdnDataSource(e, def.dataSources); 131 | if (res != null) { 132 | results.push(res); 133 | } 134 | } 135 | 136 | const dsManifest = Object.fromEntries(results.map(r => r[0])); 137 | const dataSourceDefinition = Object.fromEntries(results.map(r => r[1])); 138 | 139 | return [ 140 | dsManifest, 141 | { 142 | ...def, 143 | dataSources: dataSourceDefinition, 144 | }, 145 | ]; 146 | } 147 | 148 | module.exports = { 149 | generateCdnDataSources, 150 | }; 151 | -------------------------------------------------------------------------------- /cli/env.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const fs = require('fs-extra'); 18 | const path = require('path'); 19 | 20 | async function writeDotenv({ splunkdUrl, splunkdUser, splunkdPassword }, { destFolder = process.cwd() } = {}) { 21 | console.log('Writing splunkd password to .env'); 22 | await fs.writeFile( 23 | path.join(destFolder, '.env'), 24 | [ 25 | `SPLUNKD_URL=${splunkdUrl}`, 26 | `SPLUNKD_USER=${splunkdUser}`, 27 | `SPLUNKD_PASSWORD=${splunkdPassword}`, 28 | 'BROWSER=none', 29 | '', 30 | ].join('\n'), 31 | { 32 | encoding: 'utf-8', 33 | } 34 | ); 35 | } 36 | 37 | module.exports = { 38 | writeDotenv, 39 | }; 40 | -------------------------------------------------------------------------------- /cli/exec.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const { cli } = require('cli-ux'); 18 | const chalk = require('chalk'); 19 | const execa = require('execa'); 20 | 21 | class ExecError extends Error { 22 | constructor(message, code, stdout, stderr) { 23 | super(message); 24 | this.code = code; 25 | this.stdout = stdout; 26 | this.stderr = stderr; 27 | } 28 | } 29 | 30 | class Secret { 31 | constructor(value) { 32 | this.value = value; 33 | } 34 | } 35 | 36 | const exec = async (cmd, args, options) => { 37 | try { 38 | const rawArgs = args.map(a => (a instanceof Secret ? a.value : a)); 39 | const displayArgs = args.map(a => (a instanceof Secret ? '*******' : a)); 40 | cli.action.start(`${chalk.yellow('$')} ${cmd} ${displayArgs.join(' ')}`); 41 | const res = await execa(cmd, rawArgs, options); 42 | cli.action.stop(chalk.green('OK')); 43 | return res; 44 | } catch (e) { 45 | cli.action.stop(chalk.red('FAILED')); 46 | console.error(chalk.red(e.stderr)); 47 | const code = e.code; 48 | throw new ExecError(`${cmd} exited with code ${code}`, code, e.stdout, e.stderr); 49 | } 50 | }; 51 | 52 | module.exports = { 53 | exec, 54 | Secret, 55 | }; 56 | -------------------------------------------------------------------------------- /cli/init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | Copyright 2020 Splunk Inc. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | 19 | const prompts = require('./prompts'); 20 | const splunkd = require('./splunkd'); 21 | const { cli } = require('cli-ux'); 22 | const fs = require('fs-extra'); 23 | const path = require('path'); 24 | const { exec, Secret } = require('./exec'); 25 | const { generate } = require('./builddash'); 26 | const chalk = require('chalk'); 27 | const { updatePackageJson } = require('./pkgjson'); 28 | const { writeDotenv } = require('./env'); 29 | const { SPLUNK_DASHBOARDS_APP } = require('./constants'); 30 | const { initVercelProject } = require('./vercel'); 31 | 32 | const toFolderName = projectName => projectName.toLowerCase().replace(/[\W_]+/g, '-'); 33 | 34 | const postInitInstructions = ({ folderName }) => chalk` 35 | 36 | {green Project successfully generated in {bold ./${folderName}}} 37 | 38 | Next steps: 39 | 40 | {yellow $} cd ./${folderName} 41 | 42 | {gray # Start developing} 43 | 44 | {yellow $} yarn dev 45 | 46 | {gray Open a browser at http://localhost:3000} 47 | `; 48 | 49 | async function generateDashboards(selectedDashboards, splunkdInfo, destFolder) { 50 | await generate(SPLUNK_DASHBOARDS_APP, selectedDashboards, splunkdInfo, destFolder); 51 | } 52 | 53 | async function initNewProject() { 54 | console.log(`Welcome to DASHPUB, let's setup a new project.\n`); 55 | 56 | const projectName = await prompts.string('Project name:'); 57 | const folderName = await prompts.string('Folder name:', { 58 | default: toFolderName(projectName), 59 | }); 60 | 61 | console.log('\nEnter information to access your dashboards in Splunk Enterprise:'); 62 | 63 | const splunkdUrl = await prompts.splunkdUrl(); 64 | const splunkdUser = await prompts.splunkdUsername(); 65 | const splunkdPassword = await prompts.splunkdPassword(splunkdUrl, splunkdUser); 66 | 67 | const splunkdInfo = { 68 | url: splunkdUrl, 69 | username: splunkdUser, 70 | password: splunkdPassword, 71 | }; 72 | 73 | cli.action.start(`Loading dashboards from ${SPLUNK_DASHBOARDS_APP} app`); 74 | const dashboards = await splunkd.listDashboards(SPLUNK_DASHBOARDS_APP, splunkdInfo); 75 | cli.action.stop(`found ${dashboards.length} dashboards`); 76 | 77 | const selectedDashboards = await prompts.selectDashboards(dashboards); 78 | 79 | console.log(`\nCreating project in ./${folderName}`); 80 | const srcFolder = path.join(__dirname, '..'); 81 | const destFolder = path.join(process.cwd(), folderName); 82 | await fs.mkdir(destFolder); 83 | 84 | await fs.copy(path.join(srcFolder, 'template'), destFolder, { recursive: true }); 85 | const copyToDest = (p, opts) => fs.copy(path.join(srcFolder, p), path.join(destFolder, p), opts); 86 | await copyToDest('yarn.lock'); 87 | 88 | await updatePackageJson({ folderName, version: '1.0.0', projectName, splunkdUrl, splunkdUser, selectedDashboards }, { destFolder }); 89 | await writeDotenv({ splunkdUrl, splunkdUser, splunkdPassword }, { destFolder }); 90 | 91 | await exec('yarn', ['install'], { cwd: destFolder }); 92 | await generateDashboards(selectedDashboards, splunkdInfo, destFolder); 93 | 94 | await exec('git', ['init'], { cwd: destFolder }); 95 | await exec('git', ['add', '.'], { cwd: destFolder }); 96 | await exec('git', ['commit', '-m', 'initialized dashpub project'], { cwd: destFolder }); 97 | 98 | if (await prompts.confirm(`Setup Vercel project?`)) { 99 | await initVercelProject({ folderName, destFolder, splunkdUrl, splunkdUser, splunkdPassword }); 100 | } else { 101 | console.log(postInitInstructions({ folderName })); 102 | } 103 | } 104 | 105 | module.exports = { 106 | initNewProject, 107 | generateDashboards, 108 | }; 109 | -------------------------------------------------------------------------------- /cli/pkgjson.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const fs = require('fs-extra'); 18 | const path = require('path'); 19 | 20 | async function getPackageJson(folder = process.cwd()) { 21 | return JSON.parse(await fs.readFile(path.join(folder, 'package.json'), { encoding: 'utf-8' })); 22 | } 23 | 24 | async function updatePackageJson( 25 | { folderName, version, projectName, splunkdUrl, splunkdUser, selectedDashboards, settings }, 26 | { destFolder = process.cwd() } = {} 27 | ) { 28 | const pkg = await getPackageJson(destFolder); 29 | if (folderName != null) { 30 | pkg.name = folderName; 31 | } 32 | if (version != null) { 33 | pkg.version = version; 34 | } 35 | const prev = pkg.dashpub || { splunkd: {} }; 36 | pkg.dashpub = { 37 | projectName: projectName || prev.projectName, 38 | settings: Object.assign( 39 | { 40 | useDataSnapshots: false, 41 | }, 42 | prev.settings, 43 | settings 44 | ), 45 | splunkd: { 46 | url: splunkdUrl || prev.splunkd.url, 47 | user: splunkdUser || prev.splunkd.user, 48 | }, 49 | dashboards: selectedDashboards || prev.dashboards, 50 | }; 51 | 52 | await fs.writeFile(path.join(destFolder, 'package.json'), JSON.stringify(pkg, null, 4)); 53 | } 54 | 55 | module.exports = { 56 | updatePackageJson, 57 | getPackageJson, 58 | }; 59 | -------------------------------------------------------------------------------- /cli/prompts.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const inquirer = require('inquirer'); 18 | const fetch = require('node-fetch'); 19 | const { validateAuth } = require('./splunkd'); 20 | 21 | const noValidateHttpsAgent = new (require('https').Agent)({ 22 | rejectUnauthorized: false, 23 | }); 24 | 25 | inquirer.registerPrompt('checkbox-plus', require('inquirer-checkbox-plus-prompt')); 26 | 27 | const string = (prompt, opts) => 28 | inquirer 29 | .prompt({ 30 | name: 'value', 31 | message: prompt, 32 | ...opts, 33 | }) 34 | .then(({ value }) => value); 35 | 36 | const confirm = prompt => 37 | inquirer 38 | .prompt({ 39 | type: 'confirm', 40 | name: 'confirmed', 41 | message: prompt, 42 | }) 43 | .then(r => r.confirmed); 44 | 45 | const splunkdUrl = () => 46 | inquirer 47 | .prompt({ 48 | name: 'splunkdurl', 49 | message: 'Enter the splunkd URL of the splunk service you want to connect to:', 50 | default: 'https://my.splunkserver.com:8089', 51 | 52 | validate: async input => { 53 | if (input === 'https://my.splunkserver.com:8089') { 54 | throw new Error(`Don't use the example ;)`); 55 | } 56 | let url; 57 | try { 58 | url = new URL(input); 59 | } catch (e) { 60 | throw new Error(`Invalid URL: ${e.message}`); 61 | } 62 | 63 | try { 64 | url.pathname = '/'; 65 | url.search = '?output_mode=json'; 66 | const res = await fetch(url, { 67 | method: 'GET', 68 | agent: url.protocol === 'https:' ? noValidateHttpsAgent : undefined, 69 | }); 70 | 71 | if (res.status !== 200) { 72 | throw new Error(`Received unexpected response: HTTP status ${res.status}`); 73 | } 74 | 75 | const { generator } = await res.json().catch(() => ({})); 76 | 77 | if (generator == null || generator.version == null) { 78 | throw new Error('Received unexpected response, is this splunkd?'); 79 | } 80 | } catch (e) { 81 | throw new Error(`Failed to connect to splunkd: ${e.message}`); 82 | } 83 | 84 | return true; 85 | }, 86 | }) 87 | .then(({ splunkdurl }) => splunkdurl); 88 | 89 | const splunkdUsername = (defaultUser = 'admin') => 90 | inquirer 91 | .prompt({ 92 | name: 'username', 93 | message: 'Splunk user:', 94 | default: defaultUser, 95 | }) 96 | .then(({ username }) => username); 97 | 98 | const splunkdPassword = (url, user) => 99 | inquirer 100 | .prompt({ 101 | type: 'password', 102 | name: 'password', 103 | message: 'Password:', 104 | default: 'changeme', 105 | async validate(pwd) { 106 | try { 107 | await validateAuth({ url, user, password: pwd }); 108 | } catch (e) { 109 | throw new Error(`Failed to validate password: ${e.message}`); 110 | } 111 | return true; 112 | }, 113 | }) 114 | .then(({ password }) => password); 115 | 116 | const selectDashboards = dashboards => 117 | inquirer 118 | .prompt({ 119 | type: 'checkbox-plus', 120 | name: 'dashboards', 121 | message: 'Select one or more dashboards you want to publish', 122 | pageSize: 10, 123 | highlight: true, 124 | searchable: true, 125 | source: async (selected, input) => { 126 | const search = (input || '').toLowerCase(); 127 | const matching = dashboards.filter(d => d.name.toLowerCase().includes(search) || d.label.toLowerCase().includes(search)); 128 | return matching.map(({ name, label }) => ({ name: `${label} [${name}]`, short: label, value: name })); 129 | }, 130 | validate: selected => { 131 | if (selected.length > 0) { 132 | return true; 133 | } 134 | throw new Error('Please select one or more dashboards'); 135 | }, 136 | }) 137 | .then(({ dashboards }) => dashboards); 138 | 139 | module.exports = { 140 | string, 141 | confirm, 142 | splunkdUrl, 143 | splunkdUsername, 144 | splunkdPassword, 145 | selectDashboards, 146 | }; 147 | -------------------------------------------------------------------------------- /cli/snapshot.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | const path = require('path'); 19 | const qs = require('querystring'); 20 | const fetch = require('node-fetch'); 21 | const debug = require('debug')('datasnapshot'); 22 | const { cli } = require('cli-ux'); 23 | require('dotenv').config(); 24 | 25 | const qualifiedSearchString = query => (query.trim().startsWith('|') ? query : `search ${query}`); 26 | const sleep = ms => new Promise(r => setTimeout(r, ms)); 27 | 28 | async function fetchData(search, { id, app, refresh, splunkdUrl, splunkUser, splunkPassword }) { 29 | const log = require('debug')(`ds:${id}`); 30 | 31 | const agent = splunkdUrl.startsWith('https') 32 | ? new (require('https').Agent)({ 33 | rejectUnauthorized: false, 34 | }) 35 | : undefined; 36 | 37 | log('Executing search for data fn', id); 38 | const SERVICE_PREFIX = `servicesNS/${encodeURIComponent(splunkUser)}/${encodeURIComponent(app)}`; 39 | const r = await fetch(`${splunkdUrl}/${SERVICE_PREFIX}/search/jobs`, { 40 | method: 'POST', 41 | headers: { 42 | Authorization: `Basic ${Buffer.from([splunkUser, splunkPassword].join(':')).toString('base64')}`, 43 | 'Content-Type': 'application/x-www-form-urlencoded', 44 | }, 45 | body: qs.stringify({ 46 | output_mode: 'json', 47 | earliest_time: (search.queryParameters || {}).earliest, 48 | latest_time: (search.queryParameters || {}).latest, 49 | search: qualifiedSearchString(search.query), 50 | // reuse_max_seconds_ago: refresh, 51 | timeout: 120, 52 | }), 53 | agent, 54 | }); 55 | 56 | if (r.status > 299) { 57 | throw new Error(`Failed to dispatch job, splunkd returned HTTP status ${r.status}`); 58 | } 59 | const { sid } = await r.json(); 60 | log(`Received search job sid=${sid} - waiting for job to complete`); 61 | 62 | let complete = false; 63 | while (!complete) { 64 | const statusData = await fetch(`${splunkdUrl}/${SERVICE_PREFIX}/search/jobs/${encodeURIComponent(sid)}?output_mode=json`, { 65 | headers: { 66 | Authorization: `Basic ${Buffer.from([splunkUser, splunkPassword].join(':')).toString('base64')}`, 67 | }, 68 | agent, 69 | }).then(r => r.json()); 70 | 71 | const jobStatus = statusData.entry[0].content; 72 | if (jobStatus.isFailed) { 73 | throw new Error('Search job failed'); 74 | } 75 | complete = jobStatus.isDone; 76 | if (!complete) { 77 | await sleep(250); 78 | } 79 | } 80 | 81 | log('Search job sid=%s for data fn id=%s is complete', sid, id); 82 | 83 | const resultsQs = qs.stringify({ 84 | output_mode: 'json_cols', 85 | count: 10000, 86 | offset: 0, 87 | search: search.postprocess, 88 | }); 89 | const data = await fetch(`${splunkdUrl}/${SERVICE_PREFIX}/search/jobs/${sid}/results?${resultsQs}`, { 90 | method: 'GET', 91 | headers: { 92 | Authorization: `Basic ${Buffer.from([splunkUser, splunkPassword].join(':')).toString('base64')}`, 93 | }, 94 | agent, 95 | }).then(r => r.json()); 96 | 97 | return data; 98 | } 99 | 100 | function* findUsedDataSources(dashboardManifest, readJSON) { 101 | const seen = new Set(); 102 | 103 | for (const dashboard of Object.keys(dashboardManifest)) { 104 | const def = readJSON(`src/dashboards/${dashboard}/definition.json`); 105 | 106 | for (const viz of Object.values(def.visualizations)) { 107 | if (viz.dataSources) { 108 | for (const dsid of Object.values(viz.dataSources)) { 109 | const id = def.dataSources[dsid].options.uri.split('/').slice(-1)[0]; 110 | if (!seen.has(id)) { 111 | seen.add(id); 112 | yield id; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | async function takeDataSnapshot(projectRoot, project, splunkdInfo) { 121 | cli.action.start('Analyzing datasources from all dashboards'); 122 | const projectFile = filePath => path.join(projectRoot, filePath); 123 | const readJSON = filePath => JSON.parse(fs.readFileSync(projectFile(filePath), { encoding: 'utf-8' })); 124 | const dataSources = readJSON('src/pages/api/data/_datasources.json'); 125 | const baseOptions = { 126 | splunkdUrl: splunkdInfo.url, 127 | splunkUser: splunkdInfo.username, 128 | splunkPassword: splunkdInfo.password, 129 | }; 130 | 131 | if (!baseOptions.splunkPassword) { 132 | throw new Error('SPLUNKD_PASSWORD environment variable not set'); 133 | } 134 | 135 | const allData = {}; 136 | 137 | const usedDatasources = [...findUsedDataSources(readJSON('src/_dashboards.json'), readJSON)]; 138 | cli.action.stop(`found ${usedDatasources.length} unique data sources`); 139 | const progress = cli.progress({ 140 | format: 'Fetching search results | {bar} | {value}/{total} searches', 141 | }); 142 | let complete = 0; 143 | progress.start(usedDatasources.length, 0); 144 | 145 | for (let i = 0; i < usedDatasources.length; i += 10) { 146 | const chunk = usedDatasources.slice(i, i + 10); 147 | debug('Processing chunk idx=%d len=%d', i, chunk.length); 148 | const results = await Promise.all( 149 | chunk.map(id => 150 | (async () => { 151 | const { search, app } = dataSources[id]; 152 | debug('Fetching DS', id); 153 | const data = await fetchData(search, { id, app, ...baseOptions, refresh: 400 }); 154 | progress.update(++complete); 155 | return { id, data }; 156 | })() 157 | ) 158 | ); 159 | for (const { id, data } of results) { 160 | allData[id] = data; 161 | } 162 | } 163 | progress.stop(); 164 | 165 | cli.action.start('Writing snapshot file'); 166 | fs.writeFileSync(projectFile('src/pages/api/data/_snapshot.json'), JSON.stringify(allData, null, 2), { encoding: 'utf-8' }); 167 | cli.action.stop(); 168 | } 169 | 170 | async function clearSnapshot(projectRoot) { 171 | fs.writeFileSync(path.join(projectRoot, 'src/pages/api/data/_snapshot.json'), '{}', { encoding: 'utf-8' }); 172 | } 173 | 174 | module.exports = { 175 | clearSnapshot, 176 | takeDataSnapshot, 177 | }; 178 | -------------------------------------------------------------------------------- /cli/splunkd.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const fetch = require('node-fetch'); 18 | const { XmlDocument } = require('xmldoc'); 19 | 20 | const noValidateHttpsAgent = new (require('https').Agent)({ 21 | rejectUnauthorized: false, 22 | }); 23 | 24 | const qs = obj => 25 | Object.entries(obj) 26 | .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`) 27 | .join('&'); 28 | 29 | const splunkd = ( 30 | method, 31 | path, 32 | { body, url = process.env.SPLUNKD_URL, username = process.env.SPLUNKD_USER, password = process.env.SPLUNKD_PASSWORD } = {} 33 | ) => { 34 | return fetch(`${url}${path}`, { 35 | method, 36 | headers: { 37 | Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, 38 | }, 39 | body, 40 | agent: url.startsWith('https:') ? noValidateHttpsAgent : undefined, 41 | }).then(async res => { 42 | if (res.status > 299) { 43 | const msg = await extractErrorMessage(res, `Splunkd responded with HTTP status ${res.status} requesting ${path}`); 44 | throw new Error(msg); 45 | } 46 | return res.json(); 47 | }); 48 | }; 49 | 50 | async function extractErrorMessage(response, defaultMsg) { 51 | if (response.status === 404) { 52 | return defaultMsg; 53 | } 54 | try { 55 | const json = await response.json(); 56 | console.log(json); 57 | } catch (e) { 58 | // ignore 59 | } 60 | return defaultMsg; 61 | } 62 | 63 | const extractDashboardDefinition = xmlSrc => { 64 | const doc = new XmlDocument(xmlSrc); 65 | const def = JSON.parse(doc.childNamed('definition').val); 66 | const theme = doc.attr['theme']; 67 | return theme ? { ...def, theme } : def; 68 | }; 69 | 70 | const loadDashboard = ( 71 | name, 72 | app, 73 | { url = process.env.SPLUNKD_URL, username = process.env.SPLUNKD_USER, password = process.env.SPLUNKD_PASSWORD } = {} 74 | ) => 75 | splunkd('GET', `/servicesNS/-/${encodeURIComponent(app)}/data/ui/views/${encodeURIComponent(name)}?output_mode=json`, { 76 | url, 77 | username, 78 | password, 79 | }).then(data => extractDashboardDefinition(data.entry[0].content['eai:data'])); 80 | 81 | const listDashboards = async ( 82 | app, 83 | { url = process.env.SPLUNKD_URL, username = process.env.SPLUNKD_USER, password = process.env.SPLUNKD_PASSWORD } = {} 84 | ) => { 85 | const res = await splunkd( 86 | 'GET', 87 | `/servicesNS/-/${encodeURIComponent(app)}/data/ui/views?${qs({ 88 | output_mode: 'json', 89 | count: 0, 90 | offset: 0, 91 | search: `(isDashboard=1 AND isVisible=1 AND (version=2 OR version=1))`, 92 | })}`, 93 | { 94 | url, 95 | username, 96 | password, 97 | } 98 | ); 99 | 100 | return res.entry 101 | .filter(entry => entry.acl.app === app) 102 | .map(entry => ({ 103 | name: entry.name, 104 | label: entry.content.label, 105 | })); 106 | }; 107 | 108 | async function validateAuth({ url, user, password }) { 109 | try { 110 | await splunkd('GET', '/services/server/info', { url, username: user, password }); 111 | return true; 112 | } catch (e) { 113 | return false; 114 | } 115 | } 116 | 117 | module.exports = { 118 | splunkd, 119 | loadDashboard, 120 | listDashboards, 121 | validateAuth, 122 | qs, 123 | }; 124 | -------------------------------------------------------------------------------- /cli/vercel.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const prompts = require('./prompts'); 18 | const { exec, Secret } = require('./exec'); 19 | const fs = require('fs-extra'); 20 | const path = require('path'); 21 | const chalk = require('chalk'); 22 | const { cli } = require('cli-ux'); 23 | 24 | const postInitInstructions = ({ folderName }) => chalk` 25 | 26 | {green Project successfully generated in {bold ./${folderName}}} 27 | 28 | {gray Next steps:} 29 | 30 | {yellow $} cd ./${folderName} 31 | 32 | {gray {bold.blue 1)} Setup the project with Vercel} 33 | 34 | {yellow $} vercel 35 | 36 | {gray Follow the steps to set up the project} 37 | 38 | {gray {bold.blue 2)} Run locally} 39 | 40 | {yellow $} vercel dev --listen 3333 41 | 42 | {gray Open a browser at http://localhost:3333} 43 | 44 | {gray {bold.blue 3)} Deploy to Vercel:} 45 | 46 | {yellow $} vercel --prod 47 | 48 | {gray {bold.blue 4)} [optional] Push to a Github repository and set up the Vercel Github Integration 49 | 50 | https://vercel.com/docs/git-integrations/vercel-for-github} 51 | `; 52 | 53 | async function initVercelProject({ folderName, destFolder, splunkdUrl, splunkdUser, splunkdPassword = process.env.SPLUNKD_PASSWORD }) { 54 | const nowSplunkdPasswordSecret = `dashpub-${folderName}-splunkd-password`; 55 | 56 | cli.action.start('Creating vercel.json'); 57 | await fs.writeFile( 58 | path.join(destFolder, 'vercel.json'), 59 | JSON.stringify( 60 | { 61 | version: 2, 62 | env: { 63 | SPLUNKD_URL: splunkdUrl, 64 | SPLUNKD_USER: splunkdUser, 65 | SPLUNKD_PASSWORD: `@${nowSplunkdPasswordSecret}`, 66 | }, 67 | }, 68 | null, 69 | 2 70 | ) 71 | ); 72 | cli.action.stop(); 73 | 74 | await exec('vercel', ['secret', 'add', nowSplunkdPasswordSecret, new Secret(splunkdPassword)]); 75 | await exec('git', ['add', '.'], { cwd: destFolder }); 76 | await exec('git', ['commit', '-m', 'initialized vercel project'], { cwd: destFolder }); 77 | 78 | console.log(postInitInstructions({ folderName })); 79 | } 80 | 81 | module.exports = { 82 | initVercelProject, 83 | }; 84 | -------------------------------------------------------------------------------- /license_report.txt: -------------------------------------------------------------------------------- 1 | ├─ (BSD-2-Clause OR MIT OR Apache-2.0) 2 | │ └─ rc@1.2.8 3 | │ ├─ URL: https://github.com/dominictarr/rc.git 4 | │ ├─ VendorName: Dominic Tarr 5 | │ └─ VendorUrl: dominictarr.com 6 | ├─ (MIT OR CC0-1.0) 7 | │ └─ type-fest@0.8.1 8 | │ ├─ URL: https://github.com/sindresorhus/type-fest.git 9 | │ ├─ VendorName: Sindre Sorhus 10 | │ └─ VendorUrl: sindresorhus.com 11 | ├─ (MIT OR WTFPL) 12 | │ └─ expand-template@2.0.3 13 | │ ├─ URL: https://github.com/ralphtheninja/expand-template.git 14 | │ ├─ VendorName: LM 15 | │ └─ VendorUrl: https://github.com/ralphtheninja/expand-template 16 | ├─ Apache-2.0 17 | │ ├─ detect-libc@1.0.3 18 | │ │ ├─ URL: git://github.com/lovell/detect-libc 19 | │ │ └─ VendorName: Lovell Fuller 20 | │ ├─ human-signals@1.1.1 21 | │ │ ├─ URL: https://github.com/ehmicky/human-signals.git 22 | │ │ ├─ VendorName: ehmicky 23 | │ │ └─ VendorUrl: https://git.io/JeluP 24 | │ ├─ rxjs@5.5.12 25 | │ │ ├─ URL: git@github.com:ReactiveX/RxJS.git 26 | │ │ ├─ VendorName: Ben Lesh 27 | │ │ └─ VendorUrl: https://github.com/ReactiveX/RxJS 28 | │ ├─ rxjs@6.5.4 29 | │ │ ├─ URL: https://github.com/reactivex/rxjs.git 30 | │ │ ├─ VendorName: Ben Lesh 31 | │ │ └─ VendorUrl: https://github.com/ReactiveX/RxJS 32 | │ ├─ sharp@0.25.2 33 | │ │ ├─ URL: git://github.com/lovell/sharp 34 | │ │ ├─ VendorName: Lovell Fuller 35 | │ │ └─ VendorUrl: https://github.com/lovell/sharp 36 | │ ├─ tslib@1.10.0 37 | │ │ ├─ URL: https://github.com/Microsoft/tslib.git 38 | │ │ ├─ VendorName: Microsoft Corp. 39 | │ │ └─ VendorUrl: http://typescriptlang.org/ 40 | │ └─ tunnel-agent@0.6.0 41 | │ ├─ URL: https://github.com/mikeal/tunnel-agent 42 | │ ├─ VendorName: Mikeal Rogers 43 | │ └─ VendorUrl: http://www.futurealoof.com 44 | ├─ BSD-2-Clause 45 | │ ├─ dotenv@8.2.0 46 | │ │ └─ URL: git://github.com/motdotla/dotenv.git 47 | │ └─ esprima@4.0.1 48 | │ ├─ URL: https://github.com/jquery/esprima.git 49 | │ ├─ VendorName: Ariya Hidayat 50 | │ └─ VendorUrl: http://esprima.org/ 51 | ├─ BSD-3-Clause 52 | │ └─ sprintf-js@1.0.3 53 | │ ├─ URL: https://github.com/alexei/sprintf.js.git 54 | │ ├─ VendorName: Alexandru Marasteanu 55 | │ └─ VendorUrl: http://alexei.ro/ 56 | ├─ ISC 57 | │ ├─ @oclif/linewrap@1.0.0 58 | │ │ ├─ URL: git+https://github.com/heroku/node-linewrap.git 59 | │ │ ├─ VendorName: Jeff Dickey 60 | │ │ └─ VendorUrl: https://github.com/heroku/node-linewrap#readme 61 | │ ├─ aproba@1.2.0 62 | │ │ ├─ URL: https://github.com/iarna/aproba 63 | │ │ ├─ VendorName: Rebecca Turner 64 | │ │ └─ VendorUrl: https://github.com/iarna/aproba 65 | │ ├─ are-we-there-yet@1.1.5 66 | │ │ ├─ URL: https://github.com/iarna/are-we-there-yet.git 67 | │ │ ├─ VendorName: Rebecca Turner 68 | │ │ └─ VendorUrl: https://github.com/iarna/are-we-there-yet 69 | │ ├─ at-least-node@1.0.0 70 | │ │ ├─ URL: git+https://github.com/RyanZim/at-least-node.git 71 | │ │ ├─ VendorName: Ryan Zimmerman 72 | │ │ └─ VendorUrl: https://github.com/RyanZim/at-least-node#readme 73 | │ ├─ chownr@1.1.3 74 | │ │ ├─ URL: git://github.com/isaacs/chownr.git 75 | │ │ ├─ VendorName: Isaac Z. Schlueter 76 | │ │ └─ VendorUrl: http://blog.izs.me/ 77 | │ ├─ cli-width@2.2.0 78 | │ │ ├─ URL: git@github.com:knownasilya/cli-width.git 79 | │ │ ├─ VendorName: Ilya Radchenko 80 | │ │ └─ VendorUrl: https://github.com/knownasilya/cli-width 81 | │ ├─ console-control-strings@1.1.0 82 | │ │ ├─ URL: https://github.com/iarna/console-control-strings 83 | │ │ ├─ VendorName: Rebecca Turner 84 | │ │ └─ VendorUrl: http://re-becca.org/ 85 | │ ├─ fs-minipass@2.0.0 86 | │ │ ├─ URL: git+https://github.com/npm/fs-minipass.git 87 | │ │ ├─ VendorName: Isaac Z. Schlueter 88 | │ │ └─ VendorUrl: https://github.com/npm/fs-minipass#readme 89 | │ ├─ gauge@2.7.4 90 | │ │ ├─ URL: https://github.com/iarna/gauge 91 | │ │ ├─ VendorName: Rebecca Turner 92 | │ │ └─ VendorUrl: https://github.com/iarna/gauge 93 | │ ├─ graceful-fs@4.2.3 94 | │ │ └─ URL: https://github.com/isaacs/node-graceful-fs 95 | │ ├─ has-unicode@2.0.1 96 | │ │ ├─ URL: https://github.com/iarna/has-unicode 97 | │ │ ├─ VendorName: Rebecca Turner 98 | │ │ └─ VendorUrl: https://github.com/iarna/has-unicode 99 | │ ├─ inherits@2.0.4 100 | │ │ └─ URL: git://github.com/isaacs/inherits 101 | │ ├─ ini@1.3.5 102 | │ │ ├─ URL: git://github.com/isaacs/ini.git 103 | │ │ ├─ VendorName: Isaac Z. Schlueter 104 | │ │ └─ VendorUrl: http://blog.izs.me/ 105 | │ ├─ isexe@2.0.0 106 | │ │ ├─ URL: git+https://github.com/isaacs/isexe.git 107 | │ │ ├─ VendorName: Isaac Z. Schlueter 108 | │ │ └─ VendorUrl: https://github.com/isaacs/isexe#readme 109 | │ ├─ minipass@3.1.1 110 | │ │ ├─ URL: git+https://github.com/isaacs/minipass.git 111 | │ │ ├─ VendorName: Isaac Z. Schlueter 112 | │ │ └─ VendorUrl: http://blog.izs.me/ 113 | │ ├─ mute-stream@0.0.7 114 | │ │ ├─ URL: git://github.com/isaacs/mute-stream 115 | │ │ ├─ VendorName: Isaac Z. Schlueter 116 | │ │ └─ VendorUrl: http://blog.izs.me/ 117 | │ ├─ mute-stream@0.0.8 118 | │ │ ├─ URL: git://github.com/isaacs/mute-stream 119 | │ │ ├─ VendorName: Isaac Z. Schlueter 120 | │ │ └─ VendorUrl: http://blog.izs.me/ 121 | │ ├─ npmlog@4.1.2 122 | │ │ ├─ URL: https://github.com/npm/npmlog.git 123 | │ │ ├─ VendorName: Isaac Z. Schlueter 124 | │ │ └─ VendorUrl: http://blog.izs.me/ 125 | │ ├─ once@1.4.0 126 | │ │ ├─ URL: git://github.com/isaacs/once 127 | │ │ ├─ VendorName: Isaac Z. Schlueter 128 | │ │ └─ VendorUrl: http://blog.izs.me/ 129 | │ ├─ sax@1.2.4 130 | │ │ ├─ URL: git://github.com/isaacs/sax-js.git 131 | │ │ ├─ VendorName: Isaac Z. Schlueter 132 | │ │ └─ VendorUrl: http://blog.izs.me/ 133 | │ ├─ semver@5.7.1 134 | │ │ └─ URL: https://github.com/npm/node-semver 135 | │ ├─ semver@7.2.1 136 | │ │ └─ URL: https://github.com/npm/node-semver 137 | │ ├─ set-blocking@2.0.0 138 | │ │ ├─ URL: git+https://github.com/yargs/set-blocking.git 139 | │ │ ├─ VendorName: Ben Coe 140 | │ │ └─ VendorUrl: https://github.com/yargs/set-blocking#readme 141 | │ ├─ signal-exit@3.0.2 142 | │ │ ├─ URL: https://github.com/tapjs/signal-exit.git 143 | │ │ ├─ VendorName: Ben Coe 144 | │ │ └─ VendorUrl: https://github.com/tapjs/signal-exit 145 | │ ├─ tar@6.0.1 146 | │ │ ├─ URL: https://github.com/npm/node-tar.git 147 | │ │ ├─ VendorName: Isaac Z. Schlueter 148 | │ │ └─ VendorUrl: http://blog.izs.me/ 149 | │ ├─ which@1.3.1 150 | │ │ ├─ URL: git://github.com/isaacs/node-which.git 151 | │ │ ├─ VendorName: Isaac Z. Schlueter 152 | │ │ └─ VendorUrl: http://blog.izs.me 153 | │ ├─ which@2.0.2 154 | │ │ ├─ URL: git://github.com/isaacs/node-which.git 155 | │ │ ├─ VendorName: Isaac Z. Schlueter 156 | │ │ └─ VendorUrl: http://blog.izs.me 157 | │ ├─ wide-align@1.1.3 158 | │ │ ├─ URL: https://github.com/iarna/wide-align 159 | │ │ ├─ VendorName: Rebecca Turner 160 | │ │ └─ VendorUrl: http://re-becca.org/ 161 | │ ├─ wrappy@1.0.2 162 | │ │ ├─ URL: https://github.com/npm/wrappy 163 | │ │ ├─ VendorName: Isaac Z. Schlueter 164 | │ │ └─ VendorUrl: https://github.com/npm/wrappy 165 | │ └─ yallist@4.0.0 166 | │ ├─ URL: git+https://github.com/isaacs/yallist.git 167 | │ ├─ VendorName: Isaac Z. Schlueter 168 | │ └─ VendorUrl: http://blog.izs.me/ 169 | ├─ MIT 170 | │ ├─ @oclif/command@1.5.19 171 | │ │ ├─ URL: https://github.com/oclif/command.git 172 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 173 | │ │ └─ VendorUrl: https://github.com/oclif/command 174 | │ ├─ @oclif/config@1.13.3 175 | │ │ ├─ URL: https://github.com/oclif/config.git 176 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 177 | │ │ └─ VendorUrl: https://github.com/oclif/config 178 | │ ├─ @oclif/errors@1.2.2 179 | │ │ ├─ URL: https://github.com/oclif/errors.git 180 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 181 | │ │ └─ VendorUrl: https://github.com/oclif/errors 182 | │ ├─ @oclif/parser@3.8.4 183 | │ │ ├─ URL: https://github.com/oclif/parser.git 184 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 185 | │ │ └─ VendorUrl: https://github.com/oclif/parser 186 | │ ├─ @oclif/plugin-help@2.2.3 187 | │ │ ├─ URL: https://github.com/oclif/plugin-help.git 188 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 189 | │ │ └─ VendorUrl: https://github.com/oclif/plugin-help 190 | │ ├─ @oclif/screen@1.0.4 191 | │ │ ├─ URL: https://github.com/oclif/screen.git 192 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 193 | │ │ └─ VendorUrl: https://github.com/oclif/screen 194 | │ ├─ @types/color-name@1.1.1 195 | │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git 196 | │ ├─ ansi-escapes@3.2.0 197 | │ │ ├─ URL: https://github.com/sindresorhus/ansi-escapes.git 198 | │ │ ├─ VendorName: Sindre Sorhus 199 | │ │ └─ VendorUrl: sindresorhus.com 200 | │ ├─ ansi-escapes@4.3.0 201 | │ │ ├─ URL: https://github.com/sindresorhus/ansi-escapes.git 202 | │ │ ├─ VendorName: Sindre Sorhus 203 | │ │ └─ VendorUrl: sindresorhus.com 204 | │ ├─ ansi-regex@2.1.1 205 | │ │ ├─ URL: https://github.com/chalk/ansi-regex.git 206 | │ │ ├─ VendorName: Sindre Sorhus 207 | │ │ └─ VendorUrl: sindresorhus.com 208 | │ ├─ ansi-regex@3.0.0 209 | │ │ ├─ URL: https://github.com/chalk/ansi-regex.git 210 | │ │ ├─ VendorName: Sindre Sorhus 211 | │ │ └─ VendorUrl: sindresorhus.com 212 | │ ├─ ansi-regex@4.1.0 213 | │ │ ├─ URL: https://github.com/chalk/ansi-regex.git 214 | │ │ ├─ VendorName: Sindre Sorhus 215 | │ │ └─ VendorUrl: sindresorhus.com 216 | │ ├─ ansi-regex@5.0.0 217 | │ │ ├─ URL: https://github.com/chalk/ansi-regex.git 218 | │ │ ├─ VendorName: Sindre Sorhus 219 | │ │ └─ VendorUrl: sindresorhus.com 220 | │ ├─ ansi-styles@3.2.1 221 | │ │ ├─ URL: https://github.com/chalk/ansi-styles.git 222 | │ │ ├─ VendorName: Sindre Sorhus 223 | │ │ └─ VendorUrl: sindresorhus.com 224 | │ ├─ ansi-styles@4.2.1 225 | │ │ ├─ URL: https://github.com/chalk/ansi-styles.git 226 | │ │ ├─ VendorName: Sindre Sorhus 227 | │ │ └─ VendorUrl: sindresorhus.com 228 | │ ├─ ansicolors@0.3.2 229 | │ │ ├─ URL: git://github.com/thlorenz/ansicolors.git 230 | │ │ ├─ VendorName: Thorsten Lorenz 231 | │ │ └─ VendorUrl: thlorenz.com 232 | │ ├─ argparse@1.0.10 233 | │ │ └─ URL: https://github.com/nodeca/argparse.git 234 | │ ├─ bl@3.0.1 235 | │ │ ├─ URL: https://github.com/rvagg/bl.git 236 | │ │ └─ VendorUrl: https://github.com/rvagg/bl 237 | │ ├─ cardinal@2.1.1 238 | │ │ ├─ URL: git://github.com/thlorenz/cardinal.git 239 | │ │ ├─ VendorName: Thorsten Lorenz 240 | │ │ └─ VendorUrl: thlorenz.com 241 | │ ├─ chalk@2.4.2 242 | │ │ └─ URL: https://github.com/chalk/chalk.git 243 | │ ├─ chalk@3.0.0 244 | │ │ └─ URL: https://github.com/chalk/chalk.git 245 | │ ├─ chardet@0.4.2 246 | │ │ ├─ URL: git@github.com:runk/node-chardet.git 247 | │ │ ├─ VendorName: Dmitry Shirokov 248 | │ │ └─ VendorUrl: https://github.com/runk/node-chardet 249 | │ ├─ chardet@0.7.0 250 | │ │ ├─ URL: git@github.com:runk/node-chardet.git 251 | │ │ ├─ VendorName: Dmitry Shirokov 252 | │ │ └─ VendorUrl: https://github.com/runk/node-chardet 253 | │ ├─ clean-stack@1.3.0 254 | │ │ ├─ URL: https://github.com/sindresorhus/clean-stack.git 255 | │ │ ├─ VendorName: Sindre Sorhus 256 | │ │ └─ VendorUrl: sindresorhus.com 257 | │ ├─ clean-stack@2.2.0 258 | │ │ ├─ URL: https://github.com/sindresorhus/clean-stack.git 259 | │ │ ├─ VendorName: Sindre Sorhus 260 | │ │ └─ VendorUrl: sindresorhus.com 261 | │ ├─ cli-cursor@2.1.0 262 | │ │ ├─ URL: https://github.com/sindresorhus/cli-cursor.git 263 | │ │ ├─ VendorName: Sindre Sorhus 264 | │ │ └─ VendorUrl: sindresorhus.com 265 | │ ├─ cli-cursor@3.1.0 266 | │ │ ├─ URL: https://github.com/sindresorhus/cli-cursor.git 267 | │ │ ├─ VendorName: Sindre Sorhus 268 | │ │ └─ VendorUrl: sindresorhus.com 269 | │ ├─ cli-progress@3.5.0 270 | │ │ ├─ URL: https://github.com/AndiDittrich/Node.CLI-Progress.git 271 | │ │ ├─ VendorName: Andi Dittrich 272 | │ │ └─ VendorUrl: https://github.com/AndiDittrich/Node.CLI-Progress 273 | │ ├─ cli-ux@5.4.5 274 | │ │ ├─ URL: https://github.com/oclif/cli-ux.git 275 | │ │ ├─ VendorName: Jeff Dickey @jdxcode 276 | │ │ └─ VendorUrl: https://github.com/oclif/cli-ux 277 | │ ├─ code-point-at@1.1.0 278 | │ │ ├─ URL: https://github.com/sindresorhus/code-point-at.git 279 | │ │ ├─ VendorName: Sindre Sorhus 280 | │ │ └─ VendorUrl: sindresorhus.com 281 | │ ├─ color-convert@1.9.3 282 | │ │ ├─ URL: https://github.com/Qix-/color-convert.git 283 | │ │ └─ VendorName: Heather Arthur 284 | │ ├─ color-convert@2.0.1 285 | │ │ ├─ URL: https://github.com/Qix-/color-convert.git 286 | │ │ └─ VendorName: Heather Arthur 287 | │ ├─ color-name@1.1.3 288 | │ │ ├─ URL: git@github.com:dfcreative/color-name.git 289 | │ │ ├─ VendorName: DY 290 | │ │ └─ VendorUrl: https://github.com/dfcreative/color-name 291 | │ ├─ color-name@1.1.4 292 | │ │ ├─ URL: git@github.com:colorjs/color-name.git 293 | │ │ ├─ VendorName: DY 294 | │ │ └─ VendorUrl: https://github.com/colorjs/color-name 295 | │ ├─ color-string@1.5.3 296 | │ │ ├─ URL: https://github.com/Qix-/color-string.git 297 | │ │ └─ VendorName: Heather Arthur 298 | │ ├─ color@3.1.2 299 | │ │ └─ URL: https://github.com/Qix-/color.git 300 | │ ├─ colors@1.4.0 301 | │ │ ├─ URL: http://github.com/Marak/colors.js.git 302 | │ │ ├─ VendorName: Marak Squires 303 | │ │ └─ VendorUrl: https://github.com/Marak/colors.js 304 | │ ├─ core-util-is@1.0.2 305 | │ │ ├─ URL: git://github.com/isaacs/core-util-is 306 | │ │ ├─ VendorName: Isaac Z. Schlueter 307 | │ │ └─ VendorUrl: http://blog.izs.me/ 308 | │ ├─ cross-spawn@6.0.5 309 | │ │ ├─ URL: git@github.com:moxystudio/node-cross-spawn.git 310 | │ │ ├─ VendorName: André Cruz 311 | │ │ └─ VendorUrl: https://github.com/moxystudio/node-cross-spawn 312 | │ ├─ cross-spawn@7.0.1 313 | │ │ ├─ URL: git@github.com:moxystudio/node-cross-spawn.git 314 | │ │ ├─ VendorName: André Cruz 315 | │ │ └─ VendorUrl: https://github.com/moxystudio/node-cross-spawn 316 | │ ├─ debug@4.1.1 317 | │ │ ├─ URL: git://github.com/visionmedia/debug.git 318 | │ │ └─ VendorName: TJ Holowaychuk 319 | │ ├─ decompress-response@4.2.1 320 | │ │ ├─ URL: https://github.com/sindresorhus/decompress-response.git 321 | │ │ ├─ VendorName: Sindre Sorhus 322 | │ │ └─ VendorUrl: sindresorhus.com 323 | │ ├─ deep-extend@0.6.0 324 | │ │ ├─ URL: git://github.com/unclechu/node-deep-extend.git 325 | │ │ ├─ VendorName: Viacheslav Lotsmanov 326 | │ │ └─ VendorUrl: https://github.com/unclechu/node-deep-extend 327 | │ ├─ delegates@1.0.0 328 | │ │ └─ URL: https://github.com/visionmedia/node-delegates.git 329 | │ ├─ emoji-regex@7.0.3 330 | │ │ ├─ URL: https://github.com/mathiasbynens/emoji-regex.git 331 | │ │ ├─ VendorName: Mathias Bynens 332 | │ │ └─ VendorUrl: https://mths.be/emoji-regex 333 | │ ├─ emoji-regex@8.0.0 334 | │ │ ├─ URL: https://github.com/mathiasbynens/emoji-regex.git 335 | │ │ ├─ VendorName: Mathias Bynens 336 | │ │ └─ VendorUrl: https://mths.be/emoji-regex 337 | │ ├─ end-of-stream@1.4.4 338 | │ │ ├─ URL: git://github.com/mafintosh/end-of-stream.git 339 | │ │ ├─ VendorName: Mathias Buus 340 | │ │ └─ VendorUrl: https://github.com/mafintosh/end-of-stream 341 | │ ├─ escape-string-regexp@1.0.5 342 | │ │ ├─ URL: https://github.com/sindresorhus/escape-string-regexp.git 343 | │ │ ├─ VendorName: Sindre Sorhus 344 | │ │ └─ VendorUrl: sindresorhus.com 345 | │ ├─ execa@4.0.0 346 | │ │ ├─ URL: https://github.com/sindresorhus/execa.git 347 | │ │ ├─ VendorName: Sindre Sorhus 348 | │ │ └─ VendorUrl: sindresorhus.com 349 | │ ├─ external-editor@2.2.0 350 | │ │ ├─ URL: git+https://github.com/mrkmg/node-external-editor.git 351 | │ │ ├─ VendorName: Kevin Gravier 352 | │ │ └─ VendorUrl: https://github.com/mrkmg/node-external-editor#readme 353 | │ ├─ external-editor@3.1.0 354 | │ │ ├─ URL: git+https://github.com/mrkmg/node-external-editor.git 355 | │ │ ├─ VendorName: Kevin Gravier 356 | │ │ └─ VendorUrl: https://github.com/mrkmg/node-external-editor#readme 357 | │ ├─ extract-stack@1.0.0 358 | │ │ ├─ URL: https://github.com/sindresorhus/extract-stack.git 359 | │ │ ├─ VendorName: Sindre Sorhus 360 | │ │ └─ VendorUrl: sindresorhus.com 361 | │ ├─ figures@2.0.0 362 | │ │ ├─ URL: https://github.com/sindresorhus/figures.git 363 | │ │ ├─ VendorName: Sindre Sorhus 364 | │ │ └─ VendorUrl: sindresorhus.com 365 | │ ├─ figures@3.1.0 366 | │ │ ├─ URL: https://github.com/sindresorhus/figures.git 367 | │ │ ├─ VendorName: Sindre Sorhus 368 | │ │ └─ VendorUrl: sindresorhus.com 369 | │ ├─ fs-constants@1.0.0 370 | │ │ ├─ URL: https://github.com/mafintosh/fs-constants.git 371 | │ │ ├─ VendorName: Mathias Buus 372 | │ │ └─ VendorUrl: https://github.com/mafintosh/fs-constants 373 | │ ├─ fs-extra@7.0.1 374 | │ │ ├─ URL: https://github.com/jprichardson/node-fs-extra 375 | │ │ ├─ VendorName: JP Richardson 376 | │ │ └─ VendorUrl: https://github.com/jprichardson/node-fs-extra 377 | │ ├─ fs-extra@9.0.0 378 | │ │ ├─ URL: https://github.com/jprichardson/node-fs-extra 379 | │ │ ├─ VendorName: JP Richardson 380 | │ │ └─ VendorUrl: https://github.com/jprichardson/node-fs-extra 381 | │ ├─ get-stream@5.1.0 382 | │ │ ├─ URL: https://github.com/sindresorhus/get-stream.git 383 | │ │ ├─ VendorName: Sindre Sorhus 384 | │ │ └─ VendorUrl: sindresorhus.com 385 | │ ├─ github-from-package@0.0.0 386 | │ │ ├─ URL: git://github.com/substack/github-from-package.git 387 | │ │ ├─ VendorName: James Halliday 388 | │ │ └─ VendorUrl: https://github.com/substack/github-from-package 389 | │ ├─ has-flag@2.0.0 390 | │ │ ├─ URL: https://github.com/sindresorhus/has-flag.git 391 | │ │ ├─ VendorName: Sindre Sorhus 392 | │ │ └─ VendorUrl: sindresorhus.com 393 | │ ├─ has-flag@3.0.0 394 | │ │ ├─ URL: https://github.com/sindresorhus/has-flag.git 395 | │ │ ├─ VendorName: Sindre Sorhus 396 | │ │ └─ VendorUrl: sindresorhus.com 397 | │ ├─ has-flag@4.0.0 398 | │ │ ├─ URL: https://github.com/sindresorhus/has-flag.git 399 | │ │ ├─ VendorName: Sindre Sorhus 400 | │ │ └─ VendorUrl: sindresorhus.com 401 | │ ├─ hyperlinker@1.0.0 402 | │ │ ├─ URL: https://github.com/jamestalmage/hyperlinker.git 403 | │ │ ├─ VendorName: James Talmage 404 | │ │ └─ VendorUrl: github.com/jamestalmage 405 | │ ├─ iconv-lite@0.4.24 406 | │ │ ├─ URL: git://github.com/ashtuchkin/iconv-lite.git 407 | │ │ ├─ VendorName: Alexander Shtuchkin 408 | │ │ └─ VendorUrl: https://github.com/ashtuchkin/iconv-lite 409 | │ ├─ indent-string@3.2.0 410 | │ │ ├─ URL: https://github.com/sindresorhus/indent-string.git 411 | │ │ ├─ VendorName: Sindre Sorhus 412 | │ │ └─ VendorUrl: sindresorhus.com 413 | │ ├─ indent-string@4.0.0 414 | │ │ ├─ URL: https://github.com/sindresorhus/indent-string.git 415 | │ │ ├─ VendorName: Sindre Sorhus 416 | │ │ └─ VendorUrl: sindresorhus.com 417 | │ ├─ inquirer-checkbox-plus-prompt@1.0.1 418 | │ │ ├─ URL: https://github.com/faressoft/inquirer-checkbox-plus-prompt.git 419 | │ │ └─ VendorName: Mohammad Fares 420 | │ ├─ inquirer@5.2.0 421 | │ │ ├─ URL: https://github.com/SBoudrias/Inquirer.js.git 422 | │ │ └─ VendorName: Simon Boudrias 423 | │ ├─ inquirer@7.1.0 424 | │ │ ├─ URL: https://github.com/SBoudrias/Inquirer.js.git 425 | │ │ └─ VendorName: Simon Boudrias 426 | │ ├─ is-arrayish@0.3.2 427 | │ │ ├─ URL: https://github.com/qix-/node-is-arrayish.git 428 | │ │ ├─ VendorName: Qix 429 | │ │ └─ VendorUrl: http://github.com/qix- 430 | │ ├─ is-fullwidth-code-point@1.0.0 431 | │ │ ├─ URL: https://github.com/sindresorhus/is-fullwidth-code-point.git 432 | │ │ ├─ VendorName: Sindre Sorhus 433 | │ │ └─ VendorUrl: sindresorhus.com 434 | │ ├─ is-fullwidth-code-point@2.0.0 435 | │ │ ├─ URL: https://github.com/sindresorhus/is-fullwidth-code-point.git 436 | │ │ ├─ VendorName: Sindre Sorhus 437 | │ │ └─ VendorUrl: sindresorhus.com 438 | │ ├─ is-fullwidth-code-point@3.0.0 439 | │ │ ├─ URL: https://github.com/sindresorhus/is-fullwidth-code-point.git 440 | │ │ ├─ VendorName: Sindre Sorhus 441 | │ │ └─ VendorUrl: sindresorhus.com 442 | │ ├─ is-promise@2.1.0 443 | │ │ ├─ URL: https://github.com/then/is-promise.git 444 | │ │ └─ VendorName: ForbesLindesay 445 | │ ├─ is-stream@2.0.0 446 | │ │ ├─ URL: https://github.com/sindresorhus/is-stream.git 447 | │ │ ├─ VendorName: Sindre Sorhus 448 | │ │ └─ VendorUrl: sindresorhus.com 449 | │ ├─ is-wsl@1.1.0 450 | │ │ ├─ URL: https://github.com/sindresorhus/is-wsl.git 451 | │ │ ├─ VendorName: Sindre Sorhus 452 | │ │ └─ VendorUrl: sindresorhus.com 453 | │ ├─ isarray@1.0.0 454 | │ │ ├─ URL: git://github.com/juliangruber/isarray.git 455 | │ │ ├─ VendorName: Julian Gruber 456 | │ │ └─ VendorUrl: https://github.com/juliangruber/isarray 457 | │ ├─ js-yaml@3.13.1 458 | │ │ ├─ URL: https://github.com/nodeca/js-yaml.git 459 | │ │ ├─ VendorName: Vladimir Zapparov 460 | │ │ └─ VendorUrl: https://github.com/nodeca/js-yaml 461 | │ ├─ jsonfile@4.0.0 462 | │ │ ├─ URL: git@github.com:jprichardson/node-jsonfile.git 463 | │ │ └─ VendorName: JP Richardson 464 | │ ├─ jsonfile@6.0.1 465 | │ │ ├─ URL: git@github.com:jprichardson/node-jsonfile.git 466 | │ │ └─ VendorName: JP Richardson 467 | │ ├─ lodash._reinterpolate@3.0.0 468 | │ │ ├─ URL: https://github.com/lodash/lodash.git 469 | │ │ ├─ VendorName: John-David Dalton 470 | │ │ └─ VendorUrl: https://lodash.com/ 471 | │ ├─ lodash.template@4.5.0 472 | │ │ ├─ URL: https://github.com/lodash/lodash.git 473 | │ │ ├─ VendorName: John-David Dalton 474 | │ │ └─ VendorUrl: https://lodash.com/ 475 | │ ├─ lodash.templatesettings@4.2.0 476 | │ │ ├─ URL: https://github.com/lodash/lodash.git 477 | │ │ ├─ VendorName: John-David Dalton 478 | │ │ └─ VendorUrl: https://lodash.com/ 479 | │ ├─ lodash@4.17.20 480 | │ │ ├─ URL: https://github.com/lodash/lodash.git 481 | │ │ ├─ VendorName: John-David Dalton 482 | │ │ └─ VendorUrl: https://lodash.com/ 483 | │ ├─ merge-stream@2.0.0 484 | │ │ ├─ URL: https://github.com/grncdr/merge-stream.git 485 | │ │ └─ VendorName: Stephen Sugden 486 | │ ├─ mimic-fn@1.2.0 487 | │ │ ├─ URL: https://github.com/sindresorhus/mimic-fn.git 488 | │ │ ├─ VendorName: Sindre Sorhus 489 | │ │ └─ VendorUrl: sindresorhus.com 490 | │ ├─ mimic-fn@2.1.0 491 | │ │ ├─ URL: https://github.com/sindresorhus/mimic-fn.git 492 | │ │ ├─ VendorName: Sindre Sorhus 493 | │ │ └─ VendorUrl: sindresorhus.com 494 | │ ├─ mimic-response@2.0.0 495 | │ │ ├─ URL: https://github.com/sindresorhus/mimic-response.git 496 | │ │ ├─ VendorName: Sindre Sorhus 497 | │ │ └─ VendorUrl: sindresorhus.com 498 | │ ├─ minimist@0.0.8 499 | │ │ ├─ URL: git://github.com/substack/minimist.git 500 | │ │ ├─ VendorName: James Halliday 501 | │ │ └─ VendorUrl: https://github.com/substack/minimist 502 | │ ├─ minimist@1.2.0 503 | │ │ ├─ URL: git://github.com/substack/minimist.git 504 | │ │ ├─ VendorName: James Halliday 505 | │ │ └─ VendorUrl: https://github.com/substack/minimist 506 | │ ├─ minizlib@2.1.0 507 | │ │ ├─ URL: git+https://github.com/isaacs/minizlib.git 508 | │ │ ├─ VendorName: Isaac Z. Schlueter 509 | │ │ └─ VendorUrl: http://blog.izs.me/ 510 | │ ├─ mkdirp@0.5.1 511 | │ │ ├─ URL: https://github.com/substack/node-mkdirp.git 512 | │ │ ├─ VendorName: James Halliday 513 | │ │ └─ VendorUrl: http://substack.net 514 | │ ├─ mkdirp@1.0.4 515 | │ │ └─ URL: https://github.com/isaacs/node-mkdirp.git 516 | │ ├─ ms@2.1.2 517 | │ │ └─ URL: https://github.com/zeit/ms.git 518 | │ ├─ napi-build-utils@1.0.1 519 | │ │ ├─ URL: git+https://github.com/inspiredware/napi-build-utils.git 520 | │ │ ├─ VendorName: Jim Schlight 521 | │ │ └─ VendorUrl: https://github.com/inspiredware/napi-build-utils#readme 522 | │ ├─ natural-orderby@2.0.3 523 | │ │ ├─ URL: https://github.com/yobacca/natural-orderby.git 524 | │ │ ├─ VendorName: Olaf Ennen 525 | │ │ └─ VendorUrl: https://yobacca.github.io/natural-orderby 526 | │ ├─ nice-try@1.0.5 527 | │ │ ├─ URL: https://github.com/electerious/nice-try.git 528 | │ │ └─ VendorUrl: https://github.com/electerious/nice-try 529 | │ ├─ node-abi@2.13.0 530 | │ │ ├─ URL: https://github.com/lgeiger/node-abi.git 531 | │ │ ├─ VendorName: Lukas Geiger 532 | │ │ └─ VendorUrl: https://github.com/lgeiger/node-abi#readme 533 | │ ├─ node-addon-api@2.0.0 534 | │ │ ├─ URL: git://github.com/nodejs/node-addon-api.git 535 | │ │ └─ VendorUrl: https://github.com/nodejs/node-addon-api 536 | │ ├─ node-fetch@2.6.1 537 | │ │ ├─ URL: https://github.com/bitinn/node-fetch.git 538 | │ │ ├─ VendorName: David Frank 539 | │ │ └─ VendorUrl: https://github.com/bitinn/node-fetch 540 | │ ├─ noop-logger@0.1.1 541 | │ │ └─ URL: git://github.com/segmentio/noop-logger.git 542 | │ ├─ npm-run-path@4.0.1 543 | │ │ ├─ URL: https://github.com/sindresorhus/npm-run-path.git 544 | │ │ ├─ VendorName: Sindre Sorhus 545 | │ │ └─ VendorUrl: sindresorhus.com 546 | │ ├─ number-is-nan@1.0.1 547 | │ │ ├─ URL: https://github.com/sindresorhus/number-is-nan.git 548 | │ │ ├─ VendorName: Sindre Sorhus 549 | │ │ └─ VendorUrl: sindresorhus.com 550 | │ ├─ object-assign@4.1.1 551 | │ │ ├─ URL: https://github.com/sindresorhus/object-assign.git 552 | │ │ ├─ VendorName: Sindre Sorhus 553 | │ │ └─ VendorUrl: sindresorhus.com 554 | │ ├─ onetime@2.0.1 555 | │ │ ├─ URL: https://github.com/sindresorhus/onetime.git 556 | │ │ ├─ VendorName: Sindre Sorhus 557 | │ │ └─ VendorUrl: sindresorhus.com 558 | │ ├─ onetime@5.1.0 559 | │ │ ├─ URL: https://github.com/sindresorhus/onetime.git 560 | │ │ ├─ VendorName: Sindre Sorhus 561 | │ │ └─ VendorUrl: sindresorhus.com 562 | │ ├─ os-tmpdir@1.0.2 563 | │ │ ├─ URL: https://github.com/sindresorhus/os-tmpdir.git 564 | │ │ ├─ VendorName: Sindre Sorhus 565 | │ │ └─ VendorUrl: sindresorhus.com 566 | │ ├─ path-key@2.0.1 567 | │ │ ├─ URL: https://github.com/sindresorhus/path-key.git 568 | │ │ ├─ VendorName: Sindre Sorhus 569 | │ │ └─ VendorUrl: sindresorhus.com 570 | │ ├─ path-key@3.1.1 571 | │ │ ├─ URL: https://github.com/sindresorhus/path-key.git 572 | │ │ ├─ VendorName: Sindre Sorhus 573 | │ │ └─ VendorUrl: sindresorhus.com 574 | │ ├─ prebuild-install@5.3.3 575 | │ │ ├─ URL: https://github.com/prebuild/prebuild-install.git 576 | │ │ ├─ VendorName: Mathias Buus 577 | │ │ └─ VendorUrl: https://github.com/prebuild/prebuild-install 578 | │ ├─ prettier@1.19.1 579 | │ │ ├─ URL: https://github.com/prettier/prettier.git 580 | │ │ ├─ VendorName: James Long 581 | │ │ └─ VendorUrl: https://prettier.io/ 582 | │ ├─ process-nextick-args@2.0.1 583 | │ │ ├─ URL: https://github.com/calvinmetcalf/process-nextick-args.git 584 | │ │ └─ VendorUrl: https://github.com/calvinmetcalf/process-nextick-args 585 | │ ├─ pump@3.0.0 586 | │ │ ├─ URL: git://github.com/mafintosh/pump.git 587 | │ │ └─ VendorName: Mathias Buus Madsen 588 | │ ├─ querystring@0.2.0 589 | │ │ ├─ URL: git://github.com/Gozala/querystring.git 590 | │ │ └─ VendorName: Irakli Gozalishvili 591 | │ ├─ readable-stream@2.3.6 592 | │ │ └─ URL: git://github.com/nodejs/readable-stream 593 | │ ├─ readable-stream@3.6.0 594 | │ │ └─ URL: git://github.com/nodejs/readable-stream 595 | │ ├─ redeyed@2.1.1 596 | │ │ ├─ URL: git://github.com/thlorenz/redeyed.git 597 | │ │ ├─ VendorName: Thorsten Lorenz 598 | │ │ └─ VendorUrl: thlorenz.com 599 | │ ├─ restore-cursor@2.0.0 600 | │ │ ├─ URL: https://github.com/sindresorhus/restore-cursor.git 601 | │ │ ├─ VendorName: Sindre Sorhus 602 | │ │ └─ VendorUrl: sindresorhus.com 603 | │ ├─ restore-cursor@3.1.0 604 | │ │ ├─ URL: https://github.com/sindresorhus/restore-cursor.git 605 | │ │ ├─ VendorName: Sindre Sorhus 606 | │ │ └─ VendorUrl: sindresorhus.com 607 | │ ├─ run-async@2.3.0 608 | │ │ ├─ URL: https://github.com/SBoudrias/run-async.git 609 | │ │ └─ VendorName: Simon Boudrias 610 | │ ├─ run-async@2.4.0 611 | │ │ ├─ URL: https://github.com/SBoudrias/run-async.git 612 | │ │ └─ VendorName: Simon Boudrias 613 | │ ├─ safe-buffer@5.1.2 614 | │ │ ├─ URL: git://github.com/feross/safe-buffer.git 615 | │ │ ├─ VendorName: Feross Aboukhadijeh 616 | │ │ └─ VendorUrl: https://github.com/feross/safe-buffer 617 | │ ├─ safe-buffer@5.2.0 618 | │ │ ├─ URL: git://github.com/feross/safe-buffer.git 619 | │ │ ├─ VendorName: Feross Aboukhadijeh 620 | │ │ └─ VendorUrl: https://github.com/feross/safe-buffer 621 | │ ├─ safe-buffer@5.2.1 622 | │ │ ├─ URL: git://github.com/feross/safe-buffer.git 623 | │ │ ├─ VendorName: Feross Aboukhadijeh 624 | │ │ └─ VendorUrl: https://github.com/feross/safe-buffer 625 | │ ├─ safer-buffer@2.1.2 626 | │ │ ├─ URL: git+https://github.com/ChALkeR/safer-buffer.git 627 | │ │ ├─ VendorName: Nikita Skovoroda 628 | │ │ └─ VendorUrl: https://github.com/ChALkeR 629 | │ ├─ shebang-command@1.2.0 630 | │ │ ├─ URL: https://github.com/kevva/shebang-command.git 631 | │ │ ├─ VendorName: Kevin Martensson 632 | │ │ └─ VendorUrl: github.com/kevva 633 | │ ├─ shebang-command@2.0.0 634 | │ │ ├─ URL: https://github.com/kevva/shebang-command.git 635 | │ │ ├─ VendorName: Kevin Mårtensson 636 | │ │ └─ VendorUrl: github.com/kevva 637 | │ ├─ shebang-regex@1.0.0 638 | │ │ ├─ URL: https://github.com/sindresorhus/shebang-regex.git 639 | │ │ ├─ VendorName: Sindre Sorhus 640 | │ │ └─ VendorUrl: sindresorhus.com 641 | │ ├─ shebang-regex@3.0.0 642 | │ │ ├─ URL: https://github.com/sindresorhus/shebang-regex.git 643 | │ │ ├─ VendorName: Sindre Sorhus 644 | │ │ └─ VendorUrl: sindresorhus.com 645 | │ ├─ simple-concat@1.0.0 646 | │ │ ├─ URL: git://github.com/feross/simple-concat.git 647 | │ │ ├─ VendorName: Feross Aboukhadijeh 648 | │ │ └─ VendorUrl: https://github.com/feross/simple-concat 649 | │ ├─ simple-get@3.1.0 650 | │ │ ├─ URL: git://github.com/feross/simple-get.git 651 | │ │ ├─ VendorName: Feross Aboukhadijeh 652 | │ │ └─ VendorUrl: https://github.com/feross/simple-get 653 | │ ├─ simple-swizzle@0.2.2 654 | │ │ ├─ URL: https://github.com/qix-/node-simple-swizzle.git 655 | │ │ ├─ VendorName: Qix 656 | │ │ └─ VendorUrl: http://github.com/qix- 657 | │ ├─ string_decoder@1.1.1 658 | │ │ ├─ URL: git://github.com/nodejs/string_decoder.git 659 | │ │ └─ VendorUrl: https://github.com/nodejs/string_decoder 660 | │ ├─ string_decoder@1.3.0 661 | │ │ ├─ URL: git://github.com/nodejs/string_decoder.git 662 | │ │ └─ VendorUrl: https://github.com/nodejs/string_decoder 663 | │ ├─ string-width@1.0.2 664 | │ │ ├─ URL: https://github.com/sindresorhus/string-width.git 665 | │ │ ├─ VendorName: Sindre Sorhus 666 | │ │ └─ VendorUrl: sindresorhus.com 667 | │ ├─ string-width@2.1.1 668 | │ │ ├─ URL: https://github.com/sindresorhus/string-width.git 669 | │ │ ├─ VendorName: Sindre Sorhus 670 | │ │ └─ VendorUrl: sindresorhus.com 671 | │ ├─ string-width@3.1.0 672 | │ │ ├─ URL: https://github.com/sindresorhus/string-width.git 673 | │ │ ├─ VendorName: Sindre Sorhus 674 | │ │ └─ VendorUrl: sindresorhus.com 675 | │ ├─ string-width@4.2.0 676 | │ │ ├─ URL: https://github.com/sindresorhus/string-width.git 677 | │ │ ├─ VendorName: Sindre Sorhus 678 | │ │ └─ VendorUrl: sindresorhus.com 679 | │ ├─ strip-ansi@3.0.1 680 | │ │ ├─ URL: https://github.com/chalk/strip-ansi.git 681 | │ │ ├─ VendorName: Sindre Sorhus 682 | │ │ └─ VendorUrl: sindresorhus.com 683 | │ ├─ strip-ansi@4.0.0 684 | │ │ ├─ URL: https://github.com/chalk/strip-ansi.git 685 | │ │ ├─ VendorName: Sindre Sorhus 686 | │ │ └─ VendorUrl: sindresorhus.com 687 | │ ├─ strip-ansi@5.2.0 688 | │ │ ├─ URL: https://github.com/chalk/strip-ansi.git 689 | │ │ ├─ VendorName: Sindre Sorhus 690 | │ │ └─ VendorUrl: sindresorhus.com 691 | │ ├─ strip-ansi@6.0.0 692 | │ │ ├─ URL: https://github.com/chalk/strip-ansi.git 693 | │ │ ├─ VendorName: Sindre Sorhus 694 | │ │ └─ VendorUrl: sindresorhus.com 695 | │ ├─ strip-final-newline@2.0.0 696 | │ │ ├─ URL: https://github.com/sindresorhus/strip-final-newline.git 697 | │ │ ├─ VendorName: Sindre Sorhus 698 | │ │ └─ VendorUrl: sindresorhus.com 699 | │ ├─ strip-json-comments@2.0.1 700 | │ │ ├─ URL: https://github.com/sindresorhus/strip-json-comments.git 701 | │ │ ├─ VendorName: Sindre Sorhus 702 | │ │ └─ VendorUrl: sindresorhus.com 703 | │ ├─ supports-color@5.5.0 704 | │ │ ├─ URL: https://github.com/chalk/supports-color.git 705 | │ │ ├─ VendorName: Sindre Sorhus 706 | │ │ └─ VendorUrl: sindresorhus.com 707 | │ ├─ supports-color@7.1.0 708 | │ │ ├─ URL: https://github.com/chalk/supports-color.git 709 | │ │ ├─ VendorName: Sindre Sorhus 710 | │ │ └─ VendorUrl: sindresorhus.com 711 | │ ├─ supports-hyperlinks@1.0.1 712 | │ │ ├─ URL: https://github.com/jamestalmage/supports-hyperlinks.git 713 | │ │ ├─ VendorName: James Talmage 714 | │ │ └─ VendorUrl: github.com/jamestalmage 715 | │ ├─ symbol-observable@1.0.1 716 | │ │ ├─ URL: https://github.com/blesh/symbol-observable.git 717 | │ │ └─ VendorName: Ben Lesh 718 | │ ├─ tar-fs@2.0.0 719 | │ │ ├─ URL: https://github.com/mafintosh/tar-fs.git 720 | │ │ ├─ VendorName: Mathias Buus 721 | │ │ └─ VendorUrl: https://github.com/mafintosh/tar-fs 722 | │ ├─ tar-stream@2.1.0 723 | │ │ ├─ URL: git+https://github.com/mafintosh/tar-stream.git 724 | │ │ ├─ VendorName: Mathias Buus 725 | │ │ └─ VendorUrl: https://github.com/mafintosh/tar-stream 726 | │ ├─ through@2.3.8 727 | │ │ ├─ URL: https://github.com/dominictarr/through.git 728 | │ │ ├─ VendorName: Dominic Tarr 729 | │ │ └─ VendorUrl: https://github.com/dominictarr/through 730 | │ ├─ tmp@0.0.33 731 | │ │ ├─ URL: https://github.com/raszi/node-tmp.git 732 | │ │ ├─ VendorName: KARASZI István 733 | │ │ └─ VendorUrl: http://github.com/raszi/node-tmp 734 | │ ├─ treeify@1.1.0 735 | │ │ ├─ URL: https://github.com/notatestuser/treeify.git 736 | │ │ └─ VendorName: Luke Plaster 737 | │ ├─ universalify@0.1.2 738 | │ │ ├─ URL: git+https://github.com/RyanZim/universalify.git 739 | │ │ ├─ VendorName: Ryan Zimmerman 740 | │ │ └─ VendorUrl: https://github.com/RyanZim/universalify#readme 741 | │ ├─ universalify@1.0.0 742 | │ │ ├─ URL: git+https://github.com/RyanZim/universalify.git 743 | │ │ ├─ VendorName: Ryan Zimmerman 744 | │ │ └─ VendorUrl: https://github.com/RyanZim/universalify#readme 745 | │ ├─ util-deprecate@1.0.2 746 | │ │ ├─ URL: git://github.com/TooTallNate/util-deprecate.git 747 | │ │ ├─ VendorName: Nathan Rajlich 748 | │ │ └─ VendorUrl: https://github.com/TooTallNate/util-deprecate 749 | │ ├─ which-pm-runs@1.0.0 750 | │ │ ├─ URL: git+https://github.com/zkochan/which-pm-runs.git 751 | │ │ ├─ VendorName: Zoltan Kochan 752 | │ │ └─ VendorUrl: https://github.com/zkochan/which-pm-runs#readme 753 | │ ├─ widest-line@2.0.1 754 | │ │ ├─ URL: https://github.com/sindresorhus/widest-line.git 755 | │ │ ├─ VendorName: Sindre Sorhus 756 | │ │ └─ VendorUrl: sindresorhus.com 757 | │ ├─ wrap-ansi@4.0.0 758 | │ │ ├─ URL: https://github.com/chalk/wrap-ansi.git 759 | │ │ ├─ VendorName: Sindre Sorhus 760 | │ │ └─ VendorUrl: sindresorhus.com 761 | │ └─ xmldoc@1.1.2 762 | │ ├─ URL: git://github.com/nfarina/xmldoc.git 763 | │ ├─ VendorName: Nick Farina 764 | │ └─ VendorUrl: http://nfarina.com 765 | └─ WTFPL 766 | └─ password-prompt@1.1.2 767 | ├─ URL: https://github.com/jdxcode/password-prompt 768 | ├─ VendorName: Jeff Dickey @jdxcode 769 | └─ VendorUrl: https://github.com/jdxcode/password-prompt 770 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@splunk/dashpub", 3 | "version": "0.0.1-alpha5", 4 | "license": "Apache-2.0", 5 | "description": "Generate next.js apps to publish Splunk dashboards", 6 | "homepage": "https://github.com/splunk/dashpub", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/splunk/dashpub.git" 10 | }, 11 | "bin": { 12 | "dashpub": "./cli/cli.js" 13 | }, 14 | "dependencies": { 15 | "cli-ux": "^5.4.5", 16 | "debug": "^4.1.1", 17 | "dotenv": "^8.2.0", 18 | "execa": "^4.0.0", 19 | "fs-extra": "^9.0.0", 20 | "inquirer": "^7.3.3", 21 | "inquirer-checkbox-plus-prompt": "^1.0.1", 22 | "node-fetch": "^2.6.1", 23 | "prettier": "^1.19.1", 24 | "querystring": "^0.2.0", 25 | "sharp": "^0.29.2", 26 | "xmldoc": "^1.1.2" 27 | }, 28 | "scripts": { 29 | "update:license-report": "yarn licenses list > license_report.txt" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .vscode 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | .now 24 | .next 25 | .vercel 26 | -------------------------------------------------------------------------------- /template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /template/next.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const fs = require('fs'); 18 | const path = require('path'); 19 | const settings = Object.assign({}, require('./package.json').dashpub.settings); 20 | 21 | module.exports = { 22 | webpack(config, { buildId, webpack }) { 23 | const snapshotPath = path.join(__dirname, 'src/pages/api/data/_snapshot.json'); 24 | if (!fs.existsSync(snapshotPath)) { 25 | fs.writeFileSync(snapshotPath, '{}', { encoding: 'utf-8' }); 26 | } 27 | if (settings.useDataSnapshots) { 28 | const contents = fs.readFileSync(snapshotPath, { encoding: 'utf-8' }); 29 | if (contents === '{}') { 30 | throw new Error('Data snapshots are enabled, but snapshot is empty'); 31 | } 32 | } 33 | 34 | config.plugins.push( 35 | new webpack.DefinePlugin({ 36 | 'process.env.USE_DATA_SNAPSHOTS': JSON.stringify(settings.useDataSnapshots), 37 | 'process.env.DASHPUB_BUILD_ID': JSON.stringify(buildId), 38 | }) 39 | ); 40 | 41 | return config; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-name", 3 | "private": true, 4 | "version": "1.0.0", 5 | "license": "UNLICENSED", 6 | "dependencies": { 7 | "@babel/runtime": "^7.9.2", 8 | "@splunk/charting-bundle": "24.5.0", 9 | "@splunk/dashboard-context": "24.5.0", 10 | "@splunk/dashboard-core": "24.5.0", 11 | "@splunk/dashboard-definition": "24.5.0", 12 | "@splunk/dashboard-event-handlers": "24.5.0", 13 | "@splunk/dashboard-icons": "24.5.0", 14 | "@splunk/dashboard-inputs": "24.5.0", 15 | "@splunk/dashboard-layouts": "24.5.0", 16 | "@splunk/dashboard-presets": "24.5.0", 17 | "@splunk/dashboard-telemetry": "24.5.0", 18 | "@splunk/dashboard-ui": "24.5.0", 19 | "@splunk/dashboard-utils": "24.5.0", 20 | "@splunk/dashboard-visualizations": "24.5.0", 21 | "@splunk/datasource-utils": "24.5.0", 22 | "@splunk/datasources": "24.5.0", 23 | "@splunk/react-icons": "3.2.0", 24 | "@splunk/react-ui": "4.7.0", 25 | "@splunk/visualization-color-palettes": "24.5.0", 26 | "@splunk/visualization-context": "24.5.0", 27 | "@splunk/visualization-encoding": "24.5.0", 28 | "@splunk/visualization-encoding-parsers": "24.5.0", 29 | "@splunk/visualization-icons": "24.5.0", 30 | "@splunk/visualization-themes": "24.5.0", 31 | "@splunk/visualizations": "24.5.0", 32 | "@splunk/visualizations-shared": "24.5.0", 33 | "fast-text-encoding": "^1.0.2", 34 | "next": "^9.3.6", 35 | "prettier": "^2.0.5", 36 | "querystring": "^0.2.0", 37 | "react": "16.14.0", 38 | "react-dom": "16.14.0", 39 | "react-full-screen": "^0.2.4", 40 | "styled-components": "^5.0.0" 41 | }, 42 | "resolutions": { 43 | "@splunk/charting-bundle": "24.5.0", 44 | "@splunk/dashboard-context": "24.5.0", 45 | "@splunk/dashboard-core": "24.5.0", 46 | "@splunk/dashboard-definition": "24.5.0", 47 | "@splunk/dashboard-event-handlers": "24.5.0", 48 | "@splunk/dashboard-icons": "24.5.0", 49 | "@splunk/dashboard-inputs": "24.5.0", 50 | "@splunk/dashboard-layouts": "24.5.0", 51 | "@splunk/dashboard-presets": "24.5.0", 52 | "@splunk/dashboard-telemetry": "24.5.0", 53 | "@splunk/dashboard-ui": "24.5.0", 54 | "@splunk/dashboard-utils": "24.5.0", 55 | "@splunk/dashboard-visualizations": "24.5.0", 56 | "@splunk/datasource-utils": "24.5.0", 57 | "@splunk/datasources": "24.5.0", 58 | "@splunk/react-icons": "3.2.0", 59 | "@splunk/react-ui": "4.7.0", 60 | "@splunk/visualization-color-palettes": "24.5.0", 61 | "@splunk/visualization-context": "24.5.0", 62 | "@splunk/visualization-encoding": "24.5.0", 63 | "@splunk/visualization-encoding-parsers": "24.5.0", 64 | "@splunk/visualization-icons": "24.5.0", 65 | "@splunk/visualization-themes": "24.5.0", 66 | "@splunk/visualizations": "24.5.0", 67 | "@splunk/visualizations-shared": "24.5.0" 68 | }, 69 | "scripts": { 70 | "dev": "next dev", 71 | "build": "next build", 72 | "start": "next start" 73 | }, 74 | "eslintConfig": { 75 | "extends": "react-app" 76 | }, 77 | "browserslist": { 78 | "production": [ 79 | ">0.2%", 80 | "not dead", 81 | "not op_mini all" 82 | ], 83 | "development": [ 84 | "last 1 chrome version", 85 | "last 1 firefox version", 86 | "last 1 safari version" 87 | ] 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /template/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/favicon.ico -------------------------------------------------------------------------------- /template/public/fonts/inconsolata-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/inconsolata-regular.woff -------------------------------------------------------------------------------- /template/public/fonts/proxima-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/proxima-bold-webfont.woff -------------------------------------------------------------------------------- /template/public/fonts/proxima-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/proxima-regular-webfont.woff -------------------------------------------------------------------------------- /template/public/fonts/proxima-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/proxima-semibold-webfont.woff -------------------------------------------------------------------------------- /template/public/fonts/splunkdatasans-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/splunkdatasans-bold.woff2 -------------------------------------------------------------------------------- /template/public/fonts/splunkdatasans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/splunkdatasans-regular.woff2 -------------------------------------------------------------------------------- /template/public/fonts/splunkdatasans-semibold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/dashpub/602dc25f6b298764d6004f6b4b0c2140f5c23703/template/public/fonts/splunkdatasans-semibold.woff2 -------------------------------------------------------------------------------- /template/src/_version.json: -------------------------------------------------------------------------------- 1 | { "version": "dev" } 2 | -------------------------------------------------------------------------------- /template/src/autoupdate.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const VERSION = process.env.DASHPUB_BUILD_ID || 'dev'; 18 | 19 | export function startAutoUpdateCheck() { 20 | if (VERSION != null && VERSION !== 'dev') { 21 | console.log('Current version is', VERSION); 22 | setInterval(performUpdateCheck, 30 * 60000); 23 | } 24 | } 25 | 26 | async function performUpdateCheck() { 27 | try { 28 | const res = await fetch('/api/version'); 29 | const { version } = await res.json(); 30 | if (version && version !== VERSION) { 31 | console.log('DETECTED NEW VERSION %o (current version is %o)', version, VERSION); 32 | window.location.reload(); 33 | } else { 34 | console.log('Version %o is still latest', VERSION); 35 | } 36 | } catch (e) { 37 | console.error('Update check failed', e); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /template/src/components/dashboard.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import { DashboardContextProvider } from '@splunk/dashboard-context'; 18 | import GeoRegistry from '@splunk/dashboard-context/GeoRegistry'; 19 | import GeoJsonProvider from '@splunk/dashboard-context/GeoJsonProvider'; 20 | import DashboardCore from '@splunk/dashboard-core'; 21 | import React, { Suspense, useMemo, useEffect, useRef } from 'react'; 22 | import Loading from './loading'; 23 | import defaultPreset from '../preset'; 24 | import { SayCheese, registerScreenshotReadinessDep } from '../ready'; 25 | import { testTileConfig } from '@splunk/visualization-context/MapContext'; 26 | 27 | const mapTileConfig = { defaultTileConfig: testTileConfig }; 28 | 29 | 30 | const PROD_SRC_PREFIXES = [ 31 | // Add URL prefixes here that will be replaced with the page's current origin 32 | ]; 33 | 34 | function updateAssetUrls(orig, { origin = window.location.origin } = {}) { 35 | const images = new Set(); 36 | const def = JSON.parse(JSON.stringify(orig)); 37 | const normalizeImageUrl = (url) => { 38 | if (url.startsWith('/')) { 39 | return `${origin}${url}`; 40 | } 41 | for (const prefix of PROD_SRC_PREFIXES) { 42 | if (url.startsWith(prefix)) { 43 | return `${origin}${url.slice(prefix.length)}`; 44 | } 45 | } 46 | return url; 47 | }; 48 | // Convert server-relative URLs to absolute URLs before rendering 49 | for (const viz of Object.values(def.visualizations)) { 50 | if (viz.type === 'viz.singlevalueicon' && viz.options.icon) { 51 | viz.options.icon = normalizeImageUrl(viz.options.icon); 52 | images.add(viz.options.src); 53 | } 54 | if (viz.type === 'viz.img' && viz.options.src) { 55 | viz.options.src = normalizeImageUrl(viz.options.src); 56 | images.add(viz.options.src); 57 | } 58 | } 59 | if (def.layout.options.backgroundImage && def.layout.options.backgroundImage.src) { 60 | def.layout.options.backgroundImage.src = normalizeImageUrl(def.layout.options.backgroundImage.src); 61 | images.add(def.layout.options.backgroundImage.src); 62 | } 63 | if (!def.layout.options.backgroundColor) { 64 | def.layout.options.backgroundColor = '#ffffff'; 65 | } 66 | delete def.theme; 67 | return [def, [...images].filter((img) => img != null)]; 68 | } 69 | 70 | class Img { 71 | constructor(src) { 72 | this.src = src; 73 | this.image = new Image(); 74 | this.promise = new Promise((resolve, reject) => { 75 | this.image.onload = resolve; 76 | this.image.onerror = reject; 77 | this.image.src = src; 78 | }); 79 | } 80 | } 81 | 82 | function preloadImages(images) { 83 | useEffect(() => { 84 | const readyDef = registerScreenshotReadinessDep(`IMGs[${images.length}]`); 85 | const imgs = images.map((src) => new Img(src)); 86 | Promise.all(imgs.map((img) => img.promise)).then(() => { 87 | readyDef.ready(); 88 | }); 89 | return () => { 90 | readyDef.remove(); 91 | }; 92 | }, [images]); 93 | } 94 | 95 | export default function Dashboard({ definition, preset, width = '100vw', height = '100vh' }) { 96 | const [processedDef, images] = useMemo(() => updateAssetUrls(definition), [definition]); 97 | preloadImages(images); 98 | const geoRegistry = useMemo(() => { 99 | const geoRegistry = GeoRegistry.create(); 100 | geoRegistry.addDefaultProvider(new GeoJsonProvider()); 101 | return geoRegistry; 102 | }, []); 103 | 104 | useEffect(() => { 105 | const readyDep = registerScreenshotReadinessDep('DASH'); 106 | const t = setTimeout(() => readyDep.ready(), 500); 107 | return () => { 108 | clearTimeout(t); 109 | readyDep.remove(); 110 | }; 111 | }, []); 112 | 113 | return ( 114 | 115 | }> 116 | 117 | 124 | 125 | 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /template/src/components/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import styled from 'styled-components'; 19 | import { variables } from '@splunk/themes'; 20 | import dashboardManifest from '../_dashboards.json'; 21 | 22 | const Wrapper = styled.div` 23 | width: 100vw; 24 | height: 100vh; 25 | overflow: hidden; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | flex-direction: column; 30 | `; 31 | 32 | const DashLink = styled.a` 33 | display: flex; 34 | color: ${variables.textColor}; 35 | text-decoration: none; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | text-align: center; 40 | width: 280px; 41 | height: 80px; 42 | border: 1px solid #eee; 43 | margin: 10px 10px 0 0; 44 | box-sizing: border-box; 45 | padding: 0 15px; 46 | line-height: 120%; 47 | 48 | &:hover { 49 | background: rgb(0, 0, 0, 0.2); 50 | } 51 | `; 52 | 53 | const Title = styled.h1` 54 | color: ${variables.textColor}; 55 | `; 56 | 57 | export default function Home({ title = 'Dashboards' }) { 58 | return ( 59 | 60 | Dashboards 61 | {Object.keys(dashboardManifest).map((k) => ( 62 | 63 | {dashboardManifest[k]} 64 | 65 | ))} 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /template/src/components/loading.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import styled from 'styled-components'; 19 | 20 | const Wrapper = styled.div` 21 | width: 100vw; 22 | height: 100vh; 23 | overflow: hidden; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | `; 28 | const Inner = styled.div` 29 | font-size: 24px; 30 | `; 31 | 32 | const Msg = styled.span` 33 | color: #333; 34 | `; 35 | 36 | export default function Loading() { 37 | return ( 38 | 39 | 40 | Loading... 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /template/src/components/nossr.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | export default function NoSSR({ children }) { 18 | return process.browser ? children : null; 19 | } 20 | -------------------------------------------------------------------------------- /template/src/components/page.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import React, { useEffect } from 'react'; 18 | import { createGlobalStyle } from 'styled-components'; 19 | import Head from 'next/head'; 20 | import { startAutoUpdateCheck } from '../autoupdate'; 21 | import { SplunkThemeProvider, variables } from '@splunk/themes'; 22 | 23 | const TITLE_SUFFIX = 'Splunk Dashboard'; 24 | 25 | const GlobalBackgroundStyle = createGlobalStyle` 26 | html, body { 27 | background-color: ${(props) => props.backgroundColor || variables.backgroundColor(props)}; 28 | } 29 | `; 30 | 31 | const fullUrl = (baseUrl, path) => { 32 | if (!baseUrl) { 33 | return path; 34 | } 35 | const u = new URL(baseUrl); 36 | u.pathname = path; 37 | return u.href; 38 | }; 39 | 40 | export default function Page({ 41 | title, 42 | description, 43 | theme = 'light', 44 | backgroundColor, 45 | imageUrl, 46 | imageSize = { width: 700, height: 340 }, 47 | path, 48 | baseUrl, 49 | children, 50 | }) { 51 | useEffect(() => { 52 | startAutoUpdateCheck(); 53 | }, []); 54 | 55 | return ( 56 | <> 57 | 58 | 59 | {title} - {TITLE_SUFFIX} 60 | 61 | {description && } 62 | 63 | 64 | {description && } 65 | {imageUrl != null && baseUrl != null && ( 66 | <> 67 | 68 | 69 | 70 | 71 | )} 72 | 73 | 74 | 75 | {imageUrl != null && baseUrl != null && ( 76 | <> 77 | 78 | 79 | )} 80 | 81 | 82 | 83 | 84 | {children} 85 | 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /template/src/datasource.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | import DataSource from '@splunk/datasources/DataSource'; 18 | import DataSet from '@splunk/datasource-utils/DataSet'; 19 | import { registerScreenshotReadinessDep } from './ready'; 20 | 21 | const DEFAULT_REFRESH_TIME = 60000; 22 | const BACKGROUND_REFESH_TIME = 600 * 1000; 23 | const LAST_RESULTS = {}; 24 | 25 | async function waitForRefresh(regularInterval, backgroundInterval) { 26 | if (document.visibilityState == null || document.visibilityState === 'visible') { 27 | return new Promise((resolve) => setTimeout(resolve, regularInterval)); 28 | } 29 | return new Promise((resolve) => { 30 | let done, timer; 31 | const cb = () => { 32 | if (document.visibilityState === 'visible') { 33 | done(); 34 | } 35 | }; 36 | document.addEventListener('visibilitychange', cb); 37 | done = () => { 38 | clearTimeout(timer); 39 | document.removeEventListener('visibilitychange', cb); 40 | resolve(); 41 | }; 42 | timer = setTimeout(done, backgroundInterval); 43 | }); 44 | } 45 | 46 | export function createDataSet(data, options = {}) { 47 | let transformedData = data; 48 | if (options.sort) { 49 | const sortField = Object.keys(options.sort)[0]; 50 | const colIdx = data.fields.indexOf(sortField); 51 | if (colIdx > -1) { 52 | const column = data.columns[colIdx]; 53 | const indexes = [...Array(column.length)].map((_, i) => i); 54 | 55 | const dirSortFactor = options.sort[sortField] === 'asc' ? 1 : -1; 56 | const numColumn = column.map((v) => (v != null ? parseFloat(v, 10) : 0)); 57 | const isNumColumn = numColumn.every((v) => !isNaN(v)); 58 | if (isNumColumn) { 59 | indexes.sort((a, b) => (numColumn[a] - numColumn[b]) * dirSortFactor); 60 | } else { 61 | indexes.sort((a, b) => (column[a] || '').localeCompare(column[b] || '') * dirSortFactor); 62 | } 63 | transformedData = { 64 | fields: data.fields, 65 | columns: data.columns.map((c) => indexes.map((i) => c[i])), 66 | }; 67 | } 68 | } 69 | return DataSet.fromJSONCols(transformedData.fields, transformedData.columns); 70 | } 71 | 72 | function createNextPayload({ data, vizOptions }) { 73 | return { 74 | data, 75 | meta: { 76 | percentComplete: 100, 77 | status: 'done', 78 | totalCount: (data.columns[0] || []).length, 79 | lastUpdated: new Date().toISOString(), 80 | }, 81 | vizOptions, 82 | }; 83 | } 84 | 85 | export default class PublicDataSource extends DataSource { 86 | constructor(options = {}, context = {}, ...rest) { 87 | super(options, context); 88 | this.uri = options.uri; 89 | this.vizOptions = options.vizOptions; 90 | this.meta = options.meta; 91 | } 92 | 93 | request(options) { 94 | options = options || {}; 95 | return (observer) => { 96 | let aborted = false; 97 | let readyDep = registerScreenshotReadinessDep('DS'); 98 | 99 | (async () => { 100 | let initial = true; 101 | 102 | if (LAST_RESULTS[this.uri]) { 103 | const { ts, data } = LAST_RESULTS[this.uri]; 104 | observer.next( 105 | createNextPayload({ 106 | data: createDataSet(data, options), 107 | vizOptions: this.vizOptions, 108 | }) 109 | ); 110 | const wait = DEFAULT_REFRESH_TIME - (Date.now() - ts); 111 | if (wait > 0) { 112 | await waitForRefresh(wait, wait); 113 | } 114 | initial = false; 115 | } 116 | 117 | while (!aborted) { 118 | try { 119 | const res = await fetch(this.uri); 120 | if (aborted) { 121 | break; 122 | } 123 | if (res.status > 299) { 124 | throw new Error(`HTTP Status ${res.status}`); 125 | } 126 | const data = await res.json(); 127 | if (data.error) { 128 | throw new Error(data.error); 129 | } 130 | LAST_RESULTS[this.uri] = { 131 | ts: Date.now(), 132 | data, 133 | }; 134 | readyDep.ready(); 135 | observer.next( 136 | createNextPayload({ 137 | data: createDataSet(data, options), 138 | vizOptions: this.vizOptions, 139 | }) 140 | ); 141 | } catch (e) { 142 | if (aborted) { 143 | break; 144 | } 145 | if (initial) { 146 | observer.error({ 147 | level: 'error', 148 | message: e.message || 'Unexpected error', 149 | }); 150 | } 151 | observer.next( 152 | createNextPayload({ 153 | data: DataSet.empty(), 154 | vizOptions: this.vizOptions, 155 | }) 156 | ); 157 | } 158 | initial = false; 159 | await waitForRefresh(DEFAULT_REFRESH_TIME, BACKGROUND_REFESH_TIME); 160 | } 161 | })(); 162 | 163 | return () => { 164 | aborted = true; 165 | readyDep.remove(); 166 | }; 167 | }; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /template/src/drilldown.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Splunk Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | const INTERNAL_LINK_PREFIX = '/en-US/app/search/'; 18 | 19 | export default class DrilldownHandler { 20 | constructor(options = {}) { 21 | this.options = options; 22 | this.events = Array.isArray(options.events) ? options.events : ['any']; 23 | } 24 | 25 | canHandle(event) { 26 | return ( 27 | event && 28 | event.type !== 'range.select' && 29 | this.options && 30 | !!this.options.url && 31 | (this.events.includes('any') || this.events.includes(event.type)) 32 | ); 33 | } 34 | 35 | handle() { 36 | const { url, newTab } = this.options; 37 | 38 | if (url.indexOf(INTERNAL_LINK_PREFIX) === 0) { 39 | const dashboard = url.slice(INTERNAL_LINK_PREFIX.length); 40 | return Promise.resolve([ 41 | { 42 | type: 'linkTo', 43 | payload: { 44 | url: `/${dashboard}`, 45 | newTab: newTab, 46 | }, 47 | }, 48 | ]); 49 | } 50 | 51 | return Promise.resolve([ 52 | { 53 | type: 'linkTo', 54 | payload: { 55 | url: url, 56 | newTab: newTab, 57 | }, 58 | }, 59 | ]); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /template/src/pages/[dashboard].jsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import Loading from '../components/loading'; 3 | import NoSSR from '../components/nossr'; 4 | import Page from '../components/page'; 5 | 6 | const Dashboard = lazy(() => import('../components/dashboard')); 7 | 8 | export default function DashboardPage({ definition, dashboardId, baseUrl }) { 9 | return ( 10 | 19 | 20 | }> 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export async function getStaticProps({ params }) { 29 | const definition = require(`../dashboards/${params.dashboard}/definition.json`); 30 | return { 31 | props: { 32 | definition, 33 | dashboardId: params.dashboard, 34 | baseUrl: process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null, 35 | }, 36 | }; 37 | } 38 | 39 | export async function getStaticPaths() { 40 | const dashboards = require('../_dashboards.json'); 41 | return { 42 | paths: Object.keys(dashboards) 43 | .filter((d) => d !== 'timelapse') 44 | .map((d) => ({ params: { dashboard: d } })), 45 | fallback: false, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /template/src/pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import Document, { Main, Html, Head, NextScript } from 'next/document'; 2 | import { ServerStyleSheet } from 'styled-components'; 3 | 4 | export default class MyDocument extends Document { 5 | static async getInitialProps(ctx) { 6 | const sheet = new ServerStyleSheet(); 7 | const originalRenderPage = ctx.renderPage; 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: App => props => sheet.collectStyles(), 13 | }); 14 | 15 | const initialProps = await Document.getInitialProps(ctx); 16 | return { 17 | ...initialProps, 18 | styles: ( 19 | <> 20 | {initialProps.styles} 21 | {sheet.getStyleElement()} 22 | 23 | ), 24 | }; 25 | } finally { 26 | sheet.seal(); 27 | } 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 | 34 |