├── packages ├── insights-web │ ├── .env │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── lib │ │ │ ├── selectors │ │ │ │ └── location.js │ │ │ ├── utils │ │ │ │ ├── delay.js │ │ │ │ ├── move-caret-to-end.js │ │ │ │ ├── script.js │ │ │ │ ├── range.js │ │ │ │ └── highlight-text.js │ │ │ ├── popups │ │ │ │ ├── delete.js │ │ │ │ └── prompt.js │ │ │ ├── explorer │ │ │ │ ├── get-meta.js │ │ │ │ ├── get-sorted-meta.js │ │ │ │ ├── state-to-url.js │ │ │ │ └── url-to-state.js │ │ │ ├── client.js │ │ │ └── tags │ │ │ │ ├── submit-button.js │ │ │ │ └── spinner.js │ │ ├── scenes │ │ │ ├── settings │ │ │ │ ├── styles.scss │ │ │ │ └── index.js │ │ │ ├── users │ │ │ │ ├── styles.scss │ │ │ │ ├── logic.js │ │ │ │ └── index.js │ │ │ ├── explorer │ │ │ │ ├── tags │ │ │ │ │ └── full-path │ │ │ │ │ │ ├── styles.scss │ │ │ │ │ │ └── index.js │ │ │ │ ├── connection │ │ │ │ │ ├── database │ │ │ │ │ │ ├── form │ │ │ │ │ │ │ └── intro.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── menu │ │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── subset │ │ │ │ │ │ ├── form │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ ├── model-row │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── styles.scss │ │ │ │ │ │ │ ├── field-row │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── json │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── logic.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── menu │ │ │ │ │ │ └── index.js │ │ │ │ ├── graph │ │ │ │ │ ├── styles.scss │ │ │ │ │ ├── time-group-select.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── compare-with.js │ │ │ │ │ └── controls-right.js │ │ │ │ ├── pagination │ │ │ │ │ └── index.js │ │ │ │ ├── dashboard │ │ │ │ │ ├── views │ │ │ │ │ │ ├── styles.scss │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── breadcrumbs │ │ │ │ │ │ ├── model.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── styles.scss │ │ │ │ ├── filter │ │ │ │ │ ├── index.js │ │ │ │ │ ├── column-filters.js │ │ │ │ │ └── styles.scss │ │ │ │ ├── sidebar │ │ │ │ │ ├── models │ │ │ │ │ │ ├── logic.js │ │ │ │ │ │ └── index.js │ │ │ │ │ └── selected-model │ │ │ │ │ │ ├── aggregate │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── pin │ │ │ │ │ │ └── index.js │ │ │ │ ├── time-filter │ │ │ │ │ └── index.js │ │ │ │ ├── styles.scss │ │ │ │ └── table │ │ │ │ │ └── table-settings.js │ │ │ ├── header │ │ │ │ ├── logic.js │ │ │ │ ├── styles.scss │ │ │ │ ├── saga.js │ │ │ │ ├── views │ │ │ │ │ ├── styles.scss │ │ │ │ │ └── index.js │ │ │ │ ├── user │ │ │ │ │ └── index.js │ │ │ │ ├── copy-query │ │ │ │ │ └── index.js │ │ │ │ └── share │ │ │ │ │ └── index.js │ │ │ ├── _layout │ │ │ │ ├── logic.js │ │ │ │ ├── styles.scss │ │ │ │ └── index.js │ │ │ ├── login │ │ │ │ ├── styles.scss │ │ │ │ ├── logic.js │ │ │ │ ├── saga.js │ │ │ │ └── index.js │ │ │ ├── routes.js │ │ │ ├── index.js │ │ │ └── urls │ │ │ │ └── index.js │ │ ├── index.scss │ │ └── index.js │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── logo64.png │ │ ├── manifest.json │ │ ├── index.html │ │ └── insights.svg │ ├── .gitignore │ ├── config-overrides.js │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── insights-charts │ ├── src │ │ └── index.js │ ├── .babelrc │ └── package.json ├── insights-api │ ├── src │ │ ├── config.ts │ │ ├── middleware │ │ │ └── index.ts │ │ ├── services │ │ │ ├── favourites │ │ │ │ ├── favourites.class.ts │ │ │ │ ├── favourites.hooks.ts │ │ │ │ └── favourites.service.ts │ │ │ ├── urls │ │ │ │ ├── urls.class.ts │ │ │ │ ├── urls.service.ts │ │ │ │ └── urls.hooks.ts │ │ │ ├── users │ │ │ │ ├── users.class.ts │ │ │ │ ├── users.service.ts │ │ │ │ └── users.hooks.ts │ │ │ ├── views │ │ │ │ ├── views.class.ts │ │ │ │ ├── views.hooks.ts │ │ │ │ └── views.service.ts │ │ │ ├── connections │ │ │ │ ├── connections.class.ts │ │ │ │ ├── connections.service.ts │ │ │ │ └── connections.hooks.ts │ │ │ ├── results │ │ │ │ ├── results.hooks.ts │ │ │ │ ├── results.service.ts │ │ │ │ └── results.class.ts │ │ │ ├── structure │ │ │ │ ├── structure.hooks.ts │ │ │ │ └── structure.service.ts │ │ │ ├── connection-test │ │ │ │ ├── connection-test.hooks.ts │ │ │ │ ├── connection-test.service.ts │ │ │ │ └── connection-test.class.ts │ │ │ ├── index.ts │ │ │ └── subsets │ │ │ │ ├── subsets.service.ts │ │ │ │ ├── subsets.hooks.ts │ │ │ │ └── subsets.class.ts │ │ ├── utils │ │ │ ├── random-string.ts │ │ │ ├── set-config-folder.ts │ │ │ └── find-config-folder.ts │ │ ├── models │ │ │ ├── views.model.ts │ │ │ ├── subsets.model.ts │ │ │ ├── connections.model.ts │ │ │ ├── favourites.model.ts │ │ │ ├── urls.model.ts │ │ │ └── users.model.ts │ │ ├── insights │ │ │ ├── structure │ │ │ │ ├── generators │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── adapter │ │ │ │ ├── index.ts │ │ │ │ └── sql │ │ │ │ │ ├── sqlite.ts │ │ │ │ │ └── postgres.ts │ │ │ └── definitions.d.ts │ │ ├── declarations.d.ts │ │ ├── logger.ts │ │ ├── index.ts │ │ ├── app.hooks.ts │ │ ├── app.ts │ │ ├── authentication.ts │ │ └── channels.ts │ ├── jest.config.js │ ├── test │ │ ├── services │ │ │ ├── urls.test.ts │ │ │ ├── users.test.ts │ │ │ ├── views.test.ts │ │ │ ├── results.test.ts │ │ │ ├── subsets.test.ts │ │ │ ├── favourites.test.ts │ │ │ ├── structure.test.ts │ │ │ ├── connections.test.ts │ │ │ └── connection-test.test.ts │ │ ├── authentication.test.ts │ │ └── app.test.ts │ ├── .editorconfig │ ├── tsconfig.json │ ├── README.md │ ├── .gitignore │ └── package.json ├── insights-desktop │ ├── main.js │ ├── bin │ │ └── insights-desktop │ └── package.json └── insights │ ├── app │ ├── templates │ │ ├── development.json │ │ ├── production.json │ │ └── default.json │ ├── lib │ │ ├── random-string.js │ │ ├── create-folder.js │ │ └── find-config-folder.js │ ├── create-secret.js │ ├── start.js │ └── create-superuser.js │ ├── yarn.lock │ ├── bin │ ├── insights-createsecret │ ├── insights │ ├── insights-init │ ├── insights-createsuperuser │ └── insights-start │ └── package.json ├── tsconfig.json ├── .editorconfig ├── .vscode └── launch.json ├── .github └── FUNDING.yml ├── .eslintrc ├── .npmignore ├── TODO.md ├── scripts └── sync-versions.js ├── LICENSE ├── package.json ├── .gitignore └── README.md /packages/insights-web/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | -------------------------------------------------------------------------------- /packages/insights-charts/src/index.js: -------------------------------------------------------------------------------- 1 | export { Graph } from './graph/recharts' 2 | -------------------------------------------------------------------------------- /packages/insights-web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/selectors/location.js: -------------------------------------------------------------------------------- 1 | export default state => state.router.location 2 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/settings/styles.scss: -------------------------------------------------------------------------------- 1 | .settings-scene { 2 | margin: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /packages/insights-api/src/config.ts: -------------------------------------------------------------------------------- 1 | // timezone for the dates 2 | export const defaultTimezone = 'UTC' 3 | -------------------------------------------------------------------------------- /packages/insights-web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/insights-desktop/main.js: -------------------------------------------------------------------------------- 1 | const startInsightsDesktop = require('./src/insights-desktop') 2 | startInsightsDesktop() 3 | -------------------------------------------------------------------------------- /packages/insights-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariusandra/insights/HEAD/packages/insights-web/public/favicon.ico -------------------------------------------------------------------------------- /packages/insights-web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariusandra/insights/HEAD/packages/insights-web/public/logo192.png -------------------------------------------------------------------------------- /packages/insights-web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariusandra/insights/HEAD/packages/insights-web/public/logo512.png -------------------------------------------------------------------------------- /packages/insights-web/public/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariusandra/insights/HEAD/packages/insights-web/public/logo64.png -------------------------------------------------------------------------------- /packages/insights-web/src/lib/utils/delay.js: -------------------------------------------------------------------------------- 1 | // utility to sleep for certain time 2 | export default ms => new Promise(resolve => setTimeout(resolve, ms)) 3 | -------------------------------------------------------------------------------- /packages/insights/app/templates/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 3030, 4 | "authentication": { 5 | "secret": "" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/insights/app/templates/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost", 3 | "port": 8000, 4 | "authentication": { 5 | "secret": "" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/users/styles.scss: -------------------------------------------------------------------------------- 1 | .users-scene { 2 | margin: 20px; 3 | 4 | table.users-table { 5 | td { 6 | vertical-align: middle; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/tags/full-path/styles.scss: -------------------------------------------------------------------------------- 1 | .full-path-explorer-tag { 2 | .root span { 3 | paddingLeft: 5px; 4 | } 5 | .part span { 6 | paddingLeft: 10px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/insights-api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | globals: { 5 | 'ts-jest': { 6 | diagnostics: false 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /packages/insights-api/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | export default function (app: Application) { 5 | } 6 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/settings/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | import React from 'react' 3 | 4 | export default function SettingsScene () { 5 | return

Settings

6 | } 7 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/header/logic.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | 3 | export default kea({ 4 | path: () => ['scenes', 'header', 'index'], 5 | 6 | actions: () => ({ 7 | openLocation: (location) => ({ location }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/urls.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'urls\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('urls'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/users.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'users\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('users'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/views.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'views\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('views'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/results.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'results\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('results'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/subsets.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'subsets\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('subsets'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "baseUrl": "./src", 6 | "outDir": "./lib", 7 | "strict": false, 8 | "esModuleInterop": true 9 | }, 10 | "exclude": [ 11 | "test" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/favourites.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'favourites\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('favourites'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/structure.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'structure\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('structure'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/connections.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'connections\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('connections'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/test/services/connection-test.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../../src/app'; 2 | 3 | describe('\'connection-test\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('connection-test'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/insights-api/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/_layout/logic.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | 3 | export default kea({ 4 | actions: () => ({ 5 | toggleMenu: true 6 | }), 7 | 8 | reducers: ({ actions }) => ({ 9 | menuOpen: [true, { 10 | [actions.toggleMenu]: (state) => !state 11 | }] 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/insights-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": false, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true 10 | }, 11 | "exclude": [ 12 | "test" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/insights/app/lib/random-string.js: -------------------------------------------------------------------------------- 1 | let az09 = 'abcdefghijklmnopqrstuvwxyz0123456789' 2 | 3 | module.exports = function randomString (len, charset = az09) { 4 | let text = '' 5 | for (let i = 0; i < len; i++) { 6 | text += charset.charAt(Math.floor(Math.random() * charset.length)) 7 | } 8 | return text 9 | } 10 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/favourites/favourites.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '../../declarations'; 3 | 4 | export class Favourites extends Service { 5 | constructor(options: Partial, app: Application) { 6 | super(options); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /packages/insights-api/src/utils/random-string.ts: -------------------------------------------------------------------------------- 1 | let az09 = 'abcdefghijklmnopqrstuvwxyz0123456789' 2 | 3 | export default function randomString (len: number, charset = az09) { 4 | let text = '' 5 | for (let i = 0; i < len; i++) { 6 | text += charset.charAt(Math.floor(Math.random() * charset.length)) 7 | } 8 | return text 9 | } 10 | -------------------------------------------------------------------------------- /packages/insights-api/src/models/views.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from 'nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'views.db'), 9 | autoload: true 10 | }); 11 | 12 | return Model; 13 | } 14 | -------------------------------------------------------------------------------- /packages/insights-charts/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [["env", { "loose": true }], "react", "stage-0"] 5 | }, 6 | 7 | "cjs": { 8 | "presets": [["env", { "loose": true }], "react", "stage-0"] 9 | }, 10 | 11 | "es": { 12 | "presets": [["env", { "loose": true, "modules": false }], "react", "stage-0"] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/insights-api/src/models/subsets.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from 'nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'subsets.db'), 9 | autoload: true 10 | }); 11 | 12 | return Model; 13 | } 14 | -------------------------------------------------------------------------------- /packages/insights-api/src/models/connections.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from 'nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'connections.db'), 9 | autoload: true 10 | }); 11 | 12 | return Model; 13 | } 14 | -------------------------------------------------------------------------------- /packages/insights-api/src/models/favourites.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from 'nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'favourites.db'), 9 | autoload: true 10 | }); 11 | 12 | return Model; 13 | } 14 | -------------------------------------------------------------------------------- /packages/insights-api/src/insights/structure/generators/index.ts: -------------------------------------------------------------------------------- 1 | import postgresGenerator from './postgres' 2 | 3 | export default async function createAdapter (connection: string) { 4 | if (connection.indexOf('postgresql://') === 0 || connection.indexOf('psql://') === 0) { 5 | return postgresGenerator(connection) 6 | } else { 7 | throw new Error('No compatible database found!') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/utils/move-caret-to-end.js: -------------------------------------------------------------------------------- 1 | export default function moveCaretToEnd (el) { 2 | if (typeof el.selectionStart === 'number') { 3 | el.selectionStart = el.selectionEnd = el.value.length 4 | } else if (typeof el.createTextRange !== 'undefined') { 5 | el.focus() 6 | var range = el.createTextRange() 7 | range.collapse(false) 8 | range.select() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/urls/urls.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '../../declarations'; 3 | 4 | interface UrlData { 5 | _id?: string; 6 | code: string; 7 | path: string; 8 | } 9 | 10 | export class Urls extends Service { 11 | constructor(options: Partial, app: Application) { 12 | super(options); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/insights-web/.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.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/insights-api/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | import { Application as ExpressFeathers } from '@feathersjs/express'; 2 | import { Service } from '@feathersjs/feathers'; 3 | import '@feathersjs/transport-commons'; 4 | 5 | // A mapping of service names to types. Will be extended in service files. 6 | export interface ServiceTypes {} 7 | // The application instance type that will be used everywhere else 8 | export type Application = ExpressFeathers; 9 | -------------------------------------------------------------------------------- /packages/insights-api/src/models/urls.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from 'nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'urls.db'), 9 | autoload: true 10 | }); 11 | 12 | Model.ensureIndex({ fieldName: 'code', unique: true }); 13 | 14 | return Model; 15 | } 16 | -------------------------------------------------------------------------------- /packages/insights-api/src/utils/set-config-folder.ts: -------------------------------------------------------------------------------- 1 | import {findConfigFolder} from './find-config-folder' 2 | 3 | if (!process.env.NODE_CONFIG_DIR) { 4 | const configFolder = findConfigFolder() 5 | if (configFolder) { 6 | process.env.NODE_CONFIG_DIR = configFolder 7 | } else { 8 | console.error('Fatal Error! Could not find ".insights" config folder! Perhaps you still need to run "insights init"?') 9 | process.exit(1) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/insights-api/src/models/users.model.ts: -------------------------------------------------------------------------------- 1 | import NeDB from 'nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application) { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, 'users.db'), 9 | autoload: true 10 | }); 11 | 12 | Model.ensureIndex({ fieldName: 'email', unique: true }); 13 | 14 | return Model; 15 | } 16 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/database/form/intro.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Alert, Divider } from 'antd' 3 | 4 | export default function Intro () { 5 | return ( 6 | <> 7 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/users/users.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb' 2 | import { Application } from '../../declarations' 3 | 4 | interface UserData { 5 | _id?: string; 6 | email: string; 7 | password: string; 8 | roles: string[]; 9 | } 10 | 11 | export class Users extends Service { 12 | constructor (options: Partial, app: Application) { 13 | super(options) 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/views/views.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '../../declarations'; 3 | 4 | export interface ViewData { 5 | _id?: string; 6 | name: string; 7 | path: string; 8 | connectionId: string, 9 | subsetId: string 10 | } 11 | 12 | export class Views extends Service { 13 | constructor(options: Partial, app: Application) { 14 | super(options); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/insights-api/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | export default logger; 17 | -------------------------------------------------------------------------------- /packages/insights-api/src/index.ts: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | import app from './app'; 3 | 4 | const host = process.env.INSIGHTS_HOST || app.get('host'); 5 | const port = process.env.INSIGHTS_PORT || app.get('port'); 6 | const server = app.listen(port, host); 7 | 8 | process.on('unhandledRejection', (reason, p) => 9 | logger.error('Unhandled Rejection at: Promise ', p, reason) 10 | ); 11 | 12 | server.on('listening', () => 13 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 14 | ); 15 | -------------------------------------------------------------------------------- /packages/insights-desktop/bin/insights-desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env electron 2 | 3 | const path = require('path') 4 | const program = require('commander') 5 | const root = path.join(__dirname, '..') 6 | const pkg = require(path.join(root, 'package.json')) 7 | 8 | const startInsightsDesktop = require('../src/insights-desktop') 9 | 10 | program.version(pkg.version) 11 | .usage(' [options]') 12 | .description('Start Insights in desktop mode via Electron') 13 | 14 | program.parse(process.argv) 15 | 16 | startInsightsDesktop() 17 | -------------------------------------------------------------------------------- /packages/insights-web/src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .ant-dropdown { 16 | box-shadow: 2px 2px 6px rgba(0,0,0,0.3); 17 | } 18 | -------------------------------------------------------------------------------- /packages/insights/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | commander@^2.10.0: 6 | version "2.10.0" 7 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.10.0.tgz#e1f5d3245de246d1a5ca04702fa1ad1bd7e405fe" 8 | dependencies: 9 | graceful-readlink ">= 1.0.0" 10 | 11 | "graceful-readlink@>= 1.0.0": 12 | version "1.0.1" 13 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 14 | -------------------------------------------------------------------------------- /packages/insights/app/lib/create-folder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | module.exports = function createFolder (folder) { 4 | console.log(`Creating folder: ${folder}`) 5 | 6 | if (fs.existsSync(folder)) { 7 | console.error('!! Fatal Error! Folder already exists! Will not overwrite, exiting...') 8 | process.exit(1) 9 | } 10 | 11 | try { 12 | fs.mkdirSync(folder, {recursive: true}) 13 | } catch (error) { 14 | console.error(`!! Fatal Error! Could not create folder: ${folder}`) 15 | process.exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/connections/connections.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '../../declarations'; 3 | 4 | export interface ConnectionData { 5 | _id?: string; 6 | name: string; 7 | url: string; 8 | structurePath?: string; 9 | timeout?: number; 10 | timezone?: string; 11 | } 12 | 13 | export class Connections extends Service { 14 | constructor(options: Partial, app: Application) { 15 | super(options); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /packages/insights-web/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, fixBabelImports, addLessLoader } = require('customize-cra'); 2 | 3 | module.exports = override( 4 | fixBabelImports('import', { 5 | libraryName: 'antd', 6 | libraryDirectory: 'es', 7 | style: true, 8 | }), 9 | addLessLoader({ 10 | javascriptEnabled: true, 11 | modifyVars: { 12 | '@primary-color': '#2e5985', // hsla(210, 49%, 35%, 1) 13 | '@border-color-base': '#cdcdcd', 14 | '@heading-color': '#183149' // hsla(209, 50%, 19%, 1) 15 | }, 16 | }), 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /packages/insights-api/src/insights/adapter/index.ts: -------------------------------------------------------------------------------- 1 | import Postgres from './sql/postgres' 2 | import SQLite from './sql/sqlite' 3 | 4 | export default function createAdapter (connection: string, timeout: number, timezone: string) { 5 | if (connection.indexOf('postgresql://') === 0 || connection.indexOf('psql://') === 0) { 6 | return new Postgres(connection, timeout, timezone) 7 | } else if (connection.indexOf('sqlite://') === 0) { 8 | return new SQLite(connection, timeout, timezone) 9 | } else { 10 | throw new Error('No compatible database found!') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/insights-desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insights-desktop", 3 | "description": "insights electron app", 4 | "version": "0.0.32", 5 | "author": "Marius Andra ", 6 | "main": "main.js", 7 | "dependencies": { 8 | "get-port": "^5.1.1" 9 | }, 10 | "scripts": { 11 | "start": "electron ." 12 | }, 13 | "devDependencies": { 14 | }, 15 | "bin": { 16 | "electron": "^8.0.3", 17 | "insights-desktop": "./bin/insights-desktop" 18 | }, 19 | "files": [ 20 | "bin", 21 | "src", 22 | "main.js" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/graph/styles.scss: -------------------------------------------------------------------------------- 1 | .graph-and-controls { 2 | .graph { 3 | height: calc(100% - 20px); 4 | } 5 | 6 | .controls { 7 | height: 20px; 8 | background: #fff; 9 | 10 | .left { 11 | float: left; 12 | margin-left: 10px; 13 | 14 | > .ant-btn, > .ant-btn-group { 15 | margin-right: 8px; 16 | } 17 | } 18 | 19 | .right { 20 | float: right; 21 | margin-right: 10px; 22 | 23 | > .ant-btn, > .ant-btn-group { 24 | margin-left: 8px; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/server/index.js", 12 | "cwd": "${workspaceRoot}", 13 | "env": { 14 | "NODE_ENV": "development" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/header/styles.scss: -------------------------------------------------------------------------------- 1 | .header-navbar { 2 | background: #162c41; 3 | display: flex; 4 | width: 100%; 5 | height: 50px; 6 | justify-content: space-between; 7 | 8 | > div > button { 9 | margin-left: 4px; 10 | } 11 | 12 | .header-link { 13 | line-height: 45px; 14 | color: white; 15 | cursor: pointer; 16 | font-size: 16px; 17 | margin-left: 14px; 18 | padding-bottom: 5px; 19 | padding-left: 5px; 20 | padding-right: 5px; 21 | &.selected { 22 | border-bottom: 2px solid #86bbe4; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/pagination/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useValues } from 'kea' 3 | 4 | import explorerLogic from 'scenes/explorer/logic' 5 | 6 | export default function Pagination () { 7 | const { count, visibleStart, visibleEnd } = useValues(explorerLogic) 8 | 9 | if (count === 0) { 10 | return 11 | } 12 | 13 | return ( 14 | 15 | {count > 0 ? ( 16 | 17 | {visibleStart} - {visibleEnd} of 18 | {' '} 19 | 20 | ) : null} 21 | {count} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/dashboard/views/styles.scss: -------------------------------------------------------------------------------- 1 | .saved-views { 2 | > strong { 3 | display: block; 4 | margin-bottom: 4px; 5 | color: #333; 6 | } 7 | ul { 8 | padding-left: 0; 9 | list-style: none; 10 | li { 11 | display: flex; 12 | margin-bottom: 2px; 13 | > span { 14 | padding-top: 1px; 15 | i { 16 | color: hsl(209, 54%, 31%); 17 | } 18 | width: 18px; 19 | } 20 | > div { 21 | flex: 1; 22 | a:hover { 23 | text-decoration: underline; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/insights-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/header/saga.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | import { put } from 'redux-saga/effects' 3 | import { push } from 'connected-react-router' 4 | 5 | import headerLogic from 'scenes/header/logic' 6 | 7 | export default kea({ 8 | path: () => ['scenes', 'header', 'saga'], 9 | 10 | connect: { 11 | actions: [ 12 | headerLogic, [ 13 | 'openLocation' 14 | ] 15 | ] 16 | }, 17 | 18 | takeEvery: ({ actions, workers }) => ({ 19 | [actions.openLocation]: function * (action) { 20 | const { location } = action.payload 21 | yield put(push(location)) 22 | } 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/insights-api/src/utils/find-config-folder.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | export function findConfigFolder (currentPath = process.cwd(), trials = [".insights", "config/insights"]) { 5 | while (true) { 6 | for (const trial of trials) { 7 | const fullPath = path.join(currentPath, trial) 8 | if (fs.existsSync(fullPath)) { 9 | return fullPath 10 | } 11 | } 12 | // split out 13 | let folders = currentPath.split(path.sep) 14 | folders.pop() 15 | 16 | if (folders.length === 0) { 17 | return 18 | } 19 | 20 | currentPath = folders.join(path.sep) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/insights-web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.scss'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | import Scenes from './scenes'; 7 | import Popup from 'react-popup' 8 | 9 | ReactDOM.render(, document.getElementById('root')) 10 | ReactDOM.render(, document.getElementById('popupContainer')) 11 | 12 | // If you want your app to work offline and load faster, you can change 13 | // unregister() to register() below. Note this comes with some pitfalls. 14 | // Learn more about service workers: https://bit.ly/CRA-PWA 15 | serviceWorker.unregister() 16 | 17 | -------------------------------------------------------------------------------- /packages/insights-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "baseUrl": "./src" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { Button } from "antd"; 5 | import { useValues } from 'kea' 6 | 7 | import Database from './database' 8 | import Subset from './subset' 9 | 10 | import connectionsLogic from './logic' 11 | 12 | export default function Connection () { 13 | const { selectedConnection } = useValues(connectionsLogic) 14 | 15 | return ( 16 | 17 | 18 | {selectedConnection ? : null} 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /packages/insights/app/lib/find-config-folder.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = function findConfigFolder (currentPath = process.cwd(), trials = [".insights", "config/insights"]) { 5 | while (true) { 6 | for (const trial of trials) { 7 | const fullPath = path.join(currentPath, trial) 8 | if (fs.existsSync(fullPath)) { 9 | return fullPath 10 | } 11 | } 12 | // split out 13 | let folders = currentPath.split(path.sep) 14 | folders.pop() 15 | 16 | if (folders.length === 0) { 17 | return 18 | } 19 | 20 | currentPath = folders.join(path.sep) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/tags/full-path/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { Icon } from 'antd' 5 | 6 | export function FullPath ({ path, rootIcon = 'filter', rootIconTheme = '' }) { 7 | return ( 8 |
9 | {path.split('.').map((part, index) => ( 10 |
11 | {index > 0 ? : } 12 | 0 ? 10 : 5 }}>{index > 0 ? '.' : ''}{part} 13 |
14 | ))} 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /packages/insights/bin/insights-createsecret: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var program = require('commander') 5 | var root = path.join(__dirname, '..') 6 | var pkg = require(path.join(root, 'package.json')) 7 | 8 | program.version(pkg.version) 9 | .usage('[options]') 10 | .description('Create a new authentication secret') 11 | .option('--secret [path]', 'Where to store the authentication secret. Defaults to ~/.insights/secret') 12 | .parse(process.argv) 13 | 14 | const secretPath = program.secret || path.join(require('os').homedir(), '.insights', 'secret') 15 | 16 | const createSecret = require('../app/create-secret') 17 | createSecret(secretPath) 18 | 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mariusandra 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "extends": ["standard", "standard-react"], 8 | "parser": "babel-eslint", 9 | "rules": { 10 | "indent": [ 11 | "error", 12 | 2 13 | ], 14 | "linebreak-style": [ 15 | "error", 16 | "unix" 17 | ], 18 | "quotes": [ 19 | "error", 20 | "single" 21 | ], 22 | "react/jsx-uses-react": 2, 23 | "react/jsx-uses-vars": 2, 24 | "react/jsx-indent-props": 0, 25 | "react/prop-types": 0, 26 | "react/react-in-jsx-scope": 2, 27 | "arrow-parens": 0, 28 | "no-debugger": "off" 29 | }, 30 | "plugins": [ 31 | "react" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/popups/delete.js: -------------------------------------------------------------------------------- 1 | import Popup from 'react-popup' 2 | 3 | export default function deletePopup (text) { 4 | return new Promise((resolve, reject) => { 5 | Popup.close() 6 | Popup.create({ 7 | title: null, 8 | content: text, 9 | buttons: { 10 | left: [{ 11 | text: 'Cancel', 12 | className: '', 13 | action: () => { 14 | Popup.close() 15 | } 16 | }], 17 | right: [{ 18 | text: 'Delete', 19 | className: 'danger', 20 | action: () => { 21 | resolve(true) 22 | Popup.close() 23 | } 24 | }] 25 | } 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/styles.scss: -------------------------------------------------------------------------------- 1 | .connection-buttons { 2 | width: calc(100% + 1px); 3 | 4 | &.only-database button { 5 | width: 100%; 6 | } 7 | button { 8 | width: 50%; 9 | overflow: hidden; 10 | text-align: left; 11 | display: inline-flex; 12 | align-items: center; 13 | transition: none !important; 14 | i { 15 | margin-top: 1px; 16 | &.arrow { 17 | margin-top: 2px; 18 | } 19 | } 20 | span { 21 | flex: 1; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | } 25 | } 26 | } 27 | 28 | .connection-menu-header-title .ant-dropdown-menu-item-group-title { 29 | color: rgba(0, 0, 0, 0.65); 30 | } 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | data/ 31 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/explorer/get-meta.js: -------------------------------------------------------------------------------- 1 | export default function getMeta (column, structure) { 2 | const [ path ] = column.split('!') 3 | 4 | return path.split('.').reduce((lastStructure, key) => { 5 | if (lastStructure) { 6 | const { columns, custom, links } = lastStructure 7 | 8 | const meta = columns[key] || custom[key] 9 | if (meta) { 10 | return meta 11 | } 12 | 13 | const link = (links.incoming && links.outgoing) ? (links.incoming[key] || links.outgoing[key]) : links[key] 14 | if (link) { 15 | return structure[link.model] 16 | } 17 | } else if (structure[key]) { 18 | return structure[key] 19 | } 20 | 21 | return null 22 | }, null) 23 | } 24 | -------------------------------------------------------------------------------- /packages/insights/bin/insights: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var program = require('commander') 5 | var root = path.join(__dirname, '..') 6 | var pkg = require(path.join(root, 'package.json')) 7 | 8 | program.version(pkg.version) 9 | .usage(' [options]') 10 | .description('Run insights server or helper commands') 11 | 12 | program 13 | .command('init', 'init the .insights folder') 14 | .command('start', 'start insights on port 8000') 15 | .command('createsecret', 'create a new authentication secret') 16 | .command('createsuperuser', 'create a new admin user') 17 | .command('desktop', 'start desktop app (run `npm install -g insights-desktop` first)') 18 | 19 | program.parse(process.argv) 20 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/views/views.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | export default { 7 | before: { 8 | all: [ authenticate('jwt') ], 9 | find: [], 10 | get: [], 11 | create: [], 12 | update: [], 13 | patch: [], 14 | remove: [] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [], 20 | get: [], 21 | create: [], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/results/results.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | export default { 7 | before: { 8 | all: [ authenticate('jwt') ], 9 | find: [], 10 | get: [], 11 | create: [], 12 | update: [], 13 | patch: [], 14 | remove: [] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [], 20 | get: [], 21 | create: [], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/insights-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Insights 15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/_layout/styles.scss: -------------------------------------------------------------------------------- 1 | @import './styles/spinner.scss'; 2 | 3 | .insights-layout { 4 | height: 100vh; 5 | 6 | .insights-content-wrapper { 7 | height: calc(100vh - 50px); 8 | overflow: scroll; 9 | } 10 | } 11 | 12 | .insights-tab-row { 13 | height: 40px; 14 | background: #eee; 15 | display: flex; 16 | padding-left: 10px; 17 | align-items: center; 18 | 19 | div.tab-row-element { 20 | padding-right: 10px; 21 | white-space: nowrap; 22 | button { 23 | margin-bottom: 0; 24 | } 25 | input { 26 | box-sizing: border-box; 27 | display: inline-block; 28 | margin: 0; 29 | } 30 | } 31 | div.tab-row-separator { 32 | flex: 1; 33 | } 34 | } 35 | 36 | button.fa { 37 | padding: 8px 10px; 38 | } 39 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/favourites/favourites.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | export default { 7 | before: { 8 | all: [ authenticate('jwt') ], 9 | find: [], 10 | get: [], 11 | create: [], 12 | update: [], 13 | patch: [], 14 | remove: [] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [], 20 | get: [], 21 | create: [], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/structure/structure.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | export default { 7 | before: { 8 | all: [ authenticate('jwt') ], 9 | find: [], 10 | get: [], 11 | create: [], 12 | update: [], 13 | patch: [], 14 | remove: [] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [], 20 | get: [], 21 | create: [], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/insights/bin/insights-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const program = require('commander') 6 | const root = path.join(__dirname, '..') 7 | const pkg = require(path.join(root, 'package.json')) 8 | 9 | const initInsights = require('../app/init') 10 | 11 | program.version(pkg.version) 12 | .usage(' [options]') 13 | .description('Initialize config to start insights by answering a few questions') 14 | .option('--dev', 'Include development.json in ".insights"') 15 | .option('--login', 'Init insights with support for user accounts') 16 | .option('--no-login', 'Init insights without requiring a user to login with') 17 | 18 | program.parse(process.argv) 19 | 20 | initInsights({ dev: program.dev, login: program.login }) 21 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/connection-test/connection-test.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | export default { 7 | before: { 8 | all: [ authenticate('jwt') ], 9 | find: [], 10 | get: [], 11 | create: [], 12 | update: [], 13 | patch: [], 14 | remove: [] 15 | }, 16 | 17 | after: { 18 | all: [], 19 | find: [], 20 | get: [], 21 | create: [], 22 | update: [], 23 | patch: [], 24 | remove: [] 25 | }, 26 | 27 | error: { 28 | all: [], 29 | find: [], 30 | get: [], 31 | create: [], 32 | update: [], 33 | patch: [], 34 | remove: [] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/client.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | import io from 'socket.io-client' 3 | import feathers from '@feathersjs/client' 4 | 5 | const client = feathers() 6 | 7 | // defaults 8 | // eslint-disable-next-line 9 | const { publicUrl, apiPath, socketPath } = window.__INSIGHTS_CONFIG__ || { 10 | publicUrl: 'http://localhost:3030', 11 | apiPath: '/', 12 | socketPath: '/socket.io' 13 | } 14 | 15 | // socket.io 16 | const socket = io(publicUrl, { path: socketPath }) 17 | client.configure(feathers.socketio(socket, { timeout: 600000 })) 18 | 19 | // REST api 20 | // client.configure(feathers.rest(`${publicUrl}${apiPath === '/' ? '' : apiPath}`).fetch(window.fetch)) 21 | 22 | // authentication 23 | client.configure(feathers.authentication({ 24 | storage: window.localStorage 25 | })) 26 | 27 | export default client 28 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 1. Make /url/:code paths work 2 | 3 | ```js 4 | app.get('/url/:code', async function (req, res) { 5 | const results = await service.find({ query: { code: req.params.code } }) 6 | 7 | if (results.total > 0) { 8 | res.redirect(results.data[0].path) 9 | } else { 10 | res.send('

Short URL not found!

') 11 | } 12 | }) 13 | ``` 14 | 15 | 2. Check that you can't just CREATE users on the service 16 | 17 | 3. Make it possible to run it like so: 18 | 19 | `npx insights start` 20 | 21 | It will ask you questions if it can't find its own config data 22 | 23 | It will create a ".insights/config.yml" to store its config if none found. 24 | 25 | .insights 26 | insights 27 | config/insights 28 | 29 | This will contain the ENV for insights like DB paths. 30 | 31 | Also optional schema files (current insights.yml): 32 | 33 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/subset/form/models/model-row/index.js: -------------------------------------------------------------------------------- 1 | import { Tag } from 'antd' 2 | import React from 'react' 3 | 4 | export default function ModelRow ({ model, ignoredColumnCount, addedColumnCount, editedColumnCount }) { 5 | return ( 6 | 7 | {model}{' '} 8 | 9 | {ignoredColumnCount > 0 ? ( 10 | {ignoredColumnCount} field{ignoredColumnCount === 1 ? '' : 's'} ignored 11 | ) : null} 12 | 13 | {addedColumnCount > 0 ? ( 14 | {addedColumnCount} field{addedColumnCount === 1 ? '' : 's'} added 15 | ) : null} 16 | 17 | {editedColumnCount > 0 ? ( 18 | {editedColumnCount} field{editedColumnCount === 1 ? '' : 's'} edited 19 | ) : null} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/header/views/styles.scss: -------------------------------------------------------------------------------- 1 | .views-menu { 2 | width: 220px; 3 | font-size: 14px; 4 | div.buttons { 5 | height: 36px; 6 | line-height: 30px; 7 | span.open-new { 8 | text-decoration: underline; 9 | cursor: pointer; 10 | } 11 | } 12 | div.cannot-save-notice { 13 | height: 36px; 14 | line-height: 30px; 15 | color: #888; 16 | font-weight: 100; 17 | } 18 | form { 19 | .input-text.full { 20 | width: 150px; 21 | display: inline-block; 22 | } 23 | button.save { 24 | width: 30px; 25 | padding: 5px 0; 26 | } 27 | button.cancel { 28 | width: 30px; 29 | padding: 5px 0; 30 | background: #880000; 31 | } 32 | } 33 | .list { 34 | .list-item { 35 | cursor: pointer; 36 | &:hover { 37 | background: #eee; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/login/styles.scss: -------------------------------------------------------------------------------- 1 | .login-scene { 2 | padding-top: calc(46vh - 140px); 3 | height: 100%; 4 | background: url('https://images.unsplash.com/photo-1579961611811-54b643ddbd91?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=3450&q=80') #162c41 center center; 5 | background-size: cover; 6 | 7 | .ant-card { 8 | margin: 0 auto; 9 | .has-error { 10 | .ant-form-extra { 11 | color: #f5222d; 12 | } 13 | } 14 | } 15 | 16 | .logo { 17 | font-size: 32px; 18 | font-family: serif; 19 | margin-bottom: 20px; 20 | text-transform: uppercase; 21 | text-align: center; 22 | 23 | img { 24 | width: 32px; 25 | height: 32px; 26 | vertical-align: text-top; 27 | margin-right: 6px 28 | } 29 | } 30 | 31 | .login-box { 32 | .login-form-button { 33 | width: 100%; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/insights-web/public/insights.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/subset/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useValues } from 'kea' 3 | import { Button, Dropdown, Icon } from 'antd' 4 | 5 | import SubsetForm from 'scenes/explorer/connection/subset/form' 6 | import SubsetMenu from './menu' 7 | 8 | import connectionsLogic from 'scenes/explorer/connection/logic' 9 | 10 | export default function Subset ({ children }) { 11 | const { isLoadingSubsets, selectedSubset } = useValues(connectionsLogic) 12 | 13 | return ( 14 | <> 15 | } trigger={['click']}> 16 | {children || ( 17 | 22 | )} 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/routes.js: -------------------------------------------------------------------------------- 1 | export function combineScenesAndRoutes (scenes, routes) { 2 | let combined = {} 3 | 4 | Object.keys(routes).forEach(route => { 5 | if (scenes[routes[route]]) { 6 | combined[route] = scenes[routes[route]] 7 | } else { 8 | console.error(`[KEA-LOGIC] scene ${routes[route]} not found in scenes object (route: ${route})`) 9 | } 10 | }) 11 | 12 | return combined 13 | } 14 | 15 | const scenes = { 16 | explorer: require('./explorer').default, 17 | login: require('./login').default, 18 | users: require('./users').default, 19 | settings: require('./settings').default, 20 | urls: require('./urls').default 21 | } 22 | 23 | const routes = { 24 | '/explorer': 'explorer', 25 | '/': 'explorer', 26 | '/login': 'login', 27 | '/users': 'users', 28 | '/settings': 'settings', 29 | '/url/:url': 'urls' 30 | } 31 | 32 | export default combineScenesAndRoutes(scenes, routes) 33 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/results/results.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `results` service on path `/results` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Results } from './results.class'; 5 | import hooks from './results.hooks'; 6 | 7 | // Add this service to the service type index 8 | declare module '../../declarations' { 9 | interface ServiceTypes { 10 | 'results': Results & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application) { 15 | 16 | const paginate = app.get('paginate'); 17 | 18 | const options = { 19 | paginate 20 | }; 21 | 22 | // Initialize our service with any options it requires 23 | app.use('/results', new Results(options, app)); 24 | 25 | // Get our initialized service so that we can register hooks 26 | const service = app.service('results'); 27 | 28 | service.hooks(hooks); 29 | } 30 | -------------------------------------------------------------------------------- /packages/insights/app/templates/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "paginate": { 3 | "default": 10, 4 | "max": 50 5 | }, 6 | "authentication": { 7 | "entity": "user", 8 | "service": "users", 9 | "jwtOptions": { 10 | "header": { 11 | "typ": "access" 12 | }, 13 | "audience": "https://yourdomain.com", 14 | "issuer": "feathers", 15 | "algorithm": "HS256", 16 | "expiresIn": "1d" 17 | }, 18 | "local": { 19 | "usernameField": "email", 20 | "passwordField": "password" 21 | }, 22 | "oauth": { 23 | "redirect": "http://localhost:3000/", 24 | "google": { 25 | "key": "552189764324-l79gr3769a2fgck31nfbfpm6fuijv1ng.apps.googleusercontent.com", 26 | "secret": "LVhqLkZBkHFuSrdqdEwQ6oUm", 27 | "scope": [ 28 | "openid", 29 | "email" 30 | ], 31 | "nonce": true 32 | } 33 | } 34 | }, 35 | "nedb": "./data" 36 | } 37 | -------------------------------------------------------------------------------- /scripts/sync-versions.js: -------------------------------------------------------------------------------- 1 | const insightsVersion = require('../package.json').version 2 | const semver = require('semver') 3 | 4 | console.log(insightsVersion) 5 | 6 | const jsons = { 7 | 'insights': require('../packages/insights/package.json'), 8 | 'insights-core': require('../packages/insights-core/package.json'), 9 | 'insights-server': require('../packages/insights-server/package.json') 10 | } 11 | 12 | let error = false 13 | 14 | // check if we can update 15 | Object.keys(jsons).forEach(key => { 16 | console.log(`${key} version ${jsons[key].version}`) 17 | 18 | if (semver.lt(jsons[key].version, insightsVersion)) { 19 | console.log(`-> can update to ${insightsVersion}`) 20 | } else { 21 | error = true 22 | console.error(`[ERROR] must be less than ${insightsVersion}! Exiting!`) 23 | } 24 | }) 25 | 26 | if (error) { 27 | process.exit(1) 28 | } 29 | 30 | // do the sync 31 | Object.keys(jsons).forEach(key => { 32 | 33 | }) 34 | -------------------------------------------------------------------------------- /packages/insights-api/src/app.hooks.ts: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | import { HookContext } from '@feathersjs/feathers' 4 | 5 | const setTimestamp = (name: string) => { 6 | return async (context: HookContext) => { 7 | context.data[name] = new Date() 8 | return context 9 | } 10 | } 11 | 12 | export default { 13 | before: { 14 | all: [], 15 | find: [], 16 | get: [], 17 | create: [ setTimestamp('createdAt') ], 18 | update: [ setTimestamp('updatedAt') ], 19 | patch: [], 20 | remove: [] 21 | }, 22 | 23 | after: { 24 | all: [], 25 | find: [], 26 | get: [], 27 | create: [], 28 | update: [], 29 | patch: [], 30 | remove: [] 31 | }, 32 | 33 | error: { 34 | all: [], 35 | find: [], 36 | get: [], 37 | create: [], 38 | update: [], 39 | patch: [], 40 | remove: [] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/structure/structure.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `structure` service on path `/structure` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Structure } from './structure.class'; 5 | import hooks from './structure.hooks'; 6 | 7 | // Add this service to the service type index 8 | declare module '../../declarations' { 9 | interface ServiceTypes { 10 | 'structure': Structure & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application) { 15 | 16 | const paginate = app.get('paginate'); 17 | 18 | const options = { 19 | paginate 20 | }; 21 | 22 | // Initialize our service with any options it requires 23 | app.use('/structure', new Structure(options, app)); 24 | 25 | // Get our initialized service so that we can register hooks 26 | const service = app.service('structure'); 27 | 28 | service.hooks(hooks); 29 | } 30 | -------------------------------------------------------------------------------- /packages/insights/app/create-secret.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var randomString = require('./lib/random-string') 4 | 5 | module.exports = function createSecret(secretPath) { 6 | 7 | if (!secretPath) { 8 | console.error(`!! Fatal Error! No secretPath given!`) 9 | process.exit(1) 10 | } 11 | 12 | let folder = secretPath.split(path.sep) 13 | folder.pop() 14 | const folderPath = folder.join(path.sep) 15 | 16 | try { 17 | fs.mkdirSync(folderPath, {recursive: true}) 18 | } catch (error) { 19 | console.error(`!! Fatal Error! Could not create folder: ${folderPath}`) 20 | process.exit(1) 21 | } 22 | 23 | try { 24 | fs.writeFileSync(secretPath, randomString(64), 'utf8') 25 | console.log(`New authentication secret stored at: ${secretPath}`) 26 | } catch (error) { 27 | console.error(`!! Fatal Error! Could not write authentication secret at ${secretPath}`) 28 | process.exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/filter/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { useValues } from 'kea' 5 | 6 | import { Icon } from 'antd' 7 | 8 | import OneFilter from './one-filter' 9 | 10 | import explorerLogic from 'scenes/explorer/logic' 11 | 12 | export default function Filter () { 13 | const { filter } = useValues(explorerLogic) 14 | 15 | let i = 0 16 | 17 | return ( 18 |
19 |
20 | 21 | {filter.map(({ key, value }) => ( 22 | 23 | ))} 24 | {filter.length === 0 ? ( 25 | Click on icons to add filters 26 | ) : null} 27 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | import users from './users/users.service'; 3 | import connections from './connections/connections.service'; 4 | import views from './views/views.service'; 5 | import urls from './urls/urls.service'; 6 | import favourites from './favourites/favourites.service'; 7 | import connectionTest from './connection-test/connection-test.service'; 8 | import results from './results/results.service'; 9 | import structure from './structure/structure.service'; 10 | import subsets from './subsets/subsets.service'; 11 | // Don't remove this comment. It's needed to format import lines nicely. 12 | 13 | export default function (app: Application) { 14 | app.configure(users); 15 | app.configure(connections); 16 | app.configure(views); 17 | app.configure(urls); 18 | app.configure(favourites); 19 | app.configure(connectionTest); 20 | app.configure(results); 21 | app.configure(structure); 22 | app.configure(subsets); 23 | } 24 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/_layout/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | 5 | import { useValues } from 'kea' 6 | 7 | import Header from 'scenes/header' 8 | import Spinner from 'lib/tags/spinner' 9 | 10 | import Login from '../login' 11 | 12 | import authLogic from 'scenes/auth' 13 | 14 | export default function InsightsLayout ({ children }) { 15 | const { showLogin, showApp } = useValues(authLogic) 16 | 17 | if (!showLogin && !showApp) { 18 | return
19 | } 20 | 21 | if (showLogin || window.location.search.indexOf('embed=true') >= 0) { 22 | return ( 23 |
24 | {showLogin ? : children} 25 |
26 | ) 27 | } 28 | 29 | return ( 30 |
31 |
32 |
33 | {children} 34 |
35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/subsets/subsets.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `subsets` service on path `/subsets` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Subsets } from './subsets.class'; 5 | import createModel from '../../models/subsets.model'; 6 | import hooks from './subsets.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'subsets': Subsets & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const options = { 17 | Model: createModel(app), 18 | paginate: false, 19 | multi: ['remove'] 20 | }; 21 | 22 | // Initialize our service with any options it requires 23 | app.use('/subsets', new Subsets(options, app)); 24 | 25 | // Get our initialized service so that we can register hooks 26 | const service = app.service('subsets'); 27 | 28 | service.hooks(hooks); 29 | } 30 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/urls/urls.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `urls` service on path `/urls` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Urls } from './urls.class'; 5 | import createModel from '../../models/urls.model'; 6 | import hooks from './urls.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'urls': Urls & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = app.get('paginate'); 18 | 19 | const options = { 20 | Model, 21 | paginate 22 | }; 23 | 24 | // Initialize our service with any options it requires 25 | app.use('/urls', new Urls(options, app)); 26 | 27 | // Get our initialized service so that we can register hooks 28 | const service = app.service('urls'); 29 | 30 | service.hooks(hooks); 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-api/test/authentication.test.ts: -------------------------------------------------------------------------------- 1 | import app from '../src/app'; 2 | 3 | describe('authentication', () => { 4 | it('registered the authentication service', () => { 5 | expect(app.service('authentication')).toBeTruthy(); 6 | }); 7 | 8 | describe('local strategy', () => { 9 | const userInfo = { 10 | email: 'someone@example.com', 11 | password: 'supersecret' 12 | }; 13 | 14 | beforeAll(async () => { 15 | try { 16 | await app.service('users').create(userInfo); 17 | } catch (error) { 18 | // Do nothing, it just means the user already exists and can be tested 19 | } 20 | }); 21 | 22 | it('authenticates user and creates accessToken', async () => { 23 | const { user, accessToken } = await app.service('authentication').create({ 24 | strategy: 'local', 25 | ...userInfo 26 | }, {}); 27 | 28 | expect(accessToken).toBeTruthy(); 29 | expect(user).toBeTruthy(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/database/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useValues } from 'kea' 3 | import { Button, Icon, Dropdown } from "antd" 4 | 5 | import DatabaseForm from './form' 6 | import ConnectionMenu from './menu' 7 | 8 | import connectionsLogic from '../logic' 9 | 10 | export default function Database ({ children }) { 11 | const { selectedConnection, isLoadingConnections } = useValues(connectionsLogic) 12 | 13 | return ( 14 | <> 15 | } trigger={['click']}> 16 | {children || ( 17 | 22 | )} 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/insights-api/src/insights/adapter/sql/sqlite.ts: -------------------------------------------------------------------------------- 1 | import { ColumnType, TruncationType } from '../../definitions.d' 2 | 3 | import SQL from './index' 4 | 5 | export default class SQLite extends SQL { 6 | allowedDateTruncations () { 7 | return ['hour', 'day', 'month', 'year'] 8 | } 9 | 10 | truncateDate (sql: string, truncation: TruncationType) { 11 | if (!this.allowedDateTruncations().includes(truncation)) { 12 | throw new Error(`Bad date truncation '${truncation}'`) 13 | } 14 | 15 | const dateSql = super.truncateDate(sql, truncation) 16 | 17 | if (truncation === 'day') { 18 | return `date(${dateSql})` 19 | } else { 20 | return `datetime(${dateSql}, 'start of ${truncation}')` 21 | } 22 | } 23 | 24 | filterEquals (sql: string, type: ColumnType, string: string) { 25 | if (type === 'boolean') { 26 | return `(${sql}) = ${this.quote(string === 'true' ? 't' : 'f')}` 27 | } else { 28 | return `(${sql}) = ${this.quote(string)}` 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/users/users.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `users` service on path `/users` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Users } from './users.class'; 5 | import createModel from '../../models/users.model'; 6 | import hooks from './users.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'users': Users & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = app.get('paginate'); 18 | 19 | const options = { 20 | Model, 21 | paginate 22 | }; 23 | 24 | // Initialize our service with any options it requires 25 | app.use('/users', new Users(options, app)); 26 | 27 | // Get our initialized service so that we can register hooks 28 | const service = app.service('users'); 29 | 30 | service.hooks(hooks); 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/connection-test/connection-test.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `connection-test` service on path `/connection-test` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { ConnectionTest } from './connection-test.class'; 5 | import hooks from './connection-test.hooks'; 6 | 7 | // Add this service to the service type index 8 | declare module '../../declarations' { 9 | interface ServiceTypes { 10 | 'connection-test': ConnectionTest & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function (app: Application) { 15 | 16 | const paginate = app.get('paginate'); 17 | 18 | const options = { 19 | paginate 20 | }; 21 | 22 | // Initialize our service with any options it requires 23 | app.use('/connection-test', new ConnectionTest(options, app)); 24 | 25 | // Get our initialized service so that we can register hooks 26 | const service = app.service('connection-test'); 27 | 28 | service.hooks(hooks); 29 | } 30 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/views/views.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `views` service on path `/views` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Views } from './views.class'; 5 | import createModel from '../../models/views.model'; 6 | import hooks from './views.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'views': Views & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = app.get('paginate'); 18 | 19 | const options = { 20 | Model, 21 | paginate, 22 | multi: ['remove'] 23 | }; 24 | 25 | // Initialize our service with any options it requires 26 | app.use('/views', new Views(options, app)); 27 | 28 | // Get our initialized service so that we can register hooks 29 | const service = app.service('views'); 30 | 31 | service.hooks(hooks); 32 | } 33 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/subsets/subsets.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | const removeAllViews = async context => { 7 | const { app, result: subset } = context 8 | 9 | const viewsService = app.service('views') 10 | await viewsService.remove(null, { query: { subsetId: subset._id } }) 11 | 12 | return context 13 | } 14 | 15 | export default { 16 | before: { 17 | all: [ authenticate('jwt') ], 18 | find: [], 19 | get: [], 20 | create: [], 21 | update: [], 22 | patch: [], 23 | remove: [] 24 | }, 25 | 26 | after: { 27 | all: [], 28 | find: [], 29 | get: [], 30 | create: [], 31 | update: [], 32 | patch: [], 33 | remove: [ removeAllViews ] 34 | }, 35 | 36 | error: { 37 | all: [], 38 | find: [], 39 | get: [], 40 | create: [], 41 | update: [], 42 | patch: [], 43 | remove: [] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/connections/connections.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `connections` service on path `/connections` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Connections } from './connections.class'; 5 | import createModel from '../../models/connections.model'; 6 | import hooks from './connections.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'connections': Connections & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = false; 18 | 19 | const options = { 20 | Model, 21 | paginate 22 | }; 23 | 24 | // Initialize our service with any options it requires 25 | app.use('/connections', new Connections(options, app)); 26 | 27 | // Get our initialized service so that we can register hooks 28 | const service = app.service('connections'); 29 | 30 | service.hooks(hooks); 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/favourites/favourites.service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `favourites` service on path `/favourites` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import { Favourites } from './favourites.class'; 5 | import createModel from '../../models/favourites.model'; 6 | import hooks from './favourites.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '../../declarations' { 10 | interface ServiceTypes { 11 | 'favourites': Favourites & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application) { 16 | const Model = createModel(app); 17 | const paginate = app.get('paginate'); 18 | 19 | const options = { 20 | Model, 21 | paginate 22 | }; 23 | 24 | // Initialize our service with any options it requires 25 | app.use('/favourites', new Favourites(options, app)); 26 | 27 | // Get our initialized service so that we can register hooks 28 | const service = app.service('favourites'); 29 | 30 | service.hooks(hooks); 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/filter/column-filters.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Icon } from 'antd' 3 | import OneFilter from 'scenes/explorer/filter/one-filter' 4 | 5 | export function ColumnFilters ({ filter, path, onAddClick, filterPrefixString = 'tree' }) { 6 | let i = 0 7 | let hasFilters = false 8 | 9 | return ( 10 |
11 | {filter.map(({key, value}) => { 12 | if (key !== path) { 13 | i += 1 14 | return null 15 | } 16 | hasFilters = true 17 | return ( 18 |
19 | 20 |
21 | ) 22 | }).filter(v => v)} 23 | 24 |
25 | 28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/filter/styles.scss: -------------------------------------------------------------------------------- 1 | .insights-filter { 2 | .filter-header { 3 | line-height: 30px; 4 | box-sizing: border-box; 5 | vertical-align: middle; 6 | display: flex; 7 | 8 | .filter-preview { 9 | vertical-align: middle; 10 | } 11 | } 12 | 13 | .filter-placeholder { 14 | color: #aaa; 15 | .anticon { 16 | color: #999; 17 | } 18 | } 19 | } 20 | 21 | .filter-preview-element { 22 | vertical-align: middle; 23 | background: #eee; 24 | margin-right: 8px; 25 | height: 23px; 26 | line-height: 23px; 27 | padding: 0 5px; 28 | display: inline-block; 29 | border: 1px solid #ddd; 30 | } 31 | 32 | .insights-filter-title { 33 | min-width: 250px; 34 | display: block; 35 | } 36 | 37 | .filter-popover-delete { 38 | float: right; 39 | line-height: 20px; 40 | height: 20px; 41 | margin-left: 12px; 42 | padding: 0; 43 | } 44 | 45 | .filter-radio-popup { 46 | display: block; 47 | height: 30px; 48 | line-height: 30px; 49 | } 50 | 51 | .filter-tag { 52 | cursor: pointer; 53 | &:hover { 54 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/utils/script.js: -------------------------------------------------------------------------------- 1 | let promises = {} 2 | let loaded = {} 3 | 4 | export default function script (url) { 5 | if (Array.isArray(url)) { 6 | var prom = [] 7 | url.forEach(item => { 8 | prom.push(script(item)) 9 | }) 10 | return Promise.all(prom) 11 | } 12 | 13 | if (promises[url]) { 14 | return promises[url] 15 | } else if (loaded[url]) { 16 | return Promise.resolve() 17 | } else { 18 | promises[url] = new Promise(function (resolve, reject) { 19 | var r = false 20 | var t = document.getElementsByTagName('script')[0] 21 | var s = document.createElement('script') 22 | 23 | s.type = 'text/javascript' 24 | s.src = url 25 | s.async = true 26 | s.onload = s.onreadystatechange = function () { 27 | if (!r && (!this.readyState || this.readyState === 'complete')) { 28 | r = true 29 | promises[url] = false 30 | loaded[url] = true 31 | resolve(this) 32 | } 33 | } 34 | s.onerror = s.onabort = reject 35 | t.parentNode.insertBefore(s, t) 36 | }) 37 | return promises[url] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/tags/submit-button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Spinner from 'lib/tags/spinner' 5 | 6 | // NB: Settings isDisabled = true will just change the color of the button. 7 | // It will still have an onClick, as this is expected to be delegated to redux-form which will show validation errors. 8 | 9 | export default class SubmitButton extends Component { 10 | static propTypes = { 11 | isSubmitting: PropTypes.bool, 12 | isDisabled: PropTypes.bool, 13 | className: PropTypes.string, 14 | spinnerClass: PropTypes.string, 15 | onClick: PropTypes.func, 16 | children: PropTypes.node 17 | } 18 | 19 | render () { 20 | const { isSubmitting, isDisabled, children, className, spinnerClass, onClick, ...other } = this.props 21 | 22 | return ( 23 | 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/tags/spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class Spinner extends React.Component { 5 | static propTypes = { 6 | color: PropTypes.string 7 | }; 8 | 9 | static defaultProps = { 10 | color: 'gray' 11 | }; 12 | 13 | render () { 14 | return ( 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/graph/time-group-select.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { useActions, useValues } from 'kea' 5 | import { Select } from 'antd' 6 | 7 | import explorerLogic from 'scenes/explorer/logic' 8 | 9 | export default function TimeGroupSelect () { 10 | const { graphTimeGroup } = useValues(explorerLogic) 11 | const { setGraphTimeGroup, setGraphControls } = useActions(explorerLogic) 12 | 13 | function setGraphTimeGroupLocal (graphTimeGroup) { 14 | setGraphTimeGroup(graphTimeGroup) 15 | setGraphControls({ compareWith: 0 }) 16 | } 17 | 18 | return ( 19 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/urls/urls.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | import { HookContext } from '@feathersjs/feathers' 3 | import randomString from '../../utils/random-string' 4 | // Don't remove this comment. It's needed to format import lines nicely. 5 | 6 | const { authenticate } = authentication.hooks; 7 | 8 | // Use this hook to manipulate incoming or outgoing data. 9 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 10 | 11 | const urlRandomCode = () => { 12 | return async (context: HookContext) => { 13 | context.data.code = randomString(10) 14 | return context 15 | } 16 | } 17 | 18 | export default { 19 | before: { 20 | all: [ authenticate('jwt') ], 21 | find: [], 22 | get: [], 23 | create: [urlRandomCode()], 24 | update: [], 25 | patch: [], 26 | remove: [] 27 | }, 28 | 29 | after: { 30 | all: [], 31 | find: [], 32 | get: [], 33 | create: [], 34 | update: [], 35 | patch: [], 36 | remove: [] 37 | }, 38 | 39 | error: { 40 | all: [], 41 | find: [], 42 | get: [], 43 | create: [], 44 | update: [], 45 | patch: [], 46 | remove: [] 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/users/users.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as feathersAuthentication from '@feathersjs/authentication'; 2 | import * as local from '@feathersjs/authentication-local'; 3 | // Don't remove this comment. It's needed to format import lines nicely. 4 | 5 | const { authenticate } = feathersAuthentication.hooks; 6 | const { hashPassword, protect } = local.hooks; 7 | 8 | export default { 9 | before: { 10 | all: [], 11 | find: [ authenticate('jwt') ], 12 | get: [ authenticate('jwt') ], 13 | create: [ hashPassword('password') ], 14 | update: [ hashPassword('password'), authenticate('jwt') ], 15 | patch: [ hashPassword('password'), authenticate('jwt') ], 16 | remove: [ authenticate('jwt') ] 17 | }, 18 | 19 | after: { 20 | all: [ 21 | // Make sure the password field is never sent to the client 22 | // Always must be the last hook 23 | protect('password') 24 | ], 25 | find: [], 26 | get: [], 27 | create: [], 28 | update: [], 29 | patch: [], 30 | remove: [] 31 | }, 32 | 33 | error: { 34 | all: [], 35 | find: [], 36 | get: [], 37 | create: [], 38 | update: [], 39 | patch: [], 40 | remove: [] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/sidebar/models/logic.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | 3 | import explorerLogic from '../../logic' 4 | import urlToState from '../../../../lib/explorer/url-to-state' 5 | 6 | export default kea({ 7 | connect: { 8 | values: [ 9 | explorerLogic, ['subsetPinnedFields', 'subsetViews'] 10 | ] 11 | }, 12 | 13 | selectors: ({ selectors }) => ({ 14 | pinnedPerModel: [ 15 | () => [selectors.subsetPinnedFields], 16 | (subsetPinnedFields) => { 17 | const pinned = {} 18 | subsetPinnedFields.forEach(({ model }) => { 19 | pinned[model] = (pinned[model] || 0) + 1 20 | }) 21 | return pinned 22 | } 23 | ], 24 | viewsPerModel: [ 25 | () => [selectors.subsetViews], 26 | (subsetViews) => { 27 | const views = {} 28 | subsetViews.forEach((view) => { 29 | const state = urlToState(view.path) 30 | const [column, ] = state.columns 31 | 32 | const model = (column || '').split('!')[0].split('.')[0] 33 | if (model) { 34 | views[model] = (views[model] || 0) + 1 35 | } 36 | }) 37 | return views 38 | } 39 | ] 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/subsets/subsets.class.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '../../declarations'; 3 | import {FieldType, StructureColumn, StructureCustom, StructureLink} from "../../insights/definitions"; 4 | 5 | export interface SubsetData { 6 | _id?: string, 7 | connectionId: string, 8 | type: 'all_data' | 'custom', 9 | name: string, 10 | addNewModels: boolean, 11 | addNewFields: boolean, 12 | selection?: { 13 | [key: string]: false | { [key: string]: boolean } 14 | }, 15 | newFields?: { 16 | [key: string]: { 17 | [key: string]: { 18 | key: string, 19 | type: FieldType, 20 | meta: StructureColumn | StructureCustom | StructureLink 21 | } 22 | } 23 | }, 24 | editedFields: { 25 | [key: string]: { 26 | [key: string]: { 27 | key: string, 28 | originalKey: string, 29 | type: FieldType, 30 | meta: StructureColumn | StructureCustom | StructureLink 31 | } 32 | } 33 | } 34 | } 35 | 36 | export class Subsets extends Service { 37 | constructor (options: Partial, app: Application) { 38 | super(options); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/graph/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React, { Component } from 'react' 4 | import { useValues } from 'kea' 5 | 6 | import Dimensions from 'react-dimensions' 7 | import { Graph } from 'insights-charts' 8 | 9 | import CompareWith from './compare-with' 10 | import ControlsRight from './controls-right' 11 | 12 | import explorerLogic from 'scenes/explorer/logic' 13 | 14 | function GraphView ({ containerHeight }) { 15 | const { graph, graphControls } = useValues(explorerLogic) 16 | 17 | return ( 18 |
19 |
20 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | ) 30 | } 31 | 32 | // Dimensions adds a ref to its children and functional components don't support them 33 | class GraphViewContainer extends Component { 34 | render () { 35 | return 36 | } 37 | } 38 | 39 | export default Dimensions({ elementResize: true })(GraphViewContainer) 40 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/login/logic.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | import PropTypes from 'prop-types' 3 | 4 | export default kea({ 5 | path: () => ['scenes', 'login', 'index'], 6 | 7 | actions: () => ({ 8 | setEmail: email => ({ email }), 9 | setPassword: password => ({ password }), 10 | setErrors: errors => ({ errors }), 11 | 12 | loginRequest: true, 13 | loginSuccess: true, 14 | loginFailure: errors => ({ errors }), 15 | 16 | performLogin: true 17 | }), 18 | 19 | reducers: ({ actions }) => ({ 20 | email: ['', PropTypes.string, { 21 | [actions.setEmail]: (_, payload) => payload.email 22 | }], 23 | password: ['', PropTypes.string, { 24 | [actions.setPassword]: (_, payload) => payload.password 25 | }], 26 | errors: [{}, PropTypes.object, { 27 | [actions.setEmail]: () => ({}), 28 | [actions.setPassword]: () => ({}), 29 | [actions.performLogin]: () => ({}), 30 | [actions.loginFailure]: (_, payload) => payload.errors, 31 | [actions.setErrors]: (_, payload) => payload.errors 32 | }], 33 | isSubmitting: [false, PropTypes.bool, { 34 | [actions.loginRequest]: () => true, 35 | [actions.loginFailure]: () => false 36 | }] 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/users/logic.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | import NProgress from 'nprogress' 3 | 4 | import client from '../../lib/client' 5 | 6 | export default kea({ 7 | actions: () => ({ 8 | getUsers: () => ({}), 9 | gotUsers: (users) => ({ users }), 10 | gotError: true 11 | }), 12 | 13 | reducers: ({ actions }) => ({ 14 | isLoading: [false, { 15 | [actions.getUsers]: () => true, 16 | [actions.gotUsers]: () => false, 17 | [actions.gotError]: () => false 18 | }], 19 | users: [[], { 20 | [actions.gotUsers]: (_, payload) => payload.users 21 | }], 22 | hasError: [false, { 23 | [actions.gotError]: () => true 24 | }] 25 | }), 26 | 27 | events: ({ actions }) => ({ 28 | afterMount: () => { 29 | actions.getUsers() 30 | } 31 | }), 32 | 33 | listeners: ({ actions }) => ({ 34 | [actions.getUsers]: async () => { 35 | NProgress.start() 36 | try { 37 | const usersService = client.service('users') 38 | const usersResponse = await usersService.find() 39 | actions.gotUsers(usersResponse.data) 40 | } catch (error) { 41 | actions.gotError() 42 | } 43 | NProgress.done() 44 | } 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /packages/insights-api/README.md: -------------------------------------------------------------------------------- 1 | # insights-core 2 | 3 | > 4 | 5 | ## About 6 | 7 | This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications. 8 | 9 | ## Getting Started 10 | 11 | Getting up and running is as easy as 1, 2, 3. 12 | 13 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 14 | 2. Install your dependencies 15 | 16 | ``` 17 | cd path/to/insights-core 18 | npm install 19 | ``` 20 | 21 | 3. Start your app 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Testing 28 | 29 | Simply run `npm test` and all your tests in the `test/` directory will be run. 30 | 31 | ## Scaffolding 32 | 33 | Feathers has a powerful command line interface. Here are a few things it can do: 34 | 35 | ``` 36 | $ npm install -g @feathersjs/cli # Install Feathers CLI 37 | 38 | $ feathers generate service # Generate a new Service 39 | $ feathers generate hook # Generate a new Hook 40 | $ feathers help # Show all commands 41 | ``` 42 | 43 | ## Help 44 | 45 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 46 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/connection-test/connection-test.class.ts: -------------------------------------------------------------------------------- 1 | import { Id, NullableId, Paginated, Params, ServiceMethods } from '@feathersjs/feathers'; 2 | import { Application } from '../../declarations'; 3 | import createAdapter from '../../insights/adapter' 4 | import getStructure from '../../insights/structure' 5 | 6 | interface Data {} 7 | 8 | interface ServiceOptions {} 9 | 10 | export class ConnectionTest implements Partial> { 11 | app: Application; 12 | options: ServiceOptions; 13 | 14 | constructor (options: ServiceOptions = {}, app: Application) { 15 | this.options = options; 16 | this.app = app; 17 | } 18 | 19 | async find (params?: Params): Promise { 20 | try { 21 | const { url, structurePath } = params.query 22 | 23 | // check that this doesn't throw up 24 | await createAdapter(url, 60, 'UTC').test() 25 | 26 | // if we want a structure from a file, test that it exists 27 | if (structurePath) { 28 | await getStructure(structurePath) 29 | } 30 | 31 | return Promise.resolve({ 32 | working: true 33 | }) 34 | } catch (e) { 35 | return Promise.resolve({ 36 | working: false, 37 | error: e.message 38 | }) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/sidebar/selected-model/aggregate/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Tag } from 'antd' 3 | 4 | export function AggregateList ({ aggregate, meta, setAggregate, className }) { 5 | if (meta.index === 'primary_key' && aggregate === 'count') { 6 | return null 7 | } 8 | if (meta.index !== 'primary_key' && meta.type !== 'string' && meta.type !== 'number') { 9 | return null 10 | } 11 | 12 | return ( 13 |
14 | {meta.type === 'string' || meta.type === 'number' ? ( 15 | <> 16 | setAggregate('min')} style={{ cursor: 'pointer' }}>Min 17 | setAggregate('max')} style={{ cursor: 'pointer' }}>Max 18 | 19 | ) : null} 20 | {meta.type === 'number' ? ( 21 | <> 22 | setAggregate('avg')} style={{ cursor: 'pointer' }}>Avg 23 | setAggregate('sum')} style={{ cursor: 'pointer' }}>Sum 24 | 25 | ) : null} 26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/explorer/get-sorted-meta.js: -------------------------------------------------------------------------------- 1 | export function getSortedMeta (column, sortedStructureObject) { 2 | if (Object.keys(sortedStructureObject).length === 0) { 3 | return null 4 | } 5 | 6 | const [ path ] = column.split('!') 7 | 8 | let field 9 | let lastModel 10 | let lastModelType 11 | 12 | path.split('.').forEach((pathPart, index) => { 13 | if (!lastModel) { 14 | if (sortedStructureObject[pathPart]) { 15 | lastModelType = pathPart 16 | lastModel = sortedStructureObject[lastModelType] 17 | } else { 18 | throw new Error(`Can not resolve "${pathPart}" in structure`) 19 | } 20 | } else { 21 | if (lastModel[pathPart]) { 22 | if (index === path.split('.').length - 1) { 23 | field = lastModel[pathPart] 24 | } else if (lastModel[pathPart].type === 'link') { 25 | lastModelType = lastModel[pathPart].meta.model 26 | lastModel = sortedStructureObject[lastModelType] 27 | } else { 28 | throw new Error(`Error, link "${pathPart}" in path "${path}" is not of type "link". ${JSON.stringify(lastModel[pathPart])}`) 29 | } 30 | } else { 31 | throw new Error(`Can not resolve link "${pathPart}" in path "${path}"`) 32 | } 33 | } 34 | }) 35 | 36 | return field 37 | } 38 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/time-filter/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useActions, useValues } from 'kea' 3 | import { Select } from 'antd' 4 | 5 | import explorerLogic from 'scenes/explorer/logic' 6 | import moment from 'moment' 7 | 8 | export default function TimeFilter () { 9 | const { graphTimeFilter } = useValues(explorerLogic) 10 | const { setGraphTimeFilter } = useActions(explorerLogic) 11 | 12 | const year = moment().year() 13 | 14 | const graphTimeFilters = [ 15 | ['All time', 'all-time'], 16 | ['Last 2 years', 'last-730'], 17 | ['Last 365 days', 'last-365'], 18 | ['Last 60 days', 'last-60'], 19 | ['Last 30 days', 'last-30'], 20 | ['This month so far', 'this-month-so-far'], 21 | ['This month', 'this-month'], 22 | ['Last month', 'last-month'], 23 | ['Yesterday', 'yesterday'], 24 | ['Today', 'today'] 25 | ] 26 | 27 | for (let i = year; i >= year - 7; i--) { 28 | graphTimeFilters.push([`${i}`, `${i}`]) 29 | } 30 | 31 | return ( 32 |
33 | 36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/utils/range.js: -------------------------------------------------------------------------------- 1 | export default function (start, end, step) { 2 | var range = [] 3 | var typeofStart = typeof start 4 | var typeofEnd = typeof end 5 | 6 | if (step === 0) { 7 | throw new TypeError('Step cannot be zero.') 8 | } 9 | 10 | if (typeofStart === 'undefined' || typeofEnd === 'undefined') { 11 | throw new TypeError('Must pass start and end arguments.') 12 | } else if (typeofStart !== typeofEnd) { 13 | throw new TypeError('Start and end arguments must be of same type.') 14 | } 15 | 16 | if (typeof step === 'undefined') { 17 | step = 1 18 | } 19 | 20 | if (end < start) { 21 | step = -step 22 | } 23 | 24 | if (typeofStart === 'number') { 25 | while (step > 0 ? end >= start : end <= start) { 26 | range.push(start) 27 | start += step 28 | } 29 | } else if (typeofStart === 'string') { 30 | if (start.length !== 1 || end.length !== 1) { 31 | throw new TypeError('Only strings with one character are supported.') 32 | } 33 | 34 | start = start.charCodeAt(0) 35 | end = end.charCodeAt(0) 36 | 37 | while (step > 0 ? end >= start : end <= start) { 38 | range.push(String.fromCharCode(start)) 39 | start += step 40 | } 41 | } else { 42 | throw new TypeError('Only string and number types are supported') 43 | } 44 | 45 | return range 46 | } 47 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { useValues } from 'kea' 5 | import { Col, Row } from 'antd' 6 | 7 | import explorerLogic from 'scenes/explorer/logic' 8 | import connectionLogic from '../connection/logic' 9 | import BreadCrumbs from './breadcrumbs' 10 | import Views from './views' 11 | import Tutorial from './tutorial' 12 | import ModelMap from './model-map' 13 | 14 | export default function Dashboard () { 15 | const { selectedModel } = useValues(explorerLogic) 16 | const { selectedConnection, selectedSubset } = useValues(connectionLogic) 17 | 18 | const showViews = selectedConnection && selectedSubset && !selectedModel 19 | 20 | return ( 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | {showViews ? ( 29 | 30 | 31 | 32 | ) : null} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/users/index.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { useValues } from 'kea' 5 | import { Table, Avatar, Tag } from "antd" 6 | 7 | import usersLogic from './logic' 8 | 9 | const columns = [ 10 | { 11 | title: '', 12 | dataIndex: 'profilePicture', 13 | key: 'avatar', 14 | width: 32, 15 | render: profilePicture => profilePicture 16 | ? 17 | : 18 | }, 19 | { 20 | title: 'E-mail', 21 | dataIndex: 'email', 22 | key: 'email', 23 | }, 24 | { 25 | title: 'Roles', 26 | dataIndex: 'roles', 27 | key: 'roles', 28 | render: roles => roles ? roles.map((role, index) => {role}) : null 29 | } 30 | ] 31 | 32 | export default function UsersScene () { 33 | const { users, isLoading, hasError } = useValues(usersLogic) 34 | 35 | return ( 36 |
37 |

Users

38 | 39 | {isLoading ? 'Loading...' : hasError ?
Error Loading Users
: ( 40 | 41 | )} 42 | 43 |
44 | 45 |
46 | To add a new user, run in the terminal: 47 |
48 | $ insights createsuperuser 49 |
50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Provider } from 'react-redux' 3 | 4 | import { resetContext, getContext } from 'kea' 5 | import sagaPlugin from 'kea-saga' 6 | import listenersPlugin from 'kea-listeners' 7 | 8 | import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router' 9 | import { Route, Switch } from 'react-router' 10 | import { createBrowserHistory } from 'history' 11 | 12 | import routes from './routes' 13 | 14 | import Layout from './_layout' 15 | 16 | export const history = createBrowserHistory() 17 | 18 | resetContext({ 19 | createStore: { 20 | paths: ['kea', 'scenes', 'auth'], 21 | reducers: { 22 | router: connectRouter(history) 23 | }, 24 | middleware: [ 25 | routerMiddleware(history) 26 | ], 27 | }, 28 | plugins: [ 29 | sagaPlugin({ useLegacyUnboundActions: true }), 30 | listenersPlugin 31 | ] 32 | }) 33 | 34 | export default function Scenes() { 35 | return ( 36 | 37 | 38 | 39 | 40 | {Object.entries(routes).map(([path, Component]) => ( 41 | } /> 42 | ))} 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /packages/insights/bin/insights-createsuperuser: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require('path') 4 | var program = require('commander') 5 | var root = path.join(__dirname, '..') 6 | var pkg = require(path.join(root, 'package.json')) 7 | var randomString = require('../app/lib/random-string') 8 | var findConfigFolder = require('../app/lib/find-config-folder') 9 | 10 | program.version(pkg.version) 11 | .usage('[options]') 12 | .description('Create superuser from args or interactive') 13 | .option('-e, --email [string]', 'Email') 14 | .option('-p, --password [string]', 'Password') 15 | .option('--data [folder]', 'Where to store the local NeDB database. Defaults to ~/.insights/data') 16 | .option('-d --develop', 'Run in development mode') 17 | .parse(process.argv) 18 | 19 | process.env.NODE_CONFIG_DIR = findConfigFolder() 20 | process.env.INSIGHTS_DATA = program.data || path.join(process.env.NODE_CONFIG_DIR, 'data') 21 | 22 | console.log(`Using config in: ${process.env.NODE_CONFIG_DIR}`) 23 | 24 | if (program.email) { 25 | process.env.INSIGHTS_SUPERUSER_EMAIL = program.email 26 | } 27 | 28 | if (program.password) { 29 | process.env.INSIGHTS_SUPERUSER_PASSWORD = program.password 30 | } 31 | 32 | // can be random for this case as we will not start the server 33 | process.env.AUTHENTICATION_SECRET = randomString(64) 34 | 35 | const createSuperuser = require('../app/create-superuser') 36 | createSuperuser() 37 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/explorer/state-to-url.js: -------------------------------------------------------------------------------- 1 | // let url = { 2 | // connection: connection, 3 | // columns: columns.join(','), 4 | // sort: sort || '', 5 | // treeState: Object.keys(treeState).join(','), 6 | // graphTimeFilter: graphTimeFilter || '', 7 | // facetsColumn: facetsColumn || '', 8 | // facetsCount: facetsCount || '', 9 | // graphControls: { 10 | // ... 11 | // }, 12 | // filter: [{ key: 'value' }] 13 | // } 14 | 15 | export default function (url) { 16 | const { filter, graphControls, treeState, columns, ...restOfUrl } = url 17 | 18 | let params = Object.assign({}, restOfUrl) 19 | 20 | if (filter) { 21 | filter.forEach(({ key, value }, i) => { 22 | params[`filter[${i}]`] = `${key}=${value}` 23 | }) 24 | } 25 | 26 | if (graphControls) { 27 | params.graphControls = JSON.stringify(graphControls) 28 | } 29 | if (treeState) { 30 | params.treeState = Object.entries(treeState).map(([key, enabled]) => `${enabled ? '' : '!'}${key}`).join(',') 31 | } 32 | if (columns) { 33 | params.columns = columns.join(',') 34 | } 35 | 36 | const anythingSelected = Object.values(params).filter(v => v).length > 0 37 | 38 | const pathname = '/explorer' 39 | const search = anythingSelected ? '?' + Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&') : '' 40 | 41 | return `${pathname}${search}` 42 | } 43 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/styles.scss: -------------------------------------------------------------------------------- 1 | .explorer-scene { 2 | .top-controls { 3 | float: left; 4 | button { 5 | margin-bottom: 0; 6 | margin-right: 5px; 7 | } 8 | } 9 | .top-pagination { 10 | float: right; 11 | } 12 | 13 | .explorer-tree-bar, .explorer-tree-splitter { 14 | background: #e8f3fd; 15 | } 16 | 17 | &.with-dashboard { 18 | & > .LayoutSplitter.horizontal { 19 | border-left-color: #e8f3fd; 20 | border-right-color: hsla(209, 32%, 28%, 1); 21 | } 22 | } 23 | 24 | & > .LayoutSplitter.horizontal { 25 | border-left-color: #e8f3fd; 26 | border-right-color: #ffffff; 27 | opacity: 1; 28 | background: #cedfeb; 29 | &:hover { 30 | border-left-color: hsla(209, 19%, 47%, 1); 31 | border-right-color: hsla(209, 19%, 47%, 1); 32 | opacity: 1; 33 | background: #394b59; 34 | } 35 | } 36 | 37 | .visible-overflow { 38 | overflow: visible !important; 39 | } 40 | .explorer-next-to-sidebar { 41 | min-width: 300px !important; 42 | } 43 | } 44 | .explorer-dashboard-layout { 45 | background: hsla(209, 32%, 28%, 1); 46 | overflow: hidden; 47 | .explorer-dashboard { 48 | min-width: 300px; 49 | } 50 | } 51 | .explorer-table-layout { 52 | overflow: hidden; 53 | > div { 54 | min-width: 300px; 55 | } 56 | } 57 | 58 | .ant-menu.scrollable-menu { 59 | overflow: scroll; 60 | max-height: 80vh; 61 | } 62 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/subset/menu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useActions, useValues } from 'kea' 3 | import { Icon, Menu } from 'antd' 4 | import connectionsLogic from 'scenes/explorer/connection/logic' 5 | 6 | export default function SubsetMenu () { 7 | const { selectedSubset, otherSubsets, connectionId } = useValues(connectionsLogic) 8 | const { newSubset, editSubset, setConnectionId } = useActions(connectionsLogic) 9 | 10 | return ( 11 | 12 | {selectedSubset ? 14 | 15 | {selectedSubset ? selectedSubset.name : '...'} 16 | 17 | }> 18 | 19 | 20 | Configure 21 | 22 | : null} 23 | 24 | {selectedSubset ? : null} 25 | 26 | {otherSubsets.map(subset => ( 27 | setConnectionId(connectionId, subset._id)}> 28 | 29 | {subset.name} 30 | 31 | ))} 32 | 33 | {otherSubsets.length > 0 ? : null} 34 | 35 | 36 | 37 | New Subset 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/insights/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insights", 3 | "description": "Desktop and Self-Hosted \"SQL-not-required\" data analytics and visualisation tool.", 4 | "version": "0.0.72", 5 | "homepage": "", 6 | "main": "app", 7 | "license": "MIT", 8 | "keywords": [ 9 | "feathers", 10 | "insights" 11 | ], 12 | "author": { 13 | "name": "Marius Andra", 14 | "email": "marius.andra@gmail.com" 15 | }, 16 | "contributors": [], 17 | "repository": "mariusandra/insights", 18 | "bugs": {}, 19 | "bin": { 20 | "insights": "./bin/insights", 21 | "insights-init": "./bin/insights-init", 22 | "insights-start": "./bin/insights-start", 23 | "insights-createsecret": "./bin/insights-createsecret", 24 | "insights-createsuperuser": "./bin/insights-createsuperuser" 25 | }, 26 | "directories": { 27 | "lib": "app" 28 | }, 29 | "engines": { 30 | "node": ">= 7.6.0", 31 | "yarn": ">= 0.18.0" 32 | }, 33 | "scripts": { 34 | "start": "./bin/insights-start", 35 | "build": "npm run build-web && npm run copy-web", 36 | "build-web": "cd ../insights-web && npm run build", 37 | "copy-web": "rm -rf web-build/ && cp -a ../insights-web/build ./web-build", 38 | "prepublish": "npm run build" 39 | }, 40 | "files": [ 41 | "bin", 42 | "app", 43 | "web-build" 44 | ], 45 | "peerDependencies": {}, 46 | "dependencies": { 47 | "commander": "^4.1.0", 48 | "insights-api": "0.0.19", 49 | "prompt-promise": "^1.0.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/urls/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { kea, useValues } from 'kea' 3 | import { push } from 'connected-react-router' 4 | import Spinner from '../../lib/tags/spinner' 5 | import client from 'lib/client' 6 | 7 | const urlService = client.service('urls') 8 | 9 | const logic = kea({ 10 | actions: () => ({ 11 | goToUrl: code => ({ code }), 12 | urlError: true 13 | }), 14 | 15 | reducers: ({ actions }) => ({ 16 | isLoading: [true, { 17 | [actions.goToUrl]: () => true, 18 | [actions.urlError]: () => false 19 | }] 20 | }), 21 | 22 | events: ({ actions }) => ({ 23 | afterMount: () => { 24 | const path = window.location.pathname 25 | console.log(path) 26 | actions.goToUrl(path.split('/').slice(-1)[0]) 27 | } 28 | }), 29 | 30 | listeners: ({ actions, store }) => ({ 31 | [actions.goToUrl]: async ({ code }) => { 32 | try { 33 | const urls = await urlService.find({query: {code: code}}) 34 | 35 | const url = urls.data[0] 36 | if (url) { 37 | store.dispatch(push(url.path)) 38 | } else { 39 | actions.urlError() 40 | } 41 | } catch (e) { 42 | actions.urlError() 43 | } 44 | } 45 | }) 46 | }) 47 | 48 | export default function () { 49 | const { isLoading } = useValues(logic) 50 | 51 | return
{isLoading ? :
Error Loading Short URL
}
52 | } 53 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/dashboard/breadcrumbs/model.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useActions, useValues } from 'kea' 3 | import explorerLogic from '../../logic' 4 | 5 | import { Button, Dropdown, Icon, Menu } from 'antd' 6 | 7 | function ModelMenu () { 8 | const { models, selectedModel } = useValues(explorerLogic) 9 | const { openModel } = useActions(explorerLogic) 10 | 11 | return ( 12 | 13 | {selectedModel ? 15 | 16 | {selectedModel ? selectedModel : '...'} 17 | 18 | } /> : null} 19 | 20 | {selectedModel ? : null} 21 | 22 | {models.map(model => ( 23 | openModel(model)}> 24 | 25 | {model} 26 | 27 | ))} 28 | 29 | ) 30 | } 31 | 32 | export default function Model ({ children }) { 33 | const { selectedModel } = useValues(explorerLogic) 34 | 35 | return ( 36 | <> 37 | } trigger={['click']}> 38 | {children || ( 39 | 44 | )} 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/sidebar/selected-model/pin/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'kea' 3 | import PropTypes from 'prop-types' 4 | import { Icon } from 'antd' 5 | import explorerLogic from 'scenes/explorer/logic' 6 | 7 | const connection = { 8 | actions: [ 9 | explorerLogic, [ 10 | 'addFavouriteRequest', 11 | 'removeFavouriteRequest' 12 | ] 13 | ], 14 | props: [ 15 | explorerLogic, [ 16 | 'favourites' 17 | ] 18 | ] 19 | } 20 | class Pin extends Component { 21 | static propTypes = { 22 | path: PropTypes.string 23 | } 24 | 25 | shouldComponentUpdate (nextProps, nextState) { 26 | return nextProps.path !== this.props.path || nextProps.favourites !== this.props.favourites 27 | } 28 | 29 | toggleFavourite = () => { 30 | const { path, favourites } = this.props 31 | const { addFavouriteRequest, removeFavouriteRequest } = this.props.actions 32 | 33 | if (favourites[path]) { 34 | removeFavouriteRequest(path) 35 | } else { 36 | addFavouriteRequest(path) 37 | } 38 | } 39 | 40 | render () { 41 | const { path, favourites } = this.props 42 | 43 | return ( 44 | 47 | 48 | 49 | ) 50 | } 51 | } 52 | const ConnectedPin = connect(connection)(Pin) 53 | export default ConnectedPin 54 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/subset/form/models/styles.scss: -------------------------------------------------------------------------------- 1 | .models-select-tree { 2 | li .ant-tree-node-content-wrapper { 3 | height: auto; 4 | cursor: initial; 5 | } 6 | li .ant-tree-node-content-wrapper:hover { 7 | background: hsl(210, 40%, 96%); 8 | } 9 | .ant-tree-checkbox-inner { 10 | transition-duration: 0.1s; 11 | } 12 | .ant-tree-checkbox-checked::after { 13 | transition-duration: 0.1s; 14 | } 15 | 16 | .ignore-tag { 17 | margin-left: 5px; 18 | } 19 | .new-tag { 20 | margin-left: 5px; 21 | } 22 | 23 | .tree-column-row { 24 | display: flex; 25 | 26 | .column-key { 27 | width: 200px; 28 | min-width: 25%; 29 | white-space: initial; 30 | .anticon { 31 | margin-left: 5px; 32 | color: hsl(210, 21%, 48%); 33 | &:hover { 34 | color: hsl(210, 21%, 39%); 35 | } 36 | } 37 | &.new-column, 38 | &.edited-column { 39 | font-weight: bold; 40 | } 41 | } 42 | 43 | .column-meta { 44 | max-width: calc(75% - 80px); 45 | .ant-tag { 46 | display: inline-flex; 47 | white-space: normal; 48 | > i.anticon { 49 | margin: 4px 4px 4px 0px; 50 | } 51 | > code { 52 | white-space: pre-wrap; 53 | } 54 | } 55 | .anticon-edit { 56 | color: hsl(210, 10%, 80%); 57 | &:hover { 58 | color: hsl(210, 21%, 39%); 59 | } 60 | cursor: pointer; 61 | vertical-align: top; 62 | margin-top: 6px; 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/results/results.class.ts: -------------------------------------------------------------------------------- 1 | import { ResultsParams, ResultsResponse } from '../../insights/definitions' 2 | import { Params, ServiceMethods } from '@feathersjs/feathers'; 3 | import { Application } from '../../declarations'; 4 | import createAdapter from '../../insights/adapter' 5 | import FindResults from '../../insights/results' 6 | 7 | interface ServiceOptions {} 8 | interface ResultsServiceParams extends Params { 9 | query: ResultsParams 10 | } 11 | 12 | export class Results implements Partial> { 13 | app: Application; 14 | options: ServiceOptions; 15 | 16 | constructor (options: ServiceOptions = {}, app: Application) { 17 | this.options = options; 18 | this.app = app; 19 | } 20 | 21 | async create (params: ResultsServiceParams): Promise { 22 | return await this.find(params) 23 | } 24 | 25 | async find (params: ResultsServiceParams): Promise { 26 | const connectionsService = this.app.service('connections') 27 | const structureService = this.app.service('structure') 28 | 29 | const { connection } = params.query 30 | const [connectionId, subsetId] = connection.split('--') 31 | 32 | const connectionsResult = await connectionsService.get(connectionId) 33 | const { url, timeout, timezone } = connectionsResult 34 | 35 | const structure = await structureService.get(connectionId, { query: { subsetId } }) 36 | 37 | const adapter = createAdapter(url, timeout, timezone) 38 | 39 | const results = new FindResults({ params: params.query, adapter, structure }) 40 | return results.getResponse() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/insights-api/src/insights/adapter/sql/postgres.ts: -------------------------------------------------------------------------------- 1 | import { TruncationType } from '../../definitions.d' 2 | 3 | import { Pool } from 'pg' 4 | import escape from 'pg-escape' 5 | 6 | import SQL from './index' 7 | 8 | export default class Postgres extends SQL { 9 | pool: Pool 10 | 11 | constructor (connection: string, timeout: number, timezone: string) { 12 | super(connection, timeout, timezone) 13 | 14 | this.pool = new Pool({ 15 | connectionString: connection, 16 | statement_timeout: timeout ? timeout * 1000 : 15000, 17 | connectionTimeoutMillis: timeout ? timeout * 1000 : 15000 18 | }) 19 | } 20 | 21 | quote (string: string) { 22 | return escape('%L', string) 23 | } 24 | 25 | async execute (sql: string) { 26 | try { 27 | console.log('Executing', sql) 28 | return await this.pool.query(sql) 29 | } catch (error) { 30 | console.error('Error with SQL', sql) 31 | console.error(error) 32 | throw error 33 | } 34 | } 35 | 36 | convertSqlTimezone (sql: string) { 37 | return `(${sql} at time zone 'UTC' at time zone '${this.timezone}')` 38 | } 39 | 40 | allowedDateTruncations () { 41 | return ['hour', 'day', 'week', 'month', 'quarter', 'year'] 42 | } 43 | 44 | truncateDate (sql: string, truncation: TruncationType) { 45 | if (!this.allowedDateTruncations().includes(truncation)) { 46 | throw new Error(`Bad date truncation '${truncation}'`) 47 | } 48 | 49 | return `date_trunc('${truncation}', ${sql})::date` 50 | } 51 | 52 | filterContains (sql: string, string: string) { 53 | return `(${sql}) ilike ${this.quote('%' + string + '%')}` 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/database/menu/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useActions, useValues } from 'kea' 3 | import { Icon, Menu } from 'antd' 4 | 5 | import connectionsLogic from '../../logic' 6 | import explorerLogic from '../../../logic' 7 | 8 | export default function ConnectionMenu () { 9 | const { selectedConnection, otherConnections } = useValues(connectionsLogic) 10 | 11 | const { openAddConnection, openEditConnection } = useActions(connectionsLogic) 12 | const { setConnectionId } = useActions(explorerLogic) 13 | 14 | return ( 15 | 16 | {selectedConnection ? ( 17 | 19 | 20 | {selectedConnection.name} 21 | 22 | }> 23 | openEditConnection(selectedConnection._id)}> 24 | 25 | Configure 26 | 27 | 28 | ) : null} 29 | 30 | {selectedConnection ? : null} 31 | 32 | {otherConnections.map(connection => ( 33 | setConnectionId(connection._id)}> 34 | 35 | {connection.name} 36 | 37 | ))} 38 | 39 | {otherConnections.length > 0 ? : null} 40 | 41 | openAddConnection(false)}> 42 | 43 | New Connection 44 | 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /packages/insights/bin/insights-start: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const path = require('path') 4 | const program = require('commander') 5 | const root = path.join(__dirname, '..') 6 | const pkg = require(path.join(root, 'package.json')) 7 | const findConfigFolder = require('../app/lib/find-config-folder') 8 | 9 | program.version(pkg.version) 10 | .usage(' [options]') 11 | .description('Start insights frontend and API server') 12 | .option('-p, --port [number]', 'Run on specified port - defaults to 8000') 13 | .option('-H, --host [host]', 'Host name or IP address to listen on - defaults to 127.0.0.1') 14 | .option('--public-url [URL]', 'URL used in webapp - defaults to http://$HOST:$PORT') 15 | .option('--static-root [path]', 'Folder for the insights web build - defaults to $NODE_MODULES/insights/web-build') 16 | .option('--config [path]', 'Insights config folder - defaults to ".insights", searched from `pwd` all the way up to /') 17 | .option('--data [path]', 'Path to the local NeDB database - defaults to $CONFIG/data') 18 | .option('--secret [path]', 'Path to the authentication secret - defaults to $CONFIG/secret') 19 | 20 | 21 | program.parse(process.argv) 22 | 23 | const mapping = { 24 | config: 'NODE_CONFIG_DIR', 25 | data: 'INSIGHTS_DATA', 26 | host: 'INSIGHTS_HOST', 27 | port: 'INSIGHTS_PORT', 28 | publicUrl: 'INSIGHTS_PUBLIC_URL', 29 | staticRoot: 'INSIGHTS_STATIC_ROOT', 30 | apiPath: 'INSIGHTS_API_PATH', 31 | socketPath: 'INSIGHTS_SOCKET_PATH', 32 | } 33 | 34 | for (const [key, envKey] of Object.entries(mapping)) { 35 | if (program[key]) { 36 | process.env[envKey] = program[key] 37 | } 38 | } 39 | 40 | process.env.NODE_ENV = 'production' 41 | 42 | const startInsights = require('../app/start') 43 | 44 | startInsights() 45 | -------------------------------------------------------------------------------- /packages/insights-web/src/lib/explorer/url-to-state.js: -------------------------------------------------------------------------------- 1 | export default function urlToState (path) { 2 | const [, search] = (path || '').split('?', 2) 3 | 4 | let values = { 5 | connection: null, 6 | columns: [], 7 | filter: [], 8 | treeState: {}, 9 | graphTimeFilter: null, 10 | sort: null, 11 | facetsColumn: null, 12 | facetsCount: 6, 13 | graphControls: { 14 | type: 'area', 15 | sort: '123', 16 | cumulative: false, 17 | percentages: false, 18 | labels: false 19 | } 20 | }; 21 | 22 | (search || '').split('&').forEach(k => { 23 | const [key, value] = k.split('=').map(decodeURIComponent) 24 | 25 | if (key && value) { 26 | if (key === 'columns') { 27 | values[key] = value.split(',') 28 | } else if (key === 'treeState') { 29 | value.split(',').filter(v => v).forEach(v => { 30 | if (v.indexOf('!') === 0) { 31 | values[key][v.substring(1)] = false 32 | } else { 33 | values[key][v] = true 34 | } 35 | }) 36 | } else if (key === 'graphControls') { 37 | try { 38 | values.graphControls = JSON.parse(value) 39 | } catch (e) { 40 | // ignoring... ? 41 | } 42 | } else if (key === 'facetsCount') { 43 | values.facetsCount = parseInt(value) 44 | } else if (key === 'filter[]' || key.match(/^filter\[[0-9]+]$/)) { 45 | const [ k, v ] = value.split('=', 2) 46 | values.filter.push({ key: k, value: v }) 47 | } else if (key.match(/^filter\[(.+)\]$/)) { 48 | const match = key.match(/^filter\[(.+)\]$/) 49 | values.filter.push({ key: match[1], value }) 50 | } else { 51 | values[key] = value 52 | } 53 | } 54 | }) 55 | 56 | return values 57 | } 58 | -------------------------------------------------------------------------------- /packages/insights-api/src/insights/structure/index.ts: -------------------------------------------------------------------------------- 1 | import { Structure } from '../definitions' 2 | 3 | import yaml from 'js-yaml' 4 | import fs from 'fs' 5 | 6 | import generator from './generators' 7 | 8 | type StructureCache = { [database: string]: Structure } 9 | let structureCache: StructureCache = {} 10 | 11 | export default async function (configPath?: string, database?: string) : Promise { 12 | if (configPath) { 13 | return getConfigStructure(configPath) 14 | } else if (database && structureCache[database]) { 15 | return structureCache[database] 16 | } else if (database) { 17 | const strucutre = await generator(database) 18 | structureCache[database] = strucutre 19 | return strucutre 20 | } else { 21 | throw new Error('Must specify insights.yml path or database string') 22 | } 23 | } 24 | 25 | function getConfigStructure (configPath: string) : Structure { 26 | const structure = yaml.safeLoad(fs.readFileSync(configPath, 'utf8')) 27 | 28 | Object.keys(structure).forEach(model => { 29 | Object.keys(structure[model].columns).forEach(key => { 30 | if (structure[model].columns[key].type && structure[model].columns[key].type.match(/^:/)) { 31 | structure[model].columns[key].type = structure[model].columns[key].type.substring(1) 32 | } 33 | if (structure[model].columns[key].index && structure[model].columns[key].index.match(/^:/)) { 34 | structure[model].columns[key].index = structure[model].columns[key].index.substring(1) 35 | } 36 | }) 37 | Object.keys(structure[model].custom).forEach(key => { 38 | if (structure[model].custom[key].type && structure[model].custom[key].type.match(/^:/)) { 39 | structure[model].custom[key].type = structure[model].custom[key].type.substring(1) 40 | } 41 | }) 42 | }) 43 | 44 | return structure 45 | } 46 | -------------------------------------------------------------------------------- /packages/insights-api/src/services/connections/connections.hooks.ts: -------------------------------------------------------------------------------- 1 | import * as authentication from '@feathersjs/authentication'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | const { authenticate } = authentication.hooks; 5 | 6 | const addAllDataSubset = async context => { 7 | const { result: connection, app } = context 8 | 9 | const subsetsService = app.service('subsets') 10 | 11 | const subsetParams = { 12 | connectionId: connection._id, 13 | type: 'all_data', 14 | name: 'All Data', 15 | addNewModels: true, 16 | addNewFields: true, 17 | selection: {} 18 | } 19 | 20 | await subsetsService.create(subsetParams) 21 | 22 | return context; 23 | } 24 | 25 | const removeAllSubsets = async context => { 26 | const { app, result: connection } = context 27 | 28 | const subsetsService = app.service('subsets') 29 | await subsetsService.remove(null, { query: { connectionId: connection._id } }) 30 | 31 | return context 32 | } 33 | 34 | const removeAllViews = async context => { 35 | const { app, result: connection } = context 36 | 37 | const viewsService = app.service('views') 38 | await viewsService.remove(null, { query: { connectionId: connection._id } }) 39 | 40 | return context 41 | } 42 | 43 | export default { 44 | before: { 45 | all: [ authenticate('jwt') ], 46 | find: [], 47 | get: [], 48 | create: [], 49 | update: [], 50 | patch: [], 51 | remove: [] 52 | }, 53 | 54 | after: { 55 | all: [], 56 | find: [], 57 | get: [], 58 | create: [ addAllDataSubset ], 59 | update: [], 60 | patch: [], 61 | remove: [ removeAllSubsets, removeAllViews ] 62 | }, 63 | 64 | error: { 65 | all: [], 66 | find: [], 67 | get: [], 68 | create: [], 69 | update: [], 70 | patch: [], 71 | remove: [] 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/header/user/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useActions } from 'kea' 3 | import { Button, Dropdown, Menu, Icon, Modal } from 'antd' 4 | 5 | import client from 'lib/client' 6 | 7 | import headerLogic from '../logic' 8 | 9 | function handleLogout () { 10 | client.logout() 11 | window.location.href = '/login' 12 | } 13 | 14 | function openSupport () { 15 | Modal.info({ 16 | title: 'Support Insights', 17 | content: ( 18 |
19 |

If you like Insights or use it in your company, please consider sponsoring its development.

20 |

https://github.com/sponsors/mariusandra

21 |
22 | ), 23 | onOk() {}, 24 | }); 25 | } 26 | 27 | export default function User ({ email }) { 28 | const { openLocation } = useActions(headerLogic) 29 | 30 | const menu = ( 31 | 32 | 33 | 34 | {email} 35 | 36 | 37 | openLocation('/users')}> 38 | 39 | Users 40 | 41 | openLocation('/settings')}> 42 | 43 | Settings 44 | 45 | 46 | 47 | 48 | Support Insights 49 | 50 | 51 | 52 | 53 | Log out 54 | 55 | 56 | ) 57 | 58 | return ( 59 | 60 | 56 | {/*
*/} 57 | {/*
*/} 60 | 61 | 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insights-build", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "description": "Self-Hosted \"SQL-not-required\" data analytics and visualisation tool.", 8 | "version": "0.0.31", 9 | "homepage": "https://github.com/mariusandra/insights", 10 | "main": "server", 11 | "license": "MIT", 12 | "keywords": [ 13 | "feathers", 14 | "insights" 15 | ], 16 | "author": { 17 | "name": "Marius Andra", 18 | "email": "marius.andra@gmail.com" 19 | }, 20 | "contributors": [], 21 | "repository": "mariusandra/insights", 22 | "bugs": {}, 23 | "directories": { 24 | "lib": "server" 25 | }, 26 | "engines": { 27 | "node": ">= 8.0.0", 28 | "yarn": ">= 0.18.0" 29 | }, 30 | "scripts": { 31 | "desktop": "cd packages/insights-desktop && yarn run start", 32 | "init": "rm -rf .insights && ./packages/insights/bin/insights-init --dev", 33 | "init-no-login": "rm -rf .insights && ./packages/insights/bin/insights-init --dev --no-login", 34 | "start": "concurrently --kill-others \"yarn run start-api\" \"yarn run start-web\" \"yarn run start-charts\"", 35 | "start-api": "cd packages/insights-api && yarn start", 36 | "start-charts": "cd packages/insights-charts && yarn start", 37 | "start-web": "cd packages/insights-web && yarn start", 38 | "build": "concurrently \"yarn run build-api\" \"yarn run build-web\" \"yarn run build-charts\"", 39 | "build-api": "cd packages/insights-api && yarn run compile", 40 | "build-charts": "cd packages/insights-charts && yarn run build", 41 | "build-web": "cd packages/insights-web && yarn run build", 42 | "createsuperuser": "cd packages/insights && ./bin/insights-createsuperuser" 43 | }, 44 | "files": [ 45 | "bin", 46 | "README.md" 47 | ], 48 | "dependencies": { 49 | "insights-api": "*", 50 | "insights-charts": "*", 51 | "insights-web": "*" 52 | }, 53 | "peerDependencies": {}, 54 | "devDependencies": { 55 | "concurrently": "^5.0.2", 56 | "eslint-config-standard-react": "^9.2.0", 57 | "semver": "^7.1.1" 58 | }, 59 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 60 | } 61 | -------------------------------------------------------------------------------- /packages/insights/app/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const path = require('path') 3 | const fs = require('fs') 4 | const URL = require('url') 5 | const api = require('insights-api/lib/app').default 6 | const express = require('express') 7 | const bodyParser = require('body-parser') 8 | 9 | module.exports = function startInsights({ 10 | host = process.env.INSIGHTS_HOST || '127.0.0.1', 11 | port = process.env.INSIGHTS_PORT || 8000, 12 | 13 | publicUrl = process.env.INSIGHTS_PUBLIC_URL || `http://${host}:${port}`, 14 | staticRoot = process.env.INSIGHTS_STATIC_ROOT || path.join(__dirname, '..', 'web-build'), 15 | 16 | apiPath = process.env.INSIGHTS_API_PATH || `/api`, 17 | // TODO: no way to configure socketPath yet, must stay at "/socket.io" 18 | socketPath = process.env.INSIGHTS_SOCKET_PATH || '/socket.io', 19 | onListening = undefined 20 | } = {}) { 21 | console.log({ 22 | host, 23 | port, 24 | publicUrl, 25 | staticRoot, 26 | socketPath, 27 | apiPath 28 | }) 29 | 30 | let indexHtml 31 | 32 | const getIndex = (req, res) => { 33 | if (!indexHtml) { 34 | const html = fs.readFileSync(path.join(staticRoot, 'index.html'), 'utf8') 35 | const insightsConfig = { 36 | apiPath, 37 | socketPath, 38 | publicUrl, 39 | noLogin: api.get('authentication').authStrategies.includes('noLogin') 40 | } 41 | indexHtml = html.replace("", ``) 42 | } 43 | res.send(indexHtml) 44 | } 45 | 46 | const app = express() 47 | app.use(bodyParser.json()) 48 | app.use(bodyParser.urlencoded({extended: true})) 49 | app.get('/', getIndex) 50 | app.get(apiPath, getIndex) // redirect /api -> / 51 | app.use(apiPath, api) 52 | app.use(express.static(staticRoot)) 53 | app.get('*', getIndex) 54 | 55 | console.info(`Starting insights on ${host} port ${port}`) 56 | 57 | const server = app.listen(port, host) 58 | api.setup(server) 59 | 60 | process.on('unhandledRejection', (reason, p) => 61 | console.error('Unhandled Rejection at: Promise ', p, reason) 62 | ) 63 | 64 | server.on('listening', () => { 65 | console.info(`--> ${publicUrl}`) 66 | onListening && onListening(app, server) 67 | }) 68 | 69 | return app 70 | } 71 | -------------------------------------------------------------------------------- /packages/insights-charts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insights-charts", 3 | "description": "Desktop and Self-Hosted \"SQL-not-required\" data analytics and visualisation tool.", 4 | "version": "0.0.22", 5 | "homepage": "", 6 | "license": "MIT", 7 | "main": "lib/index.js", 8 | "module": "es/index.js", 9 | "jsnext:main": "es/index.js", 10 | "esnext": "src/index.js", 11 | "keywords": [ 12 | "insights" 13 | ], 14 | "author": { 15 | "name": "Marius Andra", 16 | "email": "marius.andra@gmail.com" 17 | }, 18 | "contributors": [], 19 | "repository": "mariusandra/insights", 20 | "bugs": {}, 21 | "directories": { 22 | "lib": "app" 23 | }, 24 | "engines": { 25 | "node": ">= 7.6.0", 26 | "yarn": ">= 0.18.0" 27 | }, 28 | "scripts": { 29 | "build": "concurrently \"npm run build:cjs\" \"npm run build:es\"", 30 | "build:cjs": "cross-env BABEL_ENV=cjs babel src --out-dir lib", 31 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", 32 | "start": "concurrently --kill-others \"npm run start:cjs\" \"npm run start:es\"", 33 | "start:cjs": "cross-env BABEL_ENV=cjs babel -w src --out-dir lib", 34 | "start:es": "cross-env BABEL_ENV=es babel -w src --out-dir es", 35 | "size": "size-limit", 36 | "test": "jest", 37 | "lint": "eslint src/**", 38 | "prepublish": "npm run build" 39 | }, 40 | "files": [ 41 | "es", 42 | "lib", 43 | "src" 44 | ], 45 | "peerDependencies": { 46 | "moment": "*", 47 | "prop-types": "*", 48 | "react": "*", 49 | "recharts": "*" 50 | }, 51 | "dependencies": {}, 52 | "devDependencies": { 53 | "babel-cli": "^6.26.0", 54 | "babel-core": "^6.26.0", 55 | "babel-eslint": "^10.0.3", 56 | "babel-preset-env": "^1.6.0", 57 | "babel-preset-react": "^6.24.1", 58 | "babel-preset-stage-0": "6.24.1", 59 | "concurrently": "^5.0.2", 60 | "cross-env": "^6.0.3", 61 | "eslint": "^6.8.0", 62 | "eslint-config-standard": "^14.1.0", 63 | "eslint-plugin-import": "^2.20.0", 64 | "eslint-plugin-node": "^11.0.0", 65 | "eslint-plugin-promise": "^4.2.1", 66 | "eslint-plugin-react": "^7.18.0", 67 | "eslint-plugin-standard": "^4.0.1", 68 | "moment": "2.24.0", 69 | "prop-types": "^15.5.10", 70 | "react": "^16.12.0", 71 | "react-dom": "^16.12.0", 72 | "recharts": "1.7.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/insights-api/src/app.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import compress from 'compression'; 3 | import helmet from 'helmet'; 4 | import cors from 'cors'; 5 | 6 | import './utils/set-config-folder' 7 | import feathers from '@feathersjs/feathers'; 8 | import configuration from '@feathersjs/configuration'; 9 | import express from '@feathersjs/express'; 10 | import socketio from '@feathersjs/socketio'; 11 | 12 | import { Application } from './declarations'; 13 | import logger from './logger'; 14 | import middleware from './middleware'; 15 | import services from './services'; 16 | import appHooks from './app.hooks'; 17 | import channels from './channels'; 18 | import authentication from './authentication'; 19 | // Don't remove this comment. It's needed to format import lines nicely. 20 | 21 | const app: Application = express(feathers()); 22 | 23 | // Load app configuration 24 | app.configure(configuration()); 25 | 26 | if (!app.get('authentication') || !app.get('authentication').secret) { 27 | throw new Error("A 'secret' must be provided in your authentication configuration") 28 | } 29 | 30 | if (process.env.INSIGHTS_DATA) { 31 | app.set('nedb', process.env.INSIGHTS_DATA) 32 | } else { 33 | process.env.INSIGHTS_DATA = app.get('nedb') || path.join(process.env.NODE_CONFIG_DIR, 'data') 34 | } 35 | 36 | if (process.env.INSIGHTS_PUBLIC_URL) { 37 | app.set('authentication.jwtOptions.audience', process.env.INSIGHTS_PUBLIC_URL) 38 | } 39 | 40 | // Enable security, CORS, compression, favicon and body parsing 41 | app.use(helmet()); 42 | app.use(cors()); 43 | app.use(compress()); 44 | app.use(express.json()); 45 | app.use(express.urlencoded({ extended: true })); 46 | 47 | // Host the public folder 48 | app.get('/', (req, res) => res.send('

Insights API backend!

')); 49 | 50 | // Set up Plugins and providers 51 | app.configure(express.rest()); 52 | app.configure(socketio()); 53 | 54 | // Configure other middleware (see `middleware/index.js`) 55 | app.configure(middleware); 56 | app.configure(authentication); 57 | // Set up our services (see `services/index.js`) 58 | app.configure(services); 59 | // Set up event channels (see channels.js) 60 | app.configure(channels); 61 | 62 | // Configure a middleware for 404s and the error handler 63 | app.use(express.notFound()); 64 | app.use(express.errorHandler({ logger } as any)); 65 | 66 | app.hooks(appHooks); 67 | 68 | export default app; 69 | -------------------------------------------------------------------------------- /packages/insights-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "insights-web", 3 | "version": "0.0.101", 4 | "dependencies": { 5 | "@feathersjs/client": "^4.5.0", 6 | "@types/jest": "24.9.1", 7 | "@types/node": "13.5.0", 8 | "antd": "^3.26.7", 9 | "connected-react-router": "^6.6.1", 10 | "copy-to-clipboard": "3.2.0", 11 | "diacritics": "1.3.0", 12 | "downloadjs": "1.4.7", 13 | "fixed-data-table-2": "1.0.2", 14 | "font-awesome": "4.7.0", 15 | "helmet": "3.21.2", 16 | "kea": "1.0.1", 17 | "kea-listeners": "^0.2.2", 18 | "kea-saga": "^1.0.1", 19 | "merge-ranges": "1.0.2", 20 | "messg": "^2.2.2", 21 | "moment": "2.24.0", 22 | "sass": "^1.77.8", 23 | "nprogress": "0.2.0", 24 | "prop-types": "^15.7.2", 25 | "react": "^16.12.0", 26 | "react-dimensions": "1.3.1", 27 | "react-dom": "^16.12.0", 28 | "react-flex-layout": "mariusandra/react-flex-layout", 29 | "react-popup": "0.10.0", 30 | "react-redux": "7.1.3", 31 | "react-router": "5.1.2", 32 | "react-scripts": "3.3.0", 33 | "react-wordcloud": "^1.1.1", 34 | "redux": "4.0.5", 35 | "redux-saga": "1.1.3", 36 | "reselect": "4.0.0", 37 | "scroll-into-view": "^1.14.1", 38 | "socket.io-client": "^2.3.0", 39 | "string-natural-compare": "^3.0.1", 40 | "typescript": "3.7.5" 41 | }, 42 | "scripts": { 43 | "start": "cross-env NODE_OPTIONS=--openssl-legacy-provider react-app-rewired start", 44 | "build": "cross-env NODE_OPTIONS=--openssl-legacy-provider react-app-rewired build", 45 | "test": "cross-env NODE_OPTIONS=--openssl-legacy-provider react-app-rewired test", 46 | "eject": "react-scripts eject", 47 | "prepublish": "npm run build" 48 | }, 49 | "eslintConfig": { 50 | "extends": "react-app" 51 | }, 52 | "browserslist": { 53 | "production": [ 54 | ">0.2%", 55 | "not dead", 56 | "not op_mini all" 57 | ], 58 | "development": [ 59 | "last 1 chrome version", 60 | "last 1 firefox version", 61 | "last 1 safari version" 62 | ] 63 | }, 64 | "devDependencies": { 65 | "babel-plugin-import": "^1.13.0", 66 | "customize-cra": "^0.9.1", 67 | "cross-env": "^6.0.3", 68 | "less": "^3.10.3", 69 | "less-loader": "^5.0.0", 70 | "react-app-rewired": "^2.1.5", 71 | "semver": "^7.1.1" 72 | }, 73 | "files": [ 74 | "build" 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/subset/form/models/json/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Form, Input, Modal } from 'antd' 3 | import { useActions, useValues } from 'kea' 4 | 5 | import modelsLogic from '../logic' 6 | 7 | function JSONComponent ({ visible, form: { getFieldDecorator, validateFieldsAndScroll, getFieldValue } }) { 8 | const { defaultJSON } = useValues(modelsLogic) 9 | const { hideJSON, setNewFields, setEditedFields, setCheckedKeys } = useActions(modelsLogic) 10 | 11 | const handleSave = (e) => { 12 | e.preventDefault() 13 | 14 | validateFieldsAndScroll((err, values) => { 15 | if (!err) { 16 | const { newFields, editedFields, checkedKeys } = JSON.parse(values.json) 17 | setNewFields(newFields || {}) 18 | setEditedFields(editedFields || {}) 19 | setCheckedKeys(checkedKeys || []) 20 | hideJSON() 21 | } 22 | }) 23 | } 24 | 25 | return ( 26 | 35 | Cancel 36 | , 37 | , 40 | ]}> 41 | 42 | {visible ? ( 43 |
44 | 45 | {getFieldDecorator('json', { 46 | initialValue: defaultJSON, 47 | rules: [ 48 | { 49 | required: true, 50 | message: 'Please input the JSON!', 51 | }, 52 | (rule, value, callback) => { 53 | try { 54 | JSON.parse(value) 55 | callback() 56 | } catch (e) { 57 | callback('This is not valid JSON!') 58 | } 59 | } 60 | ] 61 | })()} 62 | 63 | 64 | ) : null} 65 |
66 | ) 67 | } 68 | 69 | export default Form.create({ name: 'modelsJSON' })(JSONComponent) 70 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/sidebar/models/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useActions, useValues } from 'kea' 3 | 4 | import { Empty, Icon, Tree } from 'antd' 5 | 6 | import HighlightText from 'lib/utils/highlight-text' 7 | 8 | import connectionLogic from '../../connection/logic' 9 | import explorerLogic from '../../logic' 10 | import logic from './logic' 11 | 12 | const { TreeNode } = Tree; 13 | 14 | export default function Models () { 15 | const { connectionId } = useValues(connectionLogic) 16 | const { models, filteredModels, search, selectedKey } = useValues(explorerLogic) 17 | const { pinnedPerModel, viewsPerModel } = useValues(logic) 18 | const { openModel } = useActions(explorerLogic) 19 | 20 | if (!connectionId) { 21 | return ( 22 |
23 | ) 24 | } 25 | 26 | if (models.length === 0) { 27 | return 28 | } 29 | 30 | return ( 31 |
32 | openModel(model || selectedKey)} 38 | onExpand={([model]) => openModel(model || selectedKey)} 39 | expandedKeys={[]} 40 | > 41 | {filteredModels.map(model => ( 42 | 45 | {search ? {model} : model} 46 | 47 | {viewsPerModel[model] ? : null} 48 | {pinnedPerModel[model] ? : null} 49 | 50 |
51 | } 52 | key={model} 53 | switcherIcon={} 54 | > 55 | 56 | 57 | ))} 58 | 59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/header/share/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { Dropdown, Button, Tooltip, message } from 'antd' 4 | 5 | import copy from 'copy-to-clipboard' 6 | 7 | import client from 'lib/client' 8 | 9 | const urlService = client.service('urls') 10 | 11 | class AutoFocusInput extends Component { 12 | componentDidMount () { 13 | const ref = this._inputRef 14 | ref.focus() 15 | ref.select() 16 | } 17 | 18 | componentDidUpdate () { 19 | const ref = this._inputRef 20 | ref.focus() 21 | ref.select() 22 | } 23 | 24 | setRef = (ref) => { 25 | this._inputRef = ref 26 | } 27 | 28 | handleFocus (event) { 29 | event.target.select() 30 | } 31 | 32 | render () { 33 | return ( 34 | {}} 40 | className='bp3-input' 41 | placeholder='Generating URL...' 42 | style={{minWidth: 280}} /> 43 | ) 44 | } 45 | } 46 | 47 | export default class Share extends Component { 48 | constructor (props) { 49 | super(props) 50 | this.state = { 51 | url: '', 52 | path: '' 53 | } 54 | } 55 | 56 | handleCancel = () => { 57 | this.setState({ 58 | url: '', 59 | path: '' 60 | }) 61 | } 62 | 63 | handleShare = () => { 64 | const { url, path } = this.state 65 | const newPath = `${window.location.pathname}${window.location.search}` 66 | 67 | if (path === newPath) { 68 | copy(url) 69 | message.success('URL copied to clipboard!') 70 | this.setState({ path: '', url: '' }) 71 | } else { 72 | urlService.create({ path: newPath }).then(url => { 73 | if (url) { 74 | this.setState({ path: newPath, url: window.location.origin + '/url/' + url.code }) 75 | } else { 76 | message.error('Error') 77 | } 78 | }) 79 | } 80 | } 81 | 82 | render () { 83 | const { url } = this.state 84 | return ( 85 | } trigger={['click']} > 86 | 87 | 60 | {(moreShown ? options : compareWith ? [compareWith] : []).map(option => ( 61 | 69 | ))} 70 | {!!compareWith && compareWith !== 0 && ( 71 | 77 | )} 78 | 79 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /packages/insights-api/src/authentication.ts: -------------------------------------------------------------------------------- 1 | import { ServiceAddons, Params } from '@feathersjs/feathers'; 2 | import { AuthenticationService, JWTStrategy, AuthenticationBaseStrategy, AuthenticationResult } from '@feathersjs/authentication'; 3 | import { LocalStrategy } from '@feathersjs/authentication-local'; 4 | import { expressOauth, OAuthStrategy } from '@feathersjs/authentication-oauth'; 5 | 6 | import { Application } from './declarations'; 7 | 8 | declare module './declarations' { 9 | interface ServiceTypes { 10 | 'authentication': AuthenticationService & ServiceAddons; 11 | } 12 | } 13 | 14 | class NoLoginStrategy extends AuthenticationBaseStrategy { 15 | async authenticate(authentication: AuthenticationResult, params: Params) { 16 | const usersService = await this.app.service('users') 17 | const users = await usersService.find() 18 | 19 | return { 20 | authentication: { strategy: this.name }, 21 | user: users.data[0] 22 | } 23 | } 24 | } 25 | 26 | class GoogleStrategy extends OAuthStrategy { 27 | async getEntityData(profile) { 28 | // this will set 'googleId' 29 | const baseData = await super.getEntityData(profile, undefined, undefined) 30 | 31 | // this will grab the picture and email address of the Google profile 32 | return { 33 | ...baseData, 34 | profilePicture: profile.picture, 35 | email: profile.email 36 | } 37 | } 38 | 39 | async authenticate(authentication: AuthenticationResult, params: Params) { 40 | const { email, email_verified } = authentication.id_token.payload 41 | 42 | let user 43 | 44 | if (email && email_verified) { 45 | const usersService = await this.app.service('users') 46 | const usersResponse = await usersService.find({ query: { email } }) 47 | user = usersResponse.data[0] 48 | } 49 | 50 | return { 51 | authentication: { strategy: this.name }, 52 | user: user 53 | } 54 | } 55 | } 56 | 57 | export default function(app: Application) { 58 | const authentication = new AuthenticationService(app); 59 | 60 | if (app.get('authentication').authStrategies.includes('jwt')) { 61 | authentication.register('jwt', new JWTStrategy()); 62 | } 63 | 64 | if (app.get('authentication').authStrategies.includes('local')) { 65 | authentication.register('local', new LocalStrategy()); 66 | } 67 | 68 | if (app.get('authentication').authStrategies.includes('noLogin')) { 69 | authentication.register('noLogin', new NoLoginStrategy()); 70 | } 71 | 72 | if (app.get('authentication').authStrategies.includes('google')) { 73 | authentication.register('google', new GoogleStrategy()); 74 | } 75 | 76 | app.use('/authentication', authentication); 77 | app.configure(expressOauth()); 78 | } 79 | -------------------------------------------------------------------------------- /packages/insights-api/src/channels.ts: -------------------------------------------------------------------------------- 1 | import { HookContext } from '@feathersjs/feathers'; 2 | import { Application } from './declarations'; 3 | 4 | export default function(app: Application) { 5 | if(typeof app.channel !== 'function') { 6 | // If no real-time functionality has been configured just return 7 | return; 8 | } 9 | 10 | app.on('connection', (connection: any) => { 11 | // On a new real-time connection, add it to the anonymous channel 12 | app.channel('anonymous').join(connection); 13 | }); 14 | 15 | app.on('login', (authResult: any, { connection }: any) => { 16 | // connection can be undefined if there is no 17 | // real-time connection, e.g. when logging in via REST 18 | if(connection) { 19 | // Obtain the logged in user from the connection 20 | // const user = connection.user; 21 | 22 | // The connection is no longer anonymous, remove it 23 | app.channel('anonymous').leave(connection); 24 | 25 | // Add it to the authenticated user channel 26 | app.channel('authenticated').join(connection); 27 | 28 | // Channels can be named anything and joined on any condition 29 | 30 | // E.g. to send real-time events only to admins use 31 | // if(user.isAdmin) { app.channel('admins').join(connection); } 32 | 33 | // If the user has joined e.g. chat rooms 34 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(channel)); 35 | 36 | // Easily organize users by email and userid for things like messaging 37 | // app.channel(`emails/${user.email}`).join(channel); 38 | // app.channel(`userIds/$(user.id}`).join(channel); 39 | } 40 | }); 41 | 42 | // eslint-disable-next-line no-unused-vars 43 | app.publish((data: any, hook: HookContext) => { 44 | // Here you can add event publishers to channels set up in `channels.js` 45 | // To publish only for a specific event use `app.publish(eventname, () => {})` 46 | 47 | console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 48 | 49 | // e.g. to publish all service events to all authenticated users use 50 | return app.channel('authenticated'); 51 | }); 52 | 53 | // Here you can also add service specific event publishers 54 | // e.g. the publish the `users` service `created` event to the `admins` channel 55 | // app.service('users').publish('created', () => app.channel('admins')); 56 | 57 | // With the userid and email organization from above you can easily select involved users 58 | // app.service('messages').publish(() => { 59 | // return [ 60 | // app.channel(`userIds/${data.createdBy}`), 61 | // app.channel(`emails/${data.recipientEmail}`) 62 | // ]; 63 | // }); 64 | }; 65 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/connection/subset/form/logic.js: -------------------------------------------------------------------------------- 1 | import { kea } from 'kea' 2 | 3 | import { message, Modal } from 'antd' 4 | 5 | import client from 'lib/client' 6 | 7 | import modelsLogic from './models/logic' 8 | import connectionsLogic from '../../logic' 9 | import explorerLogic from '../../../logic' 10 | 11 | const subsetsService = client.service('subsets') 12 | 13 | export default kea({ 14 | connect: { 15 | values: [ 16 | modelsLogic, ['subsetSelection', 'newFields', 'editedFields'], 17 | connectionsLogic, ['subset', 'connectionId'] 18 | ], 19 | actions: [ 20 | explorerLogic, ['refreshData'], 21 | connectionsLogic, ['setConnectionId', 'subsetEdited', 'subsetRemoved', 'closeSubset', 'loadStructure', 'loadSubsets'] 22 | ] 23 | }, 24 | 25 | actions: () => ({ 26 | saveSubset: (formValues) => ({ formValues }), 27 | confirmRemoveSubset: subsetId => ({ subsetId }), 28 | removeSubset: subsetId => ({ subsetId }) 29 | }), 30 | 31 | listeners: ({ actions, values }) => ({ 32 | [actions.saveSubset]: async ({ formValues }) => { 33 | const { subsetSelection, newFields, editedFields, subset: { _id: subsetId }, connectionId } = values 34 | 35 | let subset 36 | 37 | if (subsetId) { 38 | subset = await subsetsService.patch(subsetId, { ...formValues, selection: subsetSelection, newFields, editedFields }) 39 | } else { 40 | subset = await subsetsService.create({ ...formValues, type: 'custom', connectionId, selection: subsetSelection, newFields, editedFields }) 41 | } 42 | 43 | actions.subsetEdited(subset) 44 | actions.closeSubset() 45 | actions.loadStructure(connectionId, subsetId) 46 | actions.refreshData() 47 | 48 | if (subsetId) { 49 | message.success(`Subset "${subset.name}" updated!`) 50 | } else { 51 | message.success(`Subset "${subset.name}" created!`) 52 | actions.setConnectionId(connectionId, subset._id) 53 | } 54 | }, 55 | [actions.confirmRemoveSubset]: async ({ subsetId }) => { 56 | const { subset: { name } } = values 57 | 58 | Modal.confirm({ 59 | title: `Delete the subset "${name}"?`, 60 | content: 'This can not be undone!', 61 | okText: 'Yes', 62 | okType: 'danger', 63 | cancelText: 'No', 64 | onOk () { 65 | actions.removeSubset(subsetId) 66 | } 67 | }) 68 | }, 69 | [actions.removeSubset]: async ({ subsetId }) => { 70 | const { subset: { connectionId, name } } = values 71 | await subsetsService.remove(subsetId) 72 | actions.subsetRemoved(subsetId) 73 | actions.closeSubset() 74 | actions.setConnectionId(connectionId) 75 | actions.loadSubsets(connectionId) 76 | actions.refreshData() 77 | message.success(`Subset "${name}" removed!`) 78 | } 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /packages/insights-web/src/scenes/explorer/graph/controls-right.js: -------------------------------------------------------------------------------- 1 | import './styles.scss' 2 | 3 | import React from 'react' 4 | import { useActions, useValues } from 'kea' 5 | import { Button, Tooltip } from 'antd' 6 | 7 | import explorerLogic from 'scenes/explorer/logic' 8 | 9 | export const colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] 10 | 11 | export default function ControlsRight () { 12 | const { graphControls, graphTimeGroup } = useValues(explorerLogic) 13 | const { setGraphControls } = useActions(explorerLogic) 14 | 15 | const { cumulative, percentages, sort, type, labels, prediction } = graphControls 16 | 17 | return ( 18 |
19 | 20 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Insights 2 | 3 | Insights is a tool to visually explore a PostgreSQL database, with an emphasis on generating graphs that show business performance over time. 4 | 5 | Think of Google Data Studio or Google Looker, but totally free, self-hosted and without the "Google" part. 6 | 7 | See a [**live demo**](https://demo.insights.sh/) for Widgets Inc, a fictional e-commerce site. 8 | 9 | ![Insights Explorer](https://user-images.githubusercontent.com/53387/74577340-e68a6000-4f8e-11ea-95bf-4682f545cc8f.png) 10 | 11 | ## Important Disclaimer and Security Notice! 12 | 13 | Please be aware that is an extremely early BETA release of Insights, which has not gone through any kind of security audit. 14 | 15 | Use on a live server at your own risk! 16 | 17 | ## Installing 18 | 19 | To install, make sure you have Node 10+ installed and then run: 20 | 21 | ``` 22 | npm install -g insights 23 | insights init 24 | insights start 25 | ``` 26 | 27 | This creates a folder `.insights` which contains all the config and runtime data. 28 | 29 | ## Implemented Features 30 | 31 | * Self Hosted, installed via NPM 32 | * PostgreSQL connection support 33 | * Auto-detect your database schema, including all foreign keys! 34 | * Connect to multiple databases 35 | * Edit the schema and add custom SQL fields right there in the interface! 36 | * Create subsets of your data (e.g. share only a few fields with marketing) 37 | * Data explorer 38 | * Filters on the data 39 | * Time-based graphs 40 | * Split the graph by some column (e.g. new users by country name) 41 | * Keyboard navigation in the sidebar 42 | * Saved views 43 | * Pinned fields 44 | 45 | 46 | ## Coming Soon 47 | 48 | * Embed React or