├── src ├── db │ ├── img │ │ ├── card.png │ │ ├── map.png │ │ ├── carto.png │ │ ├── scatter.png │ │ ├── mongodb.svg │ │ ├── sqlite.svg │ │ ├── mysql.svg │ │ └── bigquery.svg │ ├── help.js │ ├── configure.less │ ├── configure.js │ ├── bridge.js │ ├── graphql │ │ └── index.js │ ├── sqlite │ │ └── usage.txt │ └── mongo.js ├── util │ ├── toaster.js │ ├── codeviewer.js │ └── error.js ├── cell │ ├── md.less │ ├── carto.less │ ├── hipster.less │ ├── explain.js │ ├── explain.less │ ├── visualizer.less │ ├── bar_fixed.js │ └── cell.less ├── index.js ├── state │ ├── index.js │ ├── update.js │ ├── export_template.html │ ├── import.js │ ├── export.js │ └── README.md ├── index.html ├── bread.css ├── app.js ├── notebook.js ├── app.less └── delta.js ├── docker-entrypoint.sh ├── credentials.js ├── .editorconfig ├── Dockerfile ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE.md ├── public └── reciever.html ├── nwb.config.js ├── DEPLOYING_LOCALLY.md ├── data ├── employees.sql ├── explain.sql └── geo_states.sql ├── .circleci └── config.yml ├── package.json ├── README.md ├── .gitattributes ├── .gitignore ├── electron.js └── response.js /src/db/img/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HVF/franchise/HEAD/src/db/img/card.png -------------------------------------------------------------------------------- /src/db/img/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HVF/franchise/HEAD/src/db/img/map.png -------------------------------------------------------------------------------- /src/db/img/carto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HVF/franchise/HEAD/src/db/img/carto.png -------------------------------------------------------------------------------- /src/db/img/scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HVF/franchise/HEAD/src/db/img/scatter.png -------------------------------------------------------------------------------- /src/util/toaster.js: -------------------------------------------------------------------------------- 1 | import { Toaster } from '@blueprintjs/core' 2 | export default Toaster.create() 3 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | npx franchise-client & \ 6 | nginx -g 'daemon off;' 7 | 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /credentials.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: process.env.PGHOST || 'localhost', 3 | user: process.env.PGUSER || 'postgres', 4 | database: process.env.PGDATABASE || 'postgres', 5 | password: process.env.PGPASSWORD, 6 | port: process.env.PGPORT || 5432, 7 | } 8 | -------------------------------------------------------------------------------- /src/cell/md.less: -------------------------------------------------------------------------------- 1 | .cm-s-md { 2 | font-family: Georgia; 3 | font-size: 150%; 4 | letter-spacing: 0.5px; 5 | word-spacing: 2px; 6 | color: #333; 7 | .cm-header { 8 | font-size: 200%; 9 | font-family: Helvetica; 10 | color: #444; 11 | } 12 | .CodeMirror-scroll { 13 | max-height: none; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import * as State from './state' 4 | import App from './app' 5 | import _ from 'lodash' 6 | import './state/import.js' 7 | 8 | window.STATE = State 9 | 10 | const ManagedApp = State.Application((state) => ) 11 | ReactDOM.render(, document.querySelector('#app')) 12 | 13 | if (module.hot) module.hot.accept() 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | # Matches the exact files package.json and .travis.yml 19 | [{package.json,.travis.yml}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | RUN apk add --no-cache \ 4 | git \ 5 | nodejs \ 6 | nodejs-npm \ 7 | yarn 8 | 9 | ADD ./ /franchise 10 | WORKDIR /franchise 11 | RUN npm i -g npx franchise-client && \ 12 | yarn install && yarn build 13 | 14 | RUN cp -r /franchise/bundle/* /usr/share/nginx/html && \ 15 | rm -rf /franchise 16 | 17 | EXPOSE 80 14645 18 | COPY ./docker-entrypoint.sh / 19 | RUN chmod +x /docker-entrypoint.sh 20 | ENTRYPOINT ["/docker-entrypoint.sh"] 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": false, 5 | "jsxBracketSameLine": false, 6 | "trailingComma": "es5", 7 | "printWidth": 100, 8 | "arrowParens": "always", 9 | "proseWrap": "always", 10 | "singleQuote": true, 11 | "overrides": [ 12 | { 13 | "files": ["**/*.json", "*.json"], 14 | "options": { 15 | "parser": "json", 16 | "tabWidth": 2 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/util/codeviewer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import CodeMirror from 'codemirror' 4 | import 'codemirror/addon/runmode/runmode' 5 | 6 | export default class CodeViewer extends Component { 7 | componentDidMount() { 8 | CodeMirror.runMode(this.props.code, this.props.mode, this.node) 9 | } 10 | componentDidUpdate() { 11 | CodeMirror.runMode(this.props.code, this.props.mode, this.node) 12 | } 13 | render() { 14 | if (this.props.small) return (this.node = d)} /> 15 | return
 (this.node = d)} />
16 |     }
17 | }
18 | 


--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | ## Prerequisites
 2 | 
 3 | [Node.js](http://nodejs.org/) >= v4 must be installed.
 4 | 
 5 | ## Installation
 6 | 
 7 | - Running `npm install` in the app's root directory will install everything you need for development.
 8 | 
 9 | ## Development Server
10 | 
11 | - `npm start` will run the app's development server at [http://localhost:3000](http://localhost:3000) with hot module reloading.
12 | 
13 | ## Running Tests
14 | 
15 | - `npm test` will run the tests once.
16 | 
17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
18 | 
19 | - `npm run test:watch` will run the tests on every change.
20 | 
21 | ## Building
22 | 
23 | - `npm run build` creates a production build by default.
24 | 
25 |    To create a development build, set the `NODE_ENV` environment variable to `development` while running this command.
26 | 
27 | - `npm run clean` will delete built resources.
28 | 


--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2017 Kevin Kwok, Guillermo Webster
 2 | 
 3 | Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | of this software and associated documentation files (the "Software"), to deal
 5 | in the Software without restriction, including without limitation the rights
 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | copies of the Software, and to permit persons to whom the Software is
 8 | furnished to do so, subject to the following conditions:
 9 | 
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 | 
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 | 


--------------------------------------------------------------------------------
/public/reciever.html:
--------------------------------------------------------------------------------
 1 | 


--------------------------------------------------------------------------------
/nwb.config.js:
--------------------------------------------------------------------------------
 1 | var StatsPlugin = require('stats-webpack-plugin')
 2 | var webpack = require('webpack')
 3 | 
 4 | module.exports = {
 5 |     type: 'react-app',
 6 |     webpack: {
 7 |         node: {
 8 |             fs: 'empty',
 9 |             net: 'empty',
10 |             tls: 'empty',
11 |         },
12 |         extra: {
13 |             plugins: [
14 |                 new StatsPlugin('stats.json', {
15 |                     chunkModules: true,
16 |                 }),
17 |                 new webpack.ContextReplacementPlugin(
18 |                     /graphql-language-service-interface[\\/]dist$/,
19 |                     new RegExp(`^\\./.*\\.js$`)
20 |                 ),
21 |             ],
22 |         },
23 | 
24 |         publicPath: '',
25 |         uglify: {
26 |             compress: {
27 |                 warnings: false,
28 |             },
29 |             output: {
30 |                 comments: false,
31 |             },
32 |             sourceMap: true,
33 |             exclude: /worker\.js/,
34 |         },
35 |     },
36 |     babel: {
37 |         stage: 1,
38 |         // cherryPick: 'lodash',
39 |         // runtime: false,
40 |         // presets: [["env", {
41 |         //     loose: true,
42 |         //     targets: {
43 |         //         chrome: 59
44 |         //     }
45 |         // }]]
46 |     },
47 | }
48 | 


--------------------------------------------------------------------------------
/DEPLOYING_LOCALLY.md:
--------------------------------------------------------------------------------
 1 | If you want to try franchise out, there's an online version you try right now [right here](https://franchise.cloud).
 2 | 
 3 | If you're interested in contributing, there are instructions for running franchise in development mode in [the readme](https://github.com/HVF/franchise#running-locally).
 4 | 
 5 | Otherwise...
 6 | 
 7 | # Deploying Locally
 8 | 0. **If you don't have `npm` or `yarn`, install** [yarn](https://yarnpkg.com/en/docs/install).
 9 | 
10 | 1. **Open up a terminal and run**
11 | 
12 |     ```bash
13 |     git clone --depth 1 https://github.com/HVF/franchise.git
14 |     ```
15 | 
16 | 2. **cd into the project directory**
17 |     ```bash
18 |     cd franchise
19 |     ```
20 | 
21 | 3. **Install the project dependencies**
22 |     ```bash
23 |     yarn install
24 |     ```
25 | 
26 |     (you can also run `npm install`)
27 | 
28 | 4. **Build franchise to static files**
29 |     ```bash
30 |     yarn build
31 |     ```
32 |     This command makes a folder named `/bundle` containing an `index.html` file which runs franchise when you open it in a browser.
33 | 
34 | 5. **Serve the static files**
35 | 
36 |     Use the http server of your choice to serve the contents of the `/bundle` directory. Using python, you might write:
37 |     
38 |     ```bash
39 |     cd bundle
40 |     python -m SimpleHTTPServer
41 |     ```
42 | 
43 | 6. (optional) **Email us at sql@hvflabs.com if you're doing something interesting with franchise!**
44 | 


--------------------------------------------------------------------------------
/src/util/error.js:
--------------------------------------------------------------------------------
 1 | import React from 'react'
 2 | 
 3 | // largely shamelessly plagiarized from https://github.com/bvaughn/react-error-boundary
 4 | 
 5 | const toTitle = (error, componentStack) => {
 6 |     return `${error.toString()}\n\nThis is located at:${componentStack}`
 7 | }
 8 | 
 9 | function FallbackComponent({ componentStack, error }) {
10 |     return (
11 |         
12 | ${error.toString()} 13 |
14 | ) 15 | } 16 | 17 | export default class ErrorBoundary extends React.Component { 18 | constructor() { 19 | super() 20 | this.state = { 21 | error: null, 22 | info: null, 23 | } 24 | } 25 | componentDidCatch(error, info) { 26 | this.setState({ error, info }) 27 | } 28 | 29 | render() { 30 | const { children } = this.props 31 | const { error, info } = this.state 32 | 33 | if (error !== null) { 34 | return ( 35 | 42 | ) 43 | } 44 | 45 | return children || null 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /data/employees.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS employees; 2 | CREATE TABLE employees( id integer, name text, 3 | designation text, manager integer, 4 | hired_on date, salary integer, 5 | commission float, dept integer); 6 | 7 | INSERT INTO employees VALUES (1,'JOHNSON','ADMIN',6,'1990-12-17',18000,NULL,4); 8 | INSERT INTO employees VALUES (2,'HARDING','MANAGER',9,'1998-02-02',52000,300,3); 9 | INSERT INTO employees VALUES (3,'TAFT','SALES I',2,'1996-01-02',25000,500,3); 10 | INSERT INTO employees VALUES (4,'HOOVER','SALES I',2,'1990-04-02',27000,NULL,3); 11 | INSERT INTO employees VALUES (5,'LINCOLN','TECH',6,'1994-06-23',22500,1400,4); 12 | INSERT INTO employees VALUES (6,'GARFIELD','MANAGER',9,'1993-05-01',54000,NULL,4); 13 | INSERT INTO employees VALUES (7,'POLK','TECH',6,'1997-09-22',25000,NULL,4); 14 | INSERT INTO employees VALUES (8,'GRANT','ENGINEER',10,'1997-03-30',32000,NULL,2); 15 | INSERT INTO employees VALUES (9,'JACKSON','CEO',NULL,'1990-01-01',75000,NULL,4); 16 | INSERT INTO employees VALUES (10,'FILLMORE','MANAGER',9,'1994-08-09',56000,NULL,2); 17 | INSERT INTO employees VALUES (11,'ADAMS','ENGINEER',10,'1996-03-15',34000,NULL,2); 18 | INSERT INTO employees VALUES (12,'WASHINGTON','ADMIN',6,'1998-04-16',18000,NULL,4); 19 | INSERT INTO employees VALUES (13,'MONROE','ENGINEER',10,'2000-12-03',30000,NULL,2); 20 | INSERT INTO employees VALUES (14,'ROOSEVELT','CPA',9,'1995-10-12',35000,NULL,1); 21 | -------------------------------------------------------------------------------- /src/db/help.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import React from 'react' 3 | 4 | import * as State from '../state' 5 | import * as U from '../state/update' 6 | 7 | import { DB } from './configure' 8 | import ErrorBoundary from '../util/error' 9 | 10 | export default function HelpPage(props) { 11 | const { connect, empty, config } = props 12 | 13 | if (empty) return null 14 | 15 | let db = DB(connect.active) 16 | 17 | return ( 18 |
19 |
animateScrollHere(e.target)}> 20 | {db.name} Help 21 |
22 |
23 | 24 | {db.Clippy ? ( 25 | 26 | ) : null} 27 | 28 |
29 |
30 | ) 31 | } 32 | 33 | function ease(t) { 34 | return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 35 | } 36 | 37 | function animateScrollHere(el) { 38 | let s0 = document.body.scrollTop, 39 | s1 = el.offsetTop - 10, 40 | t0 = Date.now(), 41 | t1 = t0 + 400 42 | 43 | function helper() { 44 | document.body.scrollTop = s0 + ease((Date.now() - t0) / (t1 - t0)) * (s1 - s0) 45 | 46 | if (Date.now() < t1) { 47 | requestAnimationFrame(helper) 48 | } 49 | } 50 | helper() 51 | } 52 | -------------------------------------------------------------------------------- /src/cell/carto.less: -------------------------------------------------------------------------------- 1 | .inline { 2 | .pivot-visualizer, 3 | .bp-table-container { 4 | max-height: 300px; 5 | } 6 | 7 | .single-row { 8 | max-height: 400px; 9 | } 10 | 11 | .carto-container { 12 | height: 600px; 13 | display: flex; 14 | flex-wrap: wrap-reverse; 15 | flex-direction: column; 16 | 17 | .cartocss, 18 | .carto-css .pt-popover-target, 19 | .input-wrap, 20 | .input-wrap .ReactCodeMirror { 21 | width: 320px; 22 | } 23 | } 24 | } 25 | 26 | .cm-s-monokai .CodeMirror-lines, 27 | .cm-s-monokai .stale.CodeMirror-focused.CodeMirror-lines, 28 | .cm-s-monokai.CodeMirror { 29 | background: #2b3c43 !important; 30 | } 31 | 32 | .carto-css { 33 | height: 600px; 34 | 35 | .input-wrap, 36 | .ReactCodeMirror, 37 | .CodeMirror-wrap { 38 | height: 100% !important; 39 | } 40 | 41 | .fa { 42 | margin-left: -3px; 43 | color: #aaa; 44 | } 45 | 46 | .input-wrap, 47 | .ReactCodeMirror, 48 | .CodeMirror-wrap, 49 | button { 50 | z-index: 2; 51 | } 52 | 53 | button { 54 | margin-left: -15px; 55 | background: #f5f8fa; 56 | border: none; 57 | } 58 | 59 | button:focus { 60 | outline: none; 61 | } 62 | } 63 | 64 | .carto-container { 65 | width: 100%; 66 | 67 | .leaflet-left { 68 | left: 340px; 69 | } 70 | } 71 | 72 | .carto-css .input-wrap { 73 | transition: 1s; 74 | left: 0; 75 | height: 600px; 76 | } 77 | 78 | .carto-css.hide .input-wrap { 79 | transition: 1s; 80 | left: -305px; 81 | } 82 | -------------------------------------------------------------------------------- /src/state/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import * as U from './update' 3 | 4 | let currentState 5 | let batchDepth = 0 6 | let isRendering = false 7 | let renderCallback 8 | 9 | export function get(...combinators) { 10 | if (isRendering) console.warn(new Error('State.get must not be called during render call')) 11 | return U.get(currentState, ...combinators) 12 | } 13 | 14 | export function getAll(...combinators) { 15 | if (isRendering) console.warn(new Error('State.get must not be called during render call')) 16 | return U.getAll(currentState, ...combinators) 17 | } 18 | 19 | export function set(next_state) { 20 | if (isRendering) console.warn(new Error('State.set must not be called during render call')) 21 | batch((_) => (currentState = next_state)) 22 | } 23 | 24 | export function apply(...combinators) { 25 | set(U.apply(get(), ...combinators)) 26 | } 27 | 28 | export const batch = (fn) => batchify(fn)() 29 | 30 | export function batchify(fn) { 31 | return function(...args) { 32 | let originalState = currentState 33 | batchDepth++ 34 | try { 35 | return fn(...args) 36 | } finally { 37 | batchDepth-- 38 | if (batchDepth === 0 && originalState !== currentState && renderCallback) 39 | renderCallback() 40 | } 41 | } 42 | } 43 | 44 | export function Application(render) { 45 | return class Application extends React.Component { 46 | componentDidUpdate() { 47 | isRendering = false 48 | } 49 | componentDidMount() { 50 | isRendering = false 51 | renderCallback = (e) => this.setState({}) 52 | } 53 | render() { 54 | isRendering = true 55 | return render(currentState) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cell/hipster.less: -------------------------------------------------------------------------------- 1 | .cm-s-hipster span.cm-meta { 2 | color: #ff1717; 3 | } 4 | .cm-s-hipster span.cm-keyword { 5 | line-height: 1em; 6 | font-weight: bold; 7 | color: #7f0055; 8 | } 9 | .cm-s-hipster span.cm-atom { 10 | color: #219; 11 | } 12 | .cm-s-hipster span.cm-number { 13 | color: #164; 14 | } 15 | .cm-s-hipster span.cm-def { 16 | color: #00f; 17 | } 18 | .cm-s-hipster span.cm-variable { 19 | color: black; 20 | } 21 | .cm-s-hipster span.cm-variable-2 { 22 | color: #0000c0; 23 | } 24 | .cm-s-hipster span.cm-variable-3, 25 | .cm-s-hipster span.cm-type { 26 | color: #0000c0; 27 | } 28 | .cm-s-hipster span.cm-property { 29 | color: black; 30 | } 31 | .cm-s-hipster span.cm-operator { 32 | color: black; 33 | } 34 | .cm-s-hipster span.cm-comment { 35 | color: #3f7f5f; 36 | } 37 | .cm-s-hipster span.cm-string { 38 | color: #2a00ff; 39 | } 40 | .cm-s-hipster span.cm-string-2 { 41 | color: #f50; 42 | } 43 | .cm-s-hipster span.cm-qualifier { 44 | color: #555; 45 | } 46 | .cm-s-hipster span.cm-builtin { 47 | color: #30a; 48 | } 49 | .cm-s-hipster span.cm-bracket { 50 | color: #cc7; 51 | } 52 | .cm-s-hipster span.cm-tag { 53 | color: #170; 54 | } 55 | .cm-s-hipster span.cm-attribute { 56 | color: #00c; 57 | } 58 | .cm-s-hipster span.cm-link { 59 | color: #219; 60 | } 61 | .cm-s-hipster span.cm-error { 62 | color: #f00; 63 | } 64 | 65 | .cm-s-hipster .CodeMirror-activeline-background { 66 | background: #e8f2ff; 67 | } 68 | .cm-s-hipster .CodeMirror-matchingbracket { 69 | outline: 1px solid grey; 70 | color: black !important; 71 | } 72 | 73 | .cm-s-hipster span.cm-string { 74 | color: #c54997; 75 | } 76 | 77 | .cm-s-hipster span.cm-keyword { 78 | line-height: 1em; 79 | color: #2196f3; 80 | } 81 | 82 | .cm-s-hipster span.cm-variable-2 { 83 | color: #673ab7; 84 | } 85 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Franchise 8 | 9 | 73 | 74 | 75 |
Loading Franchise...
76 | 77 |
78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /data/explain.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS cust_hist; 2 | CREATE TABLE cust_hist (customerid integer, orderid integer); 3 | INSERT INTO cust_hist VALUES (883, 3); 4 | INSERT INTO cust_hist VALUES (882, 33); 5 | INSERT INTO cust_hist VALUES (882, 331); 6 | 7 | DROP TABLE IF EXISTS customers; 8 | CREATE TABLE customers (state text, customerid integer); 9 | INSERT INTO customers VALUES ('VA', 882); 10 | INSERT INTO customers VALUES ('MD', 883); 11 | INSERT INTO customers VALUES ('CA', 131); 12 | INSERT INTO customers VALUES ('MA', 431); 13 | 14 | DROP TABLE IF EXISTS orders; 15 | CREATE TABLE orders ( netamount numeric , totalamount numeric, orderid integer); 16 | INSERT INTO orders VALUES (13.2, 33.2, 3); 17 | INSERT INTO orders VALUES (11.37, 112.3, 33); 18 | INSERT INTO orders VALUES (42.4, 119.2, 331); 19 | INSERT INTO orders VALUES (105.28, 31); 20 | 21 | DROP TABLE IF EXISTS orderlines; 22 | CREATE TABLE orderlines ( prod_id integer, orderid integer); 23 | INSERT INTO orderlines VALUES (44, 3); 24 | INSERT INTO orderlines VALUES (22, 33); 25 | INSERT INTO orderlines VALUES (99, 331); 26 | INSERT INTO orderlines VALUES (44, 31); 27 | 28 | DROP TABLE IF EXISTS products; 29 | CREATE TABLE products ( prod_id integer, category integer); 30 | INSERT INTO products VALUES (44, 93); 31 | INSERT INTO products VALUES (22, 23); 32 | INSERT INTO products VALUES (99, 23); 33 | INSERT INTO products VALUES (44, 93); 34 | 35 | DROP TABLE IF EXISTS categories; 36 | CREATE TABLE categories ( categoryname text, category integer); 37 | INSERT INTO categories VALUES ('pet rock', 93); 38 | INSERT INTO categories VALUES ('headphones', 23); 39 | INSERT INTO categories VALUES ('chewing gum', 25); 40 | 41 | 42 | EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) SELECT c.state, 43 | cat.categoryname, 44 | sum(o.netamount), 45 | sum(o.totalamount) 46 | FROM customers c 47 | INNER JOIN cust_hist ch ON c.customerid = ch.customerid 48 | INNER JOIN orders o ON ch.orderid = o.orderid 49 | INNER JOIN orderlines ol ON ol.orderid = o.orderid 50 | INNER JOIN products p ON ol.prod_id = p.prod_id 51 | INNER JOIN categories cat ON p.category = cat.category 52 | GROUP BY c.state, cat.categoryname 53 | ORDER BY c.state, sum(o.totalamount) DESC 54 | LIMIT 10 OFFSET 1 -------------------------------------------------------------------------------- /src/state/update.js: -------------------------------------------------------------------------------- 1 | export const def = (val) => (orig, next) => next(typeof orig === 'undefined' ? val : orig) 2 | 3 | export const key = (key) => (object, next) => ({ ...object, [key]: next(object[key]) }) 4 | export const safe_key = (key) => (object, next) => ({ 5 | ...object, 6 | [key]: next(typeof object != 'undefined' ? object[key] : undefined), 7 | }) 8 | 9 | export const id = (id) => (array, next) => array.map((k) => (k.id === id ? next(k) : k)) 10 | export const match = (test) => (array, next) => array.map((k, i) => (test(k, i) ? next(k) : k)) 11 | export const index = (index) => (array, next) => 12 | array.slice(0, index).concat([next(array[index])], array.slice(index + 1)) 13 | 14 | export const replace = (value) => (object, next) => value 15 | export const merge = (value) => (object, next) => ({ ...object, ...value }) 16 | 17 | export const removeMatch = (test) => (array, next) => array.filter((k) => !test(k)) 18 | export const removeId = (id) => (array, next) => array.filter((k) => k.id !== id) 19 | export const removeIndex = (index) => (array, next) => 20 | array.slice(0, index).concat(array.slice(index + 1)) 21 | 22 | export const insertAt = (item, index = 0) => (array, next) => 23 | array.slice(0, index).concat([item], array.slice(index)) 24 | export const prepend = (item) => (array, next) => [item].concat(array) 25 | export const append = (item) => (array, next) => array.concat([item]) 26 | 27 | export const inc = (num, next) => (num || 0) + 1 28 | export const dec = (num, next) => (num || 0) - 1 29 | 30 | export const toggle = (bool, next) => !bool 31 | 32 | export const each = (array, next) => array.map(next) 33 | export const all = (object, next) => { 34 | let clone = {} 35 | for (let key in object) clone[key] = next(object[key]) 36 | return clone 37 | } 38 | 39 | export function apply(object, ...sequence) { 40 | if (sequence.length === 0) return object 41 | return coerce(sequence[0])(object, (obj) => apply(obj, ...sequence.slice(1))) 42 | } 43 | 44 | export function getAll(object, ...sequence) { 45 | let values = [] 46 | apply(object, ...sequence, (value) => values.push(value)) 47 | return values 48 | } 49 | 50 | export function get(object, ...sequence) { 51 | return getAll(object, ...sequence)[0] 52 | } 53 | 54 | function coerce(transform) { 55 | if (typeof transform === 'string') return safe_key(transform) 56 | if (typeof transform === 'function') return transform 57 | return (_) => transform 58 | } 59 | -------------------------------------------------------------------------------- /src/cell/explain.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import classList from 'classnames' 4 | 5 | export class ExplainVisualizer extends React.Component { 6 | static key = 'explain' 7 | static desc = 'Explain Visualizer' 8 | static icon = 9 | 10 | static test(result) { 11 | return result.columns[0] === 'QUERY PLAN' 12 | } 13 | 14 | render() { 15 | // 16 | let result = this.props.result 17 | if (typeof result.values[0][0] != 'object') { 18 | return ( 19 |
20 | Run with EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON) to 21 | use Postgres Explain Visualizer 22 |
23 | ) 24 | } 25 | // console.log(result.values[0][0][0]) 26 | return ( 27 |
28 | 29 |
30 | ) 31 | } 32 | } 33 | 34 | function PlanView({ roots }) { 35 | return ( 36 |
37 |
    38 | {roots.map((k, i) => ( 39 |
  • 40 | 41 |
  • 42 | ))} 43 |
44 |
45 | ) 46 | } 47 | 48 | function PlanNode({ plan }) { 49 | return ( 50 |
51 |
52 |
53 |

{plan['Node Type']}

54 | 55 | 56 | {plan['Actual Total Time']} 57 | s | 58 | 42 59 | % 60 | 61 | 62 |
63 |
64 | {plan.Plans ? ( 65 |
    66 | {plan.Plans.map((k, i) => ( 67 |
  • 68 | 69 |
  • 70 | ))} 71 |
72 | ) : null} 73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | 5 | build_macos: 6 | macos: 7 | xcode: "10.0.0" 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | name: Restore Yarn Package Cache 12 | keys: 13 | - yarn-packages-{{ checksum "yarn.lock" }} 14 | - run: 15 | name: Install Dependencies 16 | command: yarn install --frozen-lockfile 17 | - save_cache: 18 | name: Save Yarn Package Cache 19 | key: yarn-packages-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ~/.cache/yarn 22 | - run: security create-keychain -p $KEYCHAIN_PASSWORD build.keychain 23 | - run: security default-keychain -s build.keychain 24 | - run: security unlock-keychain -p $KEYCHAIN_PASSWORD build.keychain 25 | - run: echo $KEYSTORE | base64 --decode - > keystore.p12 26 | - run: security import keystore.p12 -k build.keychain -P $KEYCHAIN_PASSWORD -T /usr/bin/codesign 27 | - run: yarn electron-builder --publish always --mac 28 | 29 | 30 | build_windows: 31 | docker: 32 | - image: electronuserland/builder:wine 33 | steps: 34 | - checkout 35 | - restore_cache: 36 | name: Restore Yarn Package Cache 37 | keys: 38 | - yarn-packages-{{ checksum "yarn.lock" }} 39 | - run: 40 | name: Install Dependencies 41 | command: yarn install --frozen-lockfile 42 | - save_cache: 43 | name: Save Yarn Package Cache 44 | key: yarn-packages-{{ checksum "yarn.lock" }} 45 | paths: 46 | - ~/.cache/yarn 47 | - run: yarn electron-builder --publish always --win 48 | 49 | build_web: 50 | working_directory: ~/repo 51 | 52 | docker: 53 | - image: circleci/node:8 54 | 55 | steps: 56 | - checkout 57 | - restore_cache: 58 | name: Restore Yarn Package Cache 59 | keys: 60 | - yarn-packages-{{ checksum "yarn.lock" }} 61 | - run: 62 | name: Install Dependencies 63 | command: yarn install --frozen-lockfile 64 | - save_cache: 65 | name: Save Yarn Package Cache 66 | key: yarn-packages-{{ checksum "yarn.lock" }} 67 | paths: 68 | - ~/.cache/yarn 69 | 70 | - run: 71 | name: Build App 72 | command: yarn build 73 | 74 | workflows: 75 | build: 76 | jobs: 77 | - build_web 78 | - build_macos: 79 | filters: 80 | branches: 81 | only: master 82 | - build_windows: 83 | filters: 84 | branches: 85 | only: master -------------------------------------------------------------------------------- /src/cell/explain.less: -------------------------------------------------------------------------------- 1 | .plan { 2 | ol, 3 | ul { 4 | list-style: none; 5 | } 6 | } 7 | 8 | .plan-node { 9 | text-decoration: none; 10 | color: #4d525a; 11 | display: inline-block; 12 | transition: all 0.1s; 13 | position: relative; 14 | padding: 6px 10px; 15 | background-color: #fff; 16 | font-size: 12px; 17 | border: 1px solid #dedede; 18 | margin-bottom: 4px; 19 | border-radius: 3px; 20 | overflow-wrap: break-word; 21 | word-wrap: break-word; 22 | word-break: break-all; 23 | width: 240px; 24 | box-shadow: 1px 1px 3px 0px rgba(0, 0, 0, 0.1); 25 | } 26 | 27 | .plan ul, 28 | .plan li { 29 | margin: 0; 30 | padding: 0; 31 | border: 0; 32 | font: inherit; 33 | font-size: 100%; 34 | vertical-align: baseline; 35 | } 36 | 37 | .plan ul li:only-child { 38 | padding-top: 0; 39 | } 40 | 41 | .plan ul { 42 | display: flex; 43 | padding-top: 12px; 44 | position: relative; 45 | margin: auto; 46 | transition: all 0.5s; 47 | margin-top: -5px; 48 | } 49 | 50 | .plan ul ul::before { 51 | content: ''; 52 | position: absolute; 53 | top: 0; 54 | left: 50%; 55 | border-left: 2px solid #c4c4c4; 56 | height: 12px; 57 | width: 0; 58 | } 59 | 60 | .plan ul li { 61 | float: left; 62 | text-align: center; 63 | list-style-type: none; 64 | position: relative; 65 | padding: 12px 3px 0 3px; 66 | transition: all 0.5s; 67 | } 68 | 69 | .plan ul li:before, 70 | .plan ul li:after { 71 | content: ''; 72 | position: absolute; 73 | top: 0; 74 | right: 50%; 75 | border-top: 2px solid #c4c4c4; 76 | width: 50%; 77 | height: 12px; 78 | } 79 | 80 | .plan-node header h4 { 81 | font-size: 13px; 82 | float: left; 83 | font-weight: 600; 84 | } 85 | 86 | .plan-node header .node-duration { 87 | float: right; 88 | margin-left: 10px; 89 | font-size: 13px; 90 | } 91 | 92 | .text-muted, 93 | .plan-stats .btn-close { 94 | color: #999ea7; 95 | } 96 | 97 | .plan ul li:after { 98 | right: auto; 99 | left: 50%; 100 | border-left: 2px solid #c4c4c4; 101 | } 102 | 103 | .plan ul li:first-child::before, 104 | .plan ul li:last-child::after { 105 | border: 0 none; 106 | } 107 | 108 | .plan ul li:last-child::before { 109 | border-right: 2px solid #c4c4c4; 110 | border-radius: 0 6px 0 0; 111 | } 112 | 113 | .plan ul li:first-child::after { 114 | border-radius: 6px 0 0 0; 115 | } 116 | 117 | .plan ul li:before, 118 | .plan ul li:after { 119 | content: ''; 120 | position: absolute; 121 | top: 0; 122 | right: 50%; 123 | border-top: 2px solid #c4c4c4; 124 | width: 50%; 125 | height: 12px; 126 | } 127 | -------------------------------------------------------------------------------- /src/bread.css: -------------------------------------------------------------------------------- 1 | .bread-row > * { 2 | display: flex; 3 | } 4 | 5 | .divider { 6 | box-sizing: border-box; 7 | height: 10px; 8 | 9 | cursor: vertical-text; 10 | padding: 2px 0; 11 | 12 | background-clip: padding-box !important; 13 | border: 3px solid transparent; 14 | border-left: 0; 15 | border-right: 0; 16 | 17 | margin: 0 19px; 18 | 19 | opacity: 0; 20 | transition: opacity 200ms ease-in; 21 | } 22 | 23 | .vertical-divider { 24 | width: 1px; 25 | padding: 0 2px; 26 | 27 | background: transparent; 28 | border: 7px solid transparent; 29 | background-clip: padding-box !important; 30 | border-bottom: 0; 31 | border-top: 0; 32 | flex-shrink: 0; 33 | 34 | opacity: 0; 35 | transition: opacity 200ms ease-in; 36 | 37 | cursor: text; 38 | } 39 | 40 | .vertical-divider::after { 41 | width: 1px; 42 | background: #d4d4d4; 43 | content: ' '; 44 | height: 100%; 45 | display: block; 46 | flex-shrink: 0; 47 | } 48 | 49 | .divider:hover, 50 | .vertical-divider:hover { 51 | opacity: 1; 52 | } 53 | 54 | .divider::after { 55 | height: 1px; 56 | background: #d4d4d4; 57 | content: ' '; 58 | width: 100%; 59 | display: block; 60 | } 61 | 62 | .bread-row.insert-top .divider.divider-top::after, 63 | .bread-row.insert-bottom .divider.divider-bottom::after, 64 | .bread-col.insert-left .vertical-divider.divider-left::after, 65 | .bread-col.insert-right .vertical-divider.divider-right::after { 66 | background: transparent; 67 | } 68 | 69 | .bread-col { 70 | flex-basis: 0; 71 | flex-grow: 1; 72 | 73 | /*border-right: 5px solid transparent;*/ 74 | /*padding: 5px 5px 7px 5px;*/ 75 | /*overflow: hidden;*/ 76 | transition: border-color 200ms ease-in; 77 | display: flex; 78 | 79 | padding: 5px 0; 80 | } 81 | 82 | .bread-enter { 83 | /* 0 does not work so we have to use a small number */ 84 | /* Start our small */ 85 | flex: 0.00001; 86 | overflow: hidden; 87 | 88 | -webkit-animation: flexGrow 300ms ease forwards; 89 | -o-animation: flexGrow 300ms ease forwards; 90 | animation: flexGrow 300ms ease forwards; 91 | } 92 | 93 | .bread-exit { 94 | flex: 1; 95 | overflow: hidden; 96 | 97 | -webkit-animation: flexShrink 300ms ease forwards; 98 | -o-animation: flexShrink 300ms ease forwards; 99 | animation: flexShrink 300ms ease forwards; 100 | } 101 | 102 | .bread-exit > * { 103 | min-width: 200px; 104 | } 105 | 106 | @-webkit-keyframes flexGrow { 107 | to { 108 | flex: 1; 109 | } 110 | } 111 | @-o-keyframes flexGrow { 112 | to { 113 | flex: 1; 114 | } 115 | } 116 | @keyframes flexGrow { 117 | to { 118 | flex: 1; 119 | } 120 | } 121 | 122 | @-webkit-keyframes flexShrink { 123 | to { 124 | flex: 0.01; 125 | flex: 0.00001; 126 | } 127 | } 128 | @-o-keyframes flexShrink { 129 | to { 130 | flex: 0.01; 131 | flex: 0.00001; 132 | } 133 | } 134 | @keyframes flexShrink { 135 | to { 136 | flex: 0.01; 137 | flex: 0.00001; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "franchise", 3 | "version": "1.1.0", 4 | "description": "a sql notebook", 5 | "private": true, 6 | "scripts": { 7 | "build": "nwb build-react-app src/index.js bundle --no-vendor", 8 | "clean": "nwb clean-app", 9 | "start": "nwb serve-react-app", 10 | "test": "nwb test-react", 11 | "test:coverage": "nwb test-react --coverage", 12 | "test:watch": "nwb test-react --server", 13 | "deploy": "gh-pages -d bundle", 14 | "pack": "electron-builder --dir", 15 | "dist": "yarn build && electron-builder", 16 | "build:electron:linux": "yarn build && electron-builder build --linux", 17 | "build:electron:mac": "yarn build && electron-builder build --mac", 18 | "build:electron:windows": "yarn build && electron-builder build --windows" 19 | }, 20 | "dependencies": { 21 | "@blueprintjs/core": "^1.22.0", 22 | "@blueprintjs/labs": "~0.5.0", 23 | "@blueprintjs/table": "^1.19.0", 24 | "@skidding/react-codemirror": "^1.0.2", 25 | "breadloaf": "^1.3.9", 26 | "chart.js": "^2.8.0", 27 | "classnames": "^2.2.6", 28 | "codemirror": "^5.48.0", 29 | "codemirror-graphql": "^0.6.11", 30 | "css-loader": "^3.0.0", 31 | "d3-shape": "^1.2.0", 32 | "electron-notarize": "^0.1.1", 33 | "font-awesome": "^4.7.0", 34 | "franchise-client": "^0.2.7", 35 | "graphql": "^14.3.1", 36 | "graphql-request": "^1.4.0", 37 | "leaflet": "^1.4.0", 38 | "lodash": "^4.17.13", 39 | "normalize.css": "^5.0.0", 40 | "rc-tabs": "^7.1.1", 41 | "react": "^16.8.6", 42 | "react-collapse": "^4.0.2", 43 | "react-dom": "^16.8.6", 44 | "react-error-boundary": "^1.2.5", 45 | "react-json-view": "^1.19.1", 46 | "react-leaflet": "^2.4.0", 47 | "react-motion": "^0.5.0", 48 | "sql.js": "^0.4.0", 49 | "sqlgenerate": "https://github.com/jdrew1303/sqlgenerate", 50 | "sqlite-parser": "^1.0.1", 51 | "sweetalert2": "^8.13.0", 52 | "tmp": "^0.0.33", 53 | "tsparser": "^1.0.2", 54 | "whatwg-fetch": "^3.0.0", 55 | "ws": "^7.0.1", 56 | "xlsx": "^0.14.3" 57 | }, 58 | "devDependencies": { 59 | "babel": "^6.23.0", 60 | "babel-preset-env": "^1.6.0", 61 | "electron": "^4.0.5", 62 | "electron-builder": "^21.1.1", 63 | "file-loader": "^0.11.2", 64 | "gh-pages": "^1.0.0", 65 | "husky": "^2.5.0", 66 | "lint-staged": "^8.2.1", 67 | "nwb": "^0.18.10", 68 | "nwb-less": "^0.5.1", 69 | "prettier": "^1.18.2", 70 | "raw-loader": "^0.5.1", 71 | "stats-webpack-plugin": "^0.6.1", 72 | "uglify-loader": "^2.0.0", 73 | "url-loader": "^0.5.9", 74 | "worker-loader": "^0.8.1" 75 | }, 76 | "build": { 77 | "appId": "com.eponymous.franchise", 78 | "productName": "Franchise", 79 | "copyright": "Copyright © Eponymous Labs, Inc. 2017-2019", 80 | "afterSign": "./build/notarize.js", 81 | "mac": { 82 | "category": "public.app-category.developer-tools", 83 | "hardenedRuntime": true 84 | } 85 | }, 86 | "husky": { 87 | "hooks": { 88 | "pre-commit": "lint-staged" 89 | } 90 | }, 91 | "lint-staged": { 92 | "*.{tsx,ts,js,jsx,json,css,less}": ["prettier --write", "git add"], 93 | ".{prettierrc,babelrc}": ["prettier --write", "git add"] 94 | }, 95 | "author": "Eponymous Labs, Inc.", 96 | "repository": "git@github.com:HVF/franchise.git", 97 | "main": "electron.js" 98 | } 99 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import React from 'react' 3 | 4 | import * as State from './state' 5 | import * as U from './state/update' 6 | 7 | import './app.less' 8 | 9 | import Configure from './db/configure' 10 | import Notebook from './notebook' 11 | import DeltaPane from './delta' 12 | import ExportButton from './state/export' 13 | import HelpPage from './db/help' 14 | 15 | export default class App extends React.PureComponent { 16 | componentWillMount() { 17 | document.getElementById('loader').style.display = 'none' 18 | } 19 | render() { 20 | let { state } = this.props 21 | let empty = 22 | state.notebook.layout.length == 0 && 23 | // && state.trash.cells.length == 0 24 | state.connect.status != 'connected' 25 | return ( 26 |
27 |
28 |
29 |
30 | 31 |
32 | 39 | 40 |
41 | 42 |
43 | ) 44 | } 45 | } 46 | 47 | class Header extends React.PureComponent { 48 | render() { 49 | return ( 50 |
51 |
52 | 53 |

Franchise

54 |
55 | 56 | {this.props.empty ? null : } 57 |
58 |
59 | ) 60 | } 61 | } 62 | 63 | class SloganToggler extends React.PureComponent { 64 | slogans = [ 65 | 'what you get when you add a lot of sequels', 66 | 67 | 'the mcdonalds of sql clients', 68 | 'vaguely sounds like "french fries"', 69 | 'the tool that makes you click on the slogan text', 70 | 'polyamorously relational before it was cool', 71 | 'the ibuprofen for data headaches', 72 | 'Give me your tired, your poor, your huddled data yearning to breathe free.', 73 | 'remarkably painless', 74 | 'best thing since sliced bread', 75 | 'how do you pronounce sql anyway', 76 | "shall i compare thee to a summer's data", 77 | 'a sql notebook', 78 | 'a new kind of sql client', 79 | 'look on my JOINs ye mighty and despair', 80 | 'hello future', 81 | ] 82 | state = { index: 0 } 83 | render() { 84 | return ( 85 | this.setState({ index: this.state.index + 1 })} 88 | > 89 | {this.slogans[this.state.index % this.slogans.length]} 90 | 91 | ) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | # Franchise 8 | *a sql notebook* 9 | 10 | 11 | Franchise is a lightweight but powerful SQL tool with a notebook interface. You can use it online at [franchise.cloud](https://franchise.cloud). 12 | 13 | [![Franchise is pretty cool](https://franchise.cloud/images/landing-gif.gif)](https://franchise.cloud) 14 | 15 | - There's no **sign up** and **no install** 16 | - For editing CSVs, JSON, XLSX files, we've integrated [js-xlsx](https://github.com/SheetJS/js-xlsx), and [sql.js](https://github.com/kripken/sql.js/) so you can query data entirely locally in your browser 17 | - For connecting to **PostgreSQL**, **MySQL**, or **BigQuery**, just run a single command in your terminal to open a bridge that allows Franchise to directly connect to your database. Your data never touches a third party server. 18 | - Chart with a single click 19 | - Compare queries side by side 20 | - With our [unique notebook layout engine](https://github.com/antimatter15/breadloaf), you can drag and drop cells on the same line to compare views. 21 | 22 | # Running Locally (Development Mode) 23 | There's an online version of franchise [right here](https://franchise.cloud). There are also instructions for building franchise to static files [here](https://github.com/HVF/franchise/blob/master/DEPLOYING_LOCALLY.md). 24 | 25 | Otherwise, here's how to run franchise in development mode: 26 | 27 | 0. **If you don't have `npm` or `yarn`, install** [yarn](https://yarnpkg.com/en/docs/install). 28 | 29 | 1. **Open up a terminal and run** 30 | 31 | ```bash 32 | git clone --depth 1 https://github.com/HVF/franchise.git 33 | ``` 34 | 35 | 2. **cd into the project directory** 36 | ```bash 37 | cd franchise 38 | ``` 39 | 40 | 3. **Install the project dependencies** 41 | ```bash 42 | yarn install 43 | ``` 44 | 45 | (you can also run `npm install`) 46 | 47 | 4. **Start the dev server** 48 | ```bash 49 | yarn start 50 | ``` 51 | 52 | (you can also run `npm start`) 53 | 54 | 5. **Open up a browser and go to** `http://localhost:3000` 55 | 56 | 6. **Edit some files in `franchise/src`.** 57 | 58 | When you save your edits, and the browser will automatically reload. 59 | 60 | 7. (optional) **Add a bunch of great functionality and send a PR!** 61 | 62 | --- 63 | 64 | # Running in Docker 🐳 65 | 66 | Application will be available here: [http://localhost:3000](http://localhost:3000) 67 | 68 | ## Using Docker Hub image 69 | 70 | ```bash 71 | docker run \ 72 | --name franchise \ 73 | -p 3000:80 \ 74 | -p 14645:14645 \ 75 | -d binakot/franchise 76 | ``` 77 | 78 | ## Build your own image 79 | 80 | 0. Build a docker image: 81 | 82 | ```bash 83 | docker build -t franchise . 84 | ``` 85 | 86 | 1. Run a container with image: 87 | 88 | ```bash 89 | docker run \ 90 | --name franchise \ 91 | -p 3000:80 \ 92 | -p 14645:14645 \ 93 | -d franchise 94 | ``` 95 | 96 | --- 97 | 98 | # Build electron app 99 | 100 | Linux: 101 | 102 | ```bash 103 | yarn build:electron:linux 104 | ``` 105 | 106 | macOS: 107 | 108 | ```bash 109 | yarn build:electron:mac 110 | ``` 111 | 112 | Windows: 113 | 114 | ```bash 115 | yarn build:electron:windows 116 | ``` 117 | 118 | After build check out `dist` folder. 119 | -------------------------------------------------------------------------------- /src/db/configure.less: -------------------------------------------------------------------------------- 1 | .configure-wrap { 2 | margin: 0 auto; 3 | margin-bottom: 5px; 4 | position: relative; 5 | } 6 | 7 | .configure { 8 | background: white; 9 | 10 | box-shadow: 0px 2px 7px rgba(0, 0, 0, 0.1); 11 | 12 | border-radius: 4px; 13 | overflow: hidden; 14 | 15 | -webkit-app-region: no-drag; 16 | 17 | h1 { 18 | font-weight: 100; 19 | padding: 24px; 20 | } 21 | 22 | .rc-tabs-left { 23 | border-right: 0; 24 | overflow-y: hidden; 25 | padding: 10px 0; 26 | } 27 | 28 | .rc-tabs-content .rc-tabs-tabpane { 29 | overflow: auto; 30 | padding: 20px; 31 | } 32 | 33 | .rc-tabs-left .rc-tabs-tab { 34 | padding: 12px 24px; 35 | } 36 | 37 | .banner { 38 | padding: 20px; 39 | color: white; 40 | display: flex; 41 | // margin-bottom: 20px; 42 | 43 | transition: background 0.4s linear; 44 | 45 | &.connected { 46 | background: #609860; 47 | } 48 | &.unconfigured { 49 | background: #656565; 50 | } 51 | &.connecting { 52 | background: #106ba3; 53 | } 54 | &.disconnected { 55 | background: #9c0000; 56 | } 57 | 58 | &.can-toggle { 59 | cursor: pointer; 60 | } 61 | 62 | .body { 63 | flex: 1; 64 | } 65 | .toggle { 66 | user-select: none; 67 | cursor: pointer; 68 | } 69 | } 70 | } 71 | 72 | .configure > .ReactCollapse--collapse > .ReactCollapse--content { 73 | border: 1px solid #d8d8d8; 74 | border-top: none; 75 | border-bottom-left-radius: 4px; 76 | border-bottom-right-radius: 4px; 77 | } 78 | 79 | .open-thumb { 80 | border: 1px solid #d8d8d8; 81 | border-radius: 5px; 82 | margin-right: 10px; 83 | padding: 10px 15px; 84 | display: inline-block; 85 | user-select: none; 86 | cursor: pointer; 87 | transition: all 0.1s linear; 88 | } 89 | 90 | .sample-thumb { 91 | border: 1px solid #d8d8d8; 92 | border-radius: 5px; 93 | margin-right: 10px; 94 | margin-top: 5px; 95 | margin-bottom: 5px; 96 | width: 140px; 97 | height: 100px; 98 | display: inline-block; 99 | position: relative; 100 | overflow: hidden; 101 | background-size: cover; 102 | user-select: none; 103 | cursor: pointer; 104 | transition: all 0.1s linear; 105 | } 106 | 107 | @media (max-width: 700px) { 108 | .configure .rc-tabs-bar { 109 | display: none; 110 | } 111 | } 112 | 113 | .open-thumb:hover, 114 | .sample-thumb:hover { 115 | box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); 116 | transform: scale(1.05); 117 | } 118 | 119 | .sample-thumb .title { 120 | background: rgba(0, 0, 0, 0.5); 121 | color: white; 122 | padding: 5px 10px; 123 | position: absolute; 124 | bottom: 0; 125 | left: 0; 126 | right: 0; 127 | } 128 | 129 | // this is a hack that fixes a visual glitch on the file upload button 130 | .pt-file-upload-input::after { 131 | box-shadow: inset 0 0 0 1px rgba(16, 22, 26, 0.2), inset 0 -1px 0 rgba(16, 22, 26, 0.1); 132 | } 133 | 134 | .pg-form { 135 | .pt-control-group { 136 | margin-bottom: 10px; 137 | } 138 | .ReactCollapse--content { 139 | padding: 10px 0; 140 | } 141 | .pt-button { 142 | margin-top: 10px; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/state/export_template.html: -------------------------------------------------------------------------------- 1 | 2 | Franchise Notebook {{notebook_name}} 3 | 18 | 82 |
Loading Franchise...
83 |
84 | 87 | 112 | 113 | -------------------------------------------------------------------------------- /src/state/import.js: -------------------------------------------------------------------------------- 1 | import * as State from './index' 2 | import Toaster from '../util/toaster' 3 | import { Intent } from '@blueprintjs/core' 4 | import _ from 'lodash' 5 | import { getDB } from '../db/configure' 6 | 7 | function restoreDefault() { 8 | var credentials = {} 9 | try { 10 | credentials = JSON.parse(localStorage.credentials) 11 | } catch (err) {} 12 | 13 | const DEFAULT_STATE = { 14 | config: { 15 | open: false, 16 | credentials: credentials, 17 | }, 18 | connect: { 19 | active: localStorage.activeConnector || 'sqlite', 20 | status: 'unconfigured', 21 | }, 22 | trash: { 23 | open: false, 24 | cells: [], 25 | }, 26 | deltas: { 27 | open: false, 28 | changes: [], 29 | }, 30 | notebook: { 31 | layout: [], 32 | }, 33 | } 34 | 35 | State.set(DEFAULT_STATE) 36 | } 37 | 38 | window.addEventListener( 39 | 'message', 40 | (e) => { 41 | if (e.data && e.data.action === 'franchise-import') { 42 | try { 43 | console.log('restoring from postmessage') 44 | } catch (err) { 45 | console.error(err) 46 | } 47 | } 48 | }, 49 | false 50 | ) 51 | 52 | export function importData(dump) { 53 | if (dump.version != 2) throw new Error('Incompatible format version') 54 | State.set(dump.state) 55 | if (dump.autoconnect) { 56 | let db = getDB() 57 | if (db.connectDB) { 58 | db.connectDB(dump.databaseDump) 59 | } else { 60 | console.warn('Active database connector does not export connectDB method.') 61 | } 62 | } 63 | } 64 | 65 | if (State.get()) { 66 | // we already got data woot woot 67 | } else if (sessionStorage.importData) { 68 | try { 69 | var data = JSON.parse(sessionStorage.importData) 70 | delete sessionStorage.importData 71 | } catch (err) { 72 | console.error(err) 73 | } 74 | importData(data) 75 | } else if (sessionStorage.autosave) { 76 | try { 77 | var data = JSON.parse(sessionStorage.autosave) 78 | } catch (err) { 79 | console.error(err) 80 | } 81 | Toaster.show({ 82 | message: 'Restored from most recent autosave.', 83 | intent: Intent.SUCCESS, 84 | action: { 85 | onClick: () => restoreDefault(), 86 | text: 'Clear Notebook', 87 | }, 88 | }) 89 | importData(data) 90 | } else if (localStorage.autosave) { 91 | try { 92 | var data = JSON.parse(localStorage.autosave) 93 | } catch (err) { 94 | console.error(err) 95 | } 96 | if (data) { 97 | Toaster.show({ 98 | message: 'Restore from the most recent autosave?', 99 | intent: Intent.SUCCESS, 100 | action: { 101 | onClick: () => importData(data), 102 | text: 'Restore', 103 | }, 104 | }) 105 | } 106 | restoreDefault() 107 | } else { 108 | restoreDefault() 109 | } 110 | 111 | // if(sessionStorage.importData){ 112 | // try { 113 | // var data = JSON.parse(sessionStorage.importData) 114 | // delete sessionStorage.importData; 115 | // } catch (err) { console.error(err) } 116 | // console.log(data) 117 | // // if(data && data.version >= 2 && data.version < 3){ 118 | // // restoreState = data; 119 | // // } 120 | // } 121 | // if(window.opener){ 122 | // window.opener.postMessage('franchise-request-import', '*') 123 | // } 124 | 125 | // else if(location.search === '?sqlite'){ 126 | // restoreState = _.cloneDeep(DEFAULT_STATE) 127 | // restoreState.connect.active = 'sqlite' 128 | // }else if(location.search === '?postgres'){ 129 | // restoreState = _.cloneDeep(DEFAULT_STATE) 130 | // restoreState.connect.active = 'postgres' 131 | // } 132 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | #common settings that generally should always be used with your language specific settings 2 | 3 | # Auto detect text files and perform LF normalization 4 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # Documents 12 | *.doc diff=astextplain 13 | *.DOC diff=astextplain 14 | *.docx diff=astextplain 15 | *.DOCX diff=astextplain 16 | *.dot diff=astextplain 17 | *.DOT diff=astextplain 18 | *.pdf diff=astextplain 19 | *.PDF diff=astextplain 20 | *.rtf diff=astextplain 21 | *.RTF diff=astextplain 22 | *.md text 23 | *.adoc text 24 | *.textile text 25 | *.mustache text 26 | *.csv text 27 | *.tab text 28 | *.tsv text 29 | *.sql text 30 | 31 | # Graphics 32 | *.png binary 33 | *.jpg binary 34 | *.jpeg binary 35 | *.gif binary 36 | *.tif binary 37 | *.tiff binary 38 | *.ico binary 39 | # SVG treated as an asset (binary) by default. If you want to treat it as text, 40 | # comment-out the following line and uncomment the line after. 41 | *.svg binary 42 | #*.svg text 43 | *.eps binary 44 | ## GITATTRIBUTES FOR WEB PROJECTS 45 | # 46 | # These settings are for any web project. 47 | # 48 | # Details per file setting: 49 | # text These files should be normalized (i.e. convert CRLF to LF). 50 | # binary These files are binary and should be left untouched. 51 | # 52 | # Note that binary is a macro for -text -diff. 53 | ###################################################################### 54 | 55 | ## AUTO-DETECT 56 | ## Handle line endings automatically for files detected as 57 | ## text and leave all files detected as binary untouched. 58 | ## This will handle all files NOT defined below. 59 | * text=auto 60 | 61 | ## SOURCE CODE 62 | *.bat text eol=crlf 63 | *.coffee text 64 | *.css text 65 | *.htm text 66 | *.html text 67 | *.inc text 68 | *.ini text 69 | *.js text 70 | *.json text 71 | *.jsx text 72 | *.less text 73 | *.od text 74 | *.onlydata text 75 | *.php text 76 | *.pl text 77 | *.py text 78 | *.rb text 79 | *.sass text 80 | *.scm text 81 | *.scss text 82 | *.sh text eol=lf 83 | *.sql text 84 | *.styl text 85 | *.tag text 86 | *.ts text 87 | *.tsx text 88 | *.xml text 89 | *.xhtml text 90 | 91 | ## DOCKER 92 | *.dockerignore text 93 | Dockerfile text 94 | 95 | ## DOCUMENTATION 96 | *.markdown text 97 | *.md text 98 | *.mdwn text 99 | *.mdown text 100 | *.mkd text 101 | *.mkdn text 102 | *.mdtxt text 103 | *.mdtext text 104 | *.txt text 105 | AUTHORS text 106 | CHANGELOG text 107 | CHANGES text 108 | CONTRIBUTING text 109 | COPYING text 110 | copyright text 111 | *COPYRIGHT* text 112 | INSTALL text 113 | license text 114 | LICENSE text 115 | NEWS text 116 | readme text 117 | *README* text 118 | TODO text 119 | 120 | ## TEMPLATES 121 | *.dot text 122 | *.ejs text 123 | *.haml text 124 | *.handlebars text 125 | *.hbs text 126 | *.hbt text 127 | *.jade text 128 | *.latte text 129 | *.mustache text 130 | *.njk text 131 | *.phtml text 132 | *.tmpl text 133 | *.tpl text 134 | *.twig text 135 | 136 | ## LINTERS 137 | .csslintrc text 138 | .eslintrc text 139 | .htmlhintrc text 140 | .jscsrc text 141 | .jshintrc text 142 | .jshintignore text 143 | .stylelintrc text 144 | 145 | ## CONFIGS 146 | *.bowerrc text 147 | *.cnf text 148 | *.conf text 149 | *.config text 150 | .browserslistrc text 151 | .editorconfig text 152 | .gitattributes text 153 | .gitconfig text 154 | .htaccess text 155 | *.npmignore text 156 | *.yaml text 157 | *.yml text 158 | browserslist text 159 | Makefile text 160 | makefile text 161 | 162 | ## HEROKU 163 | Procfile text 164 | .slugignore text 165 | 166 | ## GRAPHICS 167 | *.ai binary 168 | *.bmp binary 169 | *.eps binary 170 | *.gif binary 171 | *.ico binary 172 | *.jng binary 173 | *.jp2 binary 174 | *.jpg binary 175 | *.jpeg binary 176 | *.jpx binary 177 | *.jxr binary 178 | *.pdf binary 179 | *.png binary 180 | *.psb binary 181 | *.psd binary 182 | *.svg text 183 | *.svgz binary 184 | *.tif binary 185 | *.tiff binary 186 | *.wbmp binary 187 | *.webp binary 188 | 189 | ## AUDIO 190 | *.kar binary 191 | *.m4a binary 192 | *.mid binary 193 | *.midi binary 194 | *.mp3 binary 195 | *.ogg binary 196 | *.ra binary 197 | 198 | ## VIDEO 199 | *.3gpp binary 200 | *.3gp binary 201 | *.as binary 202 | *.asf binary 203 | *.asx binary 204 | *.fla binary 205 | *.flv binary 206 | *.m4v binary 207 | *.mng binary 208 | *.mov binary 209 | *.mp4 binary 210 | *.mpeg binary 211 | *.mpg binary 212 | *.ogv binary 213 | *.swc binary 214 | *.swf binary 215 | *.webm binary 216 | 217 | ## ARCHIVES 218 | *.7z binary 219 | *.gz binary 220 | *.jar binary 221 | *.rar binary 222 | *.tar binary 223 | *.zip binary 224 | 225 | ## FONTS 226 | *.ttf binary 227 | *.eot binary 228 | *.otf binary 229 | *.woff binary 230 | *.woff2 binary 231 | 232 | ## EXECUTABLES 233 | *.exe binary 234 | *.pyc binary 235 | -------------------------------------------------------------------------------- /src/cell/visualizer.less: -------------------------------------------------------------------------------- 1 | @import 'explain.less'; 2 | 3 | .ct-chart { 4 | flex-grow: 1; 5 | border-top: 1px solid #dfdfdf; 6 | padding-top: 20px; 7 | min-height: 300px; 8 | } 9 | 10 | .table-popup { 11 | .table-td { 12 | word-wrap: break-word; 13 | max-width: 230px; 14 | } 15 | } 16 | 17 | .result-loading { 18 | opacity: 0.4; 19 | } 20 | 21 | .visualizer-loading { 22 | flex-grow: 1; 23 | padding: 10px; 24 | opacity: 0.4; 25 | border-top: 1px solid #d7d8da; 26 | 27 | border-bottom-left-radius: 4px; 28 | border-bottom-right-radius: 4px; 29 | 30 | background: #f5f8fa; 31 | } 32 | 33 | .output-wrap { 34 | display: flex; 35 | background: #f5f8fa; 36 | 37 | .controls { 38 | border-top: 1px solid #dfdfdf; 39 | 40 | button.selected { 41 | color: #8e8ec1; 42 | background: #e3e8ea; 43 | margin: -1px; 44 | border-radius: 3px; 45 | border: 1px solid #d3d6d8; 46 | } 47 | } 48 | 49 | border-bottom-right-radius: 4px; 50 | border-bottom-left-radius: 4px; 51 | } 52 | 53 | .output-wrap.fullscreen { 54 | position: fixed; 55 | top: 0; 56 | left: 0; 57 | z-index: 100; 58 | right: 0; 59 | bottom: 0; 60 | } 61 | 62 | .fullscreen { 63 | .map-container { 64 | height: 100vh; 65 | } 66 | } 67 | 68 | .inline { 69 | .map-container { 70 | height: 600px; 71 | } 72 | } 73 | 74 | .inline { 75 | .pivot-visualizer, 76 | .bp-table-container { 77 | max-height: 300px; 78 | } 79 | 80 | .single-row { 81 | max-height: 400px; 82 | } 83 | } 84 | .bp-table-container { 85 | min-height: 150px; 86 | box-shadow: none; 87 | border-top: 1px solid rgba(16, 22, 26, 0.15); 88 | 89 | // this is a nasty hack of a workaround for a weird rendering bug 90 | // on chrome where the border on the controls strip for the outputs 91 | // only sometimes shows up for some reason 92 | border-right: 1px solid transparent; 93 | 94 | background: transparent; 95 | flex-grow: 1; 96 | flex-shrink: 1; 97 | width: 0; 98 | 99 | &:focus { 100 | outline: none; 101 | } 102 | 103 | .bp-table-row-name { 104 | min-width: 35px; 105 | } 106 | 107 | .editable-icon { 108 | margin-top: 8px; 109 | color: #aaaaaa; 110 | float: right; 111 | } 112 | } 113 | 114 | // .error { 115 | // background: maroon; 116 | // padding: 14px 24px; 117 | // color: white; 118 | 119 | // border-bottom-left-radius: 3px; 120 | // border-bottom-right-radius: 3px; 121 | // } 122 | 123 | .error { 124 | background: rgba(148, 0, 0, 0.07); 125 | padding: 14px 24px; 126 | color: #ad5050; 127 | border-bottom-left-radius: 3px; 128 | border-top: 1px solid #dfdfdf; 129 | border-bottom-right-radius: 3px; 130 | } 131 | 132 | .single-result { 133 | flex-grow: 1; 134 | padding: 10px; 135 | border-top: 1px solid #d7d8da; 136 | 137 | border-bottom-left-radius: 4px; 138 | border-bottom-right-radius: 4px; 139 | 140 | background: #f5f8fa; 141 | 142 | display: flex; 143 | align-items: center; 144 | 145 | b { 146 | // padding: 0 5px; 147 | 148 | width: 30%; 149 | 150 | white-space: normal; 151 | word-wrap: break-word; 152 | 153 | padding: 0 5px; 154 | } 155 | 156 | pre { 157 | margin: 0; 158 | flex: 1; 159 | 160 | overflow-x: auto; 161 | } 162 | } 163 | 164 | .field-value { 165 | white-space: pre-wrap; 166 | } 167 | 168 | .single-row { 169 | overflow: auto; 170 | border-top: 1px solid #d7d8da; 171 | flex-grow: 1; 172 | padding: 20px; 173 | line-height: 1.5; 174 | 175 | th { 176 | text-align: left; 177 | min-width: 200px; 178 | padding-right: 10px; 179 | } 180 | 181 | td { 182 | width: 100%; 183 | } 184 | 185 | i.fa-pencil.fa.editable-icon { 186 | float: right; 187 | top: 4px; 188 | position: relative; 189 | } 190 | 191 | table { 192 | background: white; 193 | display: block; 194 | border: 1px solid #d7d8da; 195 | border-radius: 3px; 196 | padding: 20px 20px; 197 | cursor: auto; 198 | } 199 | } 200 | 201 | .pivot-visualizer { 202 | border-top: 1px solid #d7d8da; 203 | flex-grow: 1; 204 | overflow: auto; 205 | } 206 | 207 | .slice-wrap { 208 | width: 0; 209 | flex: 1; 210 | } 211 | 212 | .exporter { 213 | flex: 1; 214 | display: flex; 215 | position: relative; 216 | 217 | .buttons-wrap { 218 | background: rgba(245, 248, 250, 0.8); 219 | position: absolute; 220 | display: flex; 221 | align-items: center; 222 | justify-content: center; 223 | text-align: center; 224 | top: 0; 225 | width: 100%; 226 | height: 100%; 227 | .buttons { 228 | button { 229 | margin: 0 5px; 230 | } 231 | } 232 | } 233 | } 234 | 235 | .chart-container { 236 | border-top: 1px solid #dfdfdf; 237 | padding: 10px; 238 | min-height: 300px; 239 | } 240 | 241 | .chart { 242 | -webkit-user-select: none; 243 | } 244 | 245 | .leaflet-div-icon { 246 | border-radius: 50%; 247 | background-color: blue !important; 248 | } 249 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/web,node,intellij+all,webstorm+all 2 | # Edit at https://www.gitignore.io/?templates=web,node,intellij+all,webstorm+all 3 | 4 | ### Intellij+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Gradle 28 | .idea/**/gradle.xml 29 | .idea/**/libraries 30 | 31 | # Gradle and Maven with auto-import 32 | # When using Gradle or Maven with auto-import, you should exclude module files, 33 | # since they will be recreated, and may cause churn. Uncomment if using 34 | # auto-import. 35 | # .idea/modules.xml 36 | # .idea/*.iml 37 | # .idea/modules 38 | 39 | # CMake 40 | cmake-build-*/ 41 | 42 | # Mongo Explorer plugin 43 | .idea/**/mongoSettings.xml 44 | 45 | # File-based project format 46 | *.iws 47 | 48 | # IntelliJ 49 | out/ 50 | 51 | # mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # JIRA plugin 55 | atlassian-ide-plugin.xml 56 | 57 | # Cursive Clojure plugin 58 | .idea/replstate.xml 59 | 60 | # Crashlytics plugin (for Android Studio and IntelliJ) 61 | com_crashlytics_export_strings.xml 62 | crashlytics.properties 63 | crashlytics-build.properties 64 | fabric.properties 65 | 66 | # Editor-based Rest Client 67 | .idea/httpRequests 68 | 69 | # Android studio 3.1+ serialized cache file 70 | .idea/caches/build_file_checksums.ser 71 | 72 | ### Intellij+all Patch ### 73 | # Ignores the whole .idea folder and all .iml files 74 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 75 | 76 | .idea/ 77 | 78 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 79 | 80 | *.iml 81 | modules.xml 82 | .idea/misc.xml 83 | *.ipr 84 | 85 | # Sonarlint plugin 86 | .idea/sonarlint 87 | 88 | ### Node ### 89 | # Logs 90 | logs 91 | *.log 92 | npm-debug.log* 93 | yarn-debug.log* 94 | yarn-error.log* 95 | 96 | # Runtime data 97 | pids 98 | *.pid 99 | *.seed 100 | *.pid.lock 101 | 102 | # Directory for instrumented libs generated by jscoverage/JSCover 103 | lib-cov 104 | 105 | # Coverage directory used by tools like istanbul 106 | coverage 107 | 108 | # nyc test coverage 109 | .nyc_output 110 | 111 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 112 | .grunt 113 | 114 | # Bower dependency directory (https://bower.io/) 115 | bower_components 116 | 117 | # node-waf configuration 118 | .lock-wscript 119 | 120 | # Compiled binary addons (https://nodejs.org/api/addons.html) 121 | build/Release 122 | 123 | # Dependency directories 124 | node_modules/ 125 | jspm_packages/ 126 | 127 | # TypeScript v1 declaration files 128 | typings/ 129 | 130 | # Optional npm cache directory 131 | .npm 132 | 133 | # Optional eslint cache 134 | .eslintcache 135 | 136 | # Optional REPL history 137 | .node_repl_history 138 | 139 | # Output of 'npm pack' 140 | *.tgz 141 | 142 | # Yarn Integrity file 143 | .yarn-integrity 144 | 145 | # dotenv environment variables file 146 | .env 147 | .env.test 148 | 149 | # parcel-bundler cache (https://parceljs.org/) 150 | .cache 151 | 152 | # next.js build output 153 | .next 154 | 155 | # nuxt.js build output 156 | .nuxt 157 | 158 | # vuepress build output 159 | .vuepress/dist 160 | 161 | # Serverless directories 162 | .serverless/ 163 | 164 | # FuseBox cache 165 | .fusebox/ 166 | 167 | # DynamoDB Local files 168 | .dynamodb/ 169 | 170 | 171 | ### WebStorm+all ### 172 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 173 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 174 | 175 | # User-specific stuff 176 | 177 | # Generated files 178 | 179 | # Sensitive or high-churn files 180 | 181 | # Gradle 182 | 183 | # Gradle and Maven with auto-import 184 | # When using Gradle or Maven with auto-import, you should exclude module files, 185 | # since they will be recreated, and may cause churn. Uncomment if using 186 | # auto-import. 187 | # .idea/modules.xml 188 | # .idea/*.iml 189 | # .idea/modules 190 | 191 | # CMake 192 | 193 | # Mongo Explorer plugin 194 | 195 | # File-based project format 196 | 197 | # IntelliJ 198 | 199 | # mpeltonen/sbt-idea plugin 200 | 201 | # JIRA plugin 202 | 203 | # Cursive Clojure plugin 204 | 205 | # Crashlytics plugin (for Android Studio and IntelliJ) 206 | 207 | # Editor-based Rest Client 208 | 209 | # Android studio 3.1+ serialized cache file 210 | 211 | ### WebStorm+all Patch ### 212 | # Ignores the whole .idea folder and all .iml files 213 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 214 | 215 | 216 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 217 | 218 | 219 | # Sonarlint plugin 220 | 221 | # End of https://www.gitignore.io/api/web,node,intellij+all,webstorm+all 222 | 223 | bundle/ 224 | dist/ 225 | 226 | .DS_Store -------------------------------------------------------------------------------- /src/notebook.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | 6 | import BreadLoaf from 'breadloaf' 7 | import { Cell, ArchivedCell } from './cell' 8 | import { DB } from './db/configure' 9 | 10 | import * as State from './state' 11 | import * as U from './state/update' 12 | 13 | export default class Notebook extends React.PureComponent { 14 | render() { 15 | let { notebook, trash, connect, deltas, config } = this.props 16 | let show_button = notebook.layout.length > 0 || connect.status === 'connected' 17 | let db = DB(connect.active) 18 | let virtualSchema = _.flatMap(notebook.layout, (k) => k.items) 19 | .filter((k) => k.result && k.result.nameable) 20 | .map((k) => [db.reference(k.name || k.suggestedName), k.result.columns]) 21 | 22 | return ( 23 | 32 | } 33 | layout={notebook.layout} 34 | updateLayout={(layout, action, item) => updateLayout(layout, action, item)} 35 | onMoved={(node) => 36 | requestAnimationFrame((e) => State.apply('notebook', 'forceRenderToken', U.inc)) 37 | } 38 | footer={[ 39 | show_button ? : null, 40 | ...trashViewer({ notebook, trash, connect }), 41 |
, 42 | ]} 43 | /> 44 | ) 45 | } 46 | } 47 | 48 | function updateLayout(layout, action, item) { 49 | if (action == 'fork' || action == 'close') { 50 | delete item.name 51 | delete item.suggestedName 52 | if (item.result) delete item.result.nameable 53 | } 54 | 55 | State.batch((_) => { 56 | State.apply('notebook', 'layout', U.replace(layout)) 57 | if (action == 'close') { 58 | // don't add either if it's empty or the same as an existing trashed item 59 | if ( 60 | item.query && 61 | item.query.trim() && 62 | !State.get('trash', 'cells').some((k) => k.query.trim() == item.query.trim()) 63 | ) { 64 | State.apply('trash', 'cells', U.prepend(item)) 65 | } 66 | } 67 | }) 68 | } 69 | 70 | class GinormousAddButton extends React.PureComponent { 71 | render() { 72 | return ( 73 |
addCell()}> 74 | 75 |
76 |
77 |
+
78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | } 85 | 86 | // we need to return an array for react-flip-move, but you can't return 87 | // an array in a real react component, so this is a function which returns 88 | // an array of react elements 89 | 90 | function trashViewer({ notebook, trash, connect }) { 91 | if (trash.cells.length == 0) return [] 92 | 93 | let toggler = ( 94 |
95 | 96 |
State.apply('trash', 'open', U.toggle)}> 97 | {trash.open ? ( 98 |
99 | Collapse Archived Cells ({trash.cells.length}) 100 |
101 | ) : ( 102 |
103 | Open Archived Cells ({trash.cells.length}) 104 |
105 | )} 106 |
107 |
108 |
109 | ) 110 | 111 | let trashItems = !trash.open 112 | ? [] 113 | : trash.cells.map((view) => ( 114 |
115 | 116 |
117 | 118 |
119 |
120 |
121 | )) 122 | return [toggler, ...trashItems] 123 | } 124 | 125 | function uuid() { 126 | return Math.random() 127 | .toString(36) 128 | .slice(3, 10) 129 | } 130 | 131 | export function isEmpty() { 132 | return State.get('notebook', 'layout').length === 0 133 | } 134 | 135 | export function addCell(item = {}) { 136 | State.apply( 137 | 'notebook', 138 | 'layout', 139 | U.append({ 140 | rowId: uuid(), 141 | items: [{ id: uuid(), query: '', loading: false, ...item }], 142 | }) 143 | ) 144 | } 145 | 146 | export function addTrash(item = {}) { 147 | State.apply('trash', 'cells', U.prepend({ id: uuid(), query: '', loading: false, ...item })) 148 | } 149 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | const { app, Menu, BrowserWindow } = require('electron') 2 | const path = require('path') 3 | const url = require('url') 4 | 5 | const WebSocket = require('ws') 6 | const port = parseInt('bat', 36) 7 | 8 | const response = require('./response.js') 9 | 10 | // Keep a global reference of the window object, if you don't, the window will 11 | // be closed automatically when the JavaScript object is garbage collected. 12 | let win 13 | 14 | function createWindow() { 15 | // Create the browser window. 16 | win = new BrowserWindow({ 17 | width: 1100, 18 | height: 650, 19 | titleBarStyle: 'hidden-inset', 20 | webPreferences: { 21 | nodeIntegration: false, 22 | }, 23 | }) 24 | 25 | startServer() 26 | 27 | // win.openDevTools() 28 | 29 | // and load the index.html of the app. 30 | win.loadURL( 31 | url.format({ 32 | pathname: path.join(__dirname, 'bundle/index.html'), 33 | protocol: 'file:', 34 | slashes: true, 35 | }) 36 | ) 37 | 38 | // Open the DevTools. 39 | // win.webContents.openDevTools() 40 | 41 | // Create the Application's main menu 42 | var template = [ 43 | { 44 | label: 'Franchise', 45 | submenu: [ 46 | { 47 | label: 'Quit', 48 | accelerator: 'Command+Q', 49 | click: function() { 50 | app.quit() 51 | }, 52 | }, 53 | ], 54 | }, 55 | { 56 | label: 'Edit', 57 | submenu: [ 58 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, 59 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, 60 | { type: 'separator' }, 61 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, 62 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, 63 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, 64 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }, 65 | ], 66 | }, 67 | ] 68 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)) 69 | 70 | // Emitted when the window is closed. 71 | win.on('closed', () => { 72 | // Dereference the window object, usually you would store windows 73 | // in an array if your app supports multi windows, this is the time 74 | // when you should delete the corresponding element. 75 | win = null 76 | }) 77 | } 78 | 79 | // This method will be called when Electron has finished 80 | // initialization and is ready to create browser windows. 81 | // Some APIs can only be used after this event occurs. 82 | app.on('ready', createWindow) 83 | 84 | // Quit when all windows are closed. 85 | app.on('window-all-closed', () => { 86 | // On macOS it is common for applications and their menu bar 87 | // to stay active until the user quits explicitly with Cmd + Q 88 | if (process.platform !== 'darwin') { 89 | app.quit() 90 | } 91 | }) 92 | 93 | app.on('activate', () => { 94 | // On macOS it's common to re-create a window in the app when the 95 | // dock icon is clicked and there are no other windows open. 96 | if (win === null) { 97 | createWindow() 98 | } 99 | }) 100 | 101 | // In this file you can include the rest of your app's specific main process 102 | // code. You can also put them in separate files and require them here. 103 | 104 | function _asyncToGenerator(fn) { 105 | return function() { 106 | var gen = fn.apply(this, arguments) 107 | return new Promise(function(resolve, reject) { 108 | function step(key, arg) { 109 | try { 110 | var info = gen[key](arg) 111 | var value = info.value 112 | } catch (error) { 113 | reject(error) 114 | return 115 | } 116 | if (info.done) { 117 | resolve(value) 118 | } else { 119 | return Promise.resolve(value).then( 120 | function(value) { 121 | step('next', value) 122 | }, 123 | function(err) { 124 | step('throw', err) 125 | } 126 | ) 127 | } 128 | } 129 | 130 | return step('next') 131 | }) 132 | } 133 | } 134 | 135 | function startServer() { 136 | const wss = new WebSocket.Server({ port }) 137 | console.log('franchise-client listening on port:', port) 138 | wss.on('connection', (ws) => { 139 | console.log('opened connection') 140 | 141 | const ctx = {} 142 | 143 | ws.on( 144 | 'message', 145 | (() => { 146 | var _ref = _asyncToGenerator(function*(message) { 147 | console.log('received:', message) 148 | 149 | message = JSON.parse(message) 150 | const { id } = message 151 | 152 | const res = yield response(message, ctx) 153 | 154 | ws.send(JSON.stringify(Object.assign({ id }, res))) 155 | }) 156 | 157 | return function(_x) { 158 | return _ref.apply(this, arguments) 159 | } 160 | })() 161 | ) 162 | }) 163 | } 164 | -------------------------------------------------------------------------------- /src/db/configure.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import _ from 'lodash' 3 | 4 | import Tabs, { TabPane } from 'rc-tabs' 5 | import TabContent from 'rc-tabs/lib/TabContent' 6 | import InkTabBar from 'rc-tabs/lib/InkTabBar' 7 | import { UnmountClosed } from 'react-collapse' 8 | 9 | import * as State from '../state' 10 | import * as U from '../state/update' 11 | 12 | import * as SQLiteConnector from './sqlite' 13 | import * as PostgresConnector from './postgres' 14 | import * as BigQueryConnector from './bigquery' 15 | 16 | import * as MySQLConnector from './mysql' 17 | import * as MongoConnector from './mongo' 18 | 19 | import * as CartoConnector from './carto' 20 | import * as GraphQLConnector from './graphql' 21 | 22 | const Databases = [ 23 | SQLiteConnector, 24 | PostgresConnector, 25 | MySQLConnector, 26 | MongoConnector, 27 | BigQueryConnector, 28 | GraphQLConnector, 29 | CartoConnector, 30 | 'Microsoft SQL Server', 31 | 'Oracle', 32 | 'IBM DB2', 33 | 'Teradata', 34 | ] 35 | 36 | export default class Configure extends React.PureComponent { 37 | render() { 38 | let { config, connect, empty } = this.props 39 | let db = DB(connect.active) 40 | let connected = connect.status === 'connected' 41 | let connectable = connected || connect.status === 'connecting' 42 | let force_open = empty && !connected 43 | return ( 44 |
45 |
State.apply('config', 'open', U.toggle)} 47 | className={connect.status + ' banner ' + (force_open ? '' : 'can-toggle ')} 48 | > 49 | { 50 | { 51 | connecting: ( 52 |
53 | Connecting to{' '} 54 | {db.name || 'database'} 55 |
56 | ), 57 | connected:
Connected to {db.name}
, 58 | disconnected: ( 59 |
60 | Disconnected from {db.name}{' '} 61 | {connect.error ? ({connect.error}) : null} 62 |
63 | ), 64 | unconfigured:
Connect to a Database
, 65 | }[connect.status] 66 | } 67 | {!force_open ? ( 68 |
69 |
70 | Settings{' '} 71 |