├── 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 |
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 |
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 | [](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 | Click here to open Franchise SQL Notebook
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 |
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 |
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 |
77 |
78 |
79 | ) : null}
80 |
81 |
85 | State.apply('connect', 'active', U.replace(key))}
90 | renderTabBar={() => }
91 | renderTabContent={() => (
92 |
93 | )}
94 | >
95 | {Databases.map((c) => {
96 | if (typeof c == 'string')
97 | return (
98 |
99 | {c}
100 |
101 | )
102 |
103 | return (
104 |
108 | {c.name}{' '}
109 |
110 |
111 | ) : (
112 | c.name
113 | )
114 | }
115 | key={c.key}
116 | disabled={connectable && c !== db}
117 | >
118 | {c.Configure ? (
119 |
120 | ) : (
121 |
122 | No configuration interface defined for {c.name}{' '}
123 | connector
124 |
125 | )}
126 |
127 | )
128 | })}
129 |
130 |
131 |
132 | )
133 | }
134 | }
135 |
136 | export function DB(key) {
137 | return Databases.find((k) => k.key === key)
138 | }
139 |
140 | export function getDB() {
141 | return DB(State.get('connect', 'active'))
142 | }
143 |
--------------------------------------------------------------------------------
/src/state/export.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import React from 'react'
3 | import * as State from '../state'
4 | import * as U from '../state/update'
5 | import swal from 'sweetalert2'
6 |
7 | import { Tooltip, Position, Popover, Menu, MenuItem } from '@blueprintjs/core'
8 | import { getDB } from '../db/configure'
9 |
10 | export default function ExportButton() {
11 | return (
12 |
13 |
14 |
downloadNotebook(false)}
18 | >
19 | Download
20 |
21 |
25 | downloadNotebook(true)}
29 | />
30 |
31 | }
32 | >
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | function dumpCell(cell) {
41 | let dump = {
42 | query: cell.query,
43 | id: cell.id,
44 | error: cell.error && cell.error + '',
45 | suggestedName: cell.suggestedName,
46 | loading: false,
47 | markdown: cell.markdown,
48 | selected: cell.selected,
49 | }
50 | if (cell.result) {
51 | dump.result = {
52 | nameable: cell.result.nameable,
53 | columns: cell.result.columns,
54 | values: (cell.result.values || []).slice(0, 200),
55 | }
56 | }
57 | return dump
58 | }
59 |
60 | async function dumpApplicationState(withCredentials, includeDump) {
61 | let data = State.get()
62 | let dump = {
63 | state: {
64 | ...data,
65 | connect: {
66 | active: data.connect.active,
67 | status: 'unconfigured',
68 | },
69 | config: withCredentials ? data.config : {},
70 | notebook: {
71 | ...data.notebook,
72 | layout: data.notebook.layout.map((k) => ({
73 | ...k,
74 | items: k.items.map(dumpCell),
75 | })),
76 | },
77 | trash: {
78 | ...data.trash,
79 | cells: data.trash.cells.map(dumpCell),
80 | },
81 | },
82 | autoconnect: withCredentials && data.connect.status === 'connected',
83 | version: 2,
84 | }
85 |
86 | if (withCredentials && includeDump && getDB().exportData) {
87 | let db_data = await getDB().exportData()
88 | dump.databaseDump = db_data
89 | console.log(db_data)
90 | }
91 |
92 | return dump
93 | }
94 |
95 | function isEmpty(state) {
96 | if (state.connect.status === 'connected') return false
97 | if (state.notebook.layout.length === 0) return true
98 | if (state.notebook.layout.some((k) => k.items.some((f) => (f.query || '').trim() != '')))
99 | return false
100 | return true
101 | }
102 |
103 | let lastStateDump
104 | async function updateAutosave() {
105 | let nextState = State.get()
106 | if (lastStateDump === nextState) return
107 | lastStateDump = nextState
108 |
109 | if (isEmpty(nextState)) {
110 | delete localStorage.autosave
111 | delete sessionStorage.autosave
112 | } else {
113 | let dumpStr = JSON.stringify(await dumpApplicationState(true, true))
114 | localStorage.autosave = dumpStr
115 | sessionStorage.autosave = dumpStr
116 | }
117 | localStorage.credentials = JSON.stringify(State.get('config', 'credentials'))
118 | localStorage.activeConnector = State.get('connect', 'active')
119 | }
120 |
121 | async function makeURL(withCredentials, title) {
122 | let data = await dumpApplicationState(withCredentials, true)
123 |
124 | var bin_data = JSON.stringify(JSON.stringify(data))
125 | var basename = location.protocol + '//' + location.host + location.pathname
126 | return URL.createObjectURL(
127 | new Blob([
128 | require('raw-loader!./export_template.html')
129 | .replace('{{notebook_name}}', title)
130 | .replace('{{bin_data}}', bin_data)
131 | .replace(
132 | '{{notebook_contents}}',
133 | State.getAll('notebook', 'layout', U.each, 'items', U.each, 'query')
134 | .join('\n\n========================================================n\n')
135 | .replace(/<\/script/g, 'script')
136 | ),
137 | ])
138 | )
139 | }
140 |
141 | async function downloadNotebook(withCredentials) {
142 | let extension = 'html'
143 | let default_name =
144 | new Date().toISOString().slice(0, 10) + (withCredentials ? '-Credentialed' : '')
145 |
146 | const a = document.createElement('a')
147 | a.style.position = 'absolute'
148 | a.style.top = '-10000px'
149 | a.style.left = '-10000px'
150 |
151 | document.body.appendChild(a)
152 |
153 | const prompt = await swal.fire({
154 | input: 'text',
155 | showCancelButton: true,
156 | title: 'Export Notebook' + (withCredentials ? ' (with credentials)' : ''),
157 | inputPlaceholder: default_name,
158 | })
159 | if (!prompt.dismiss) {
160 | let title = prompt.value || default_name
161 | a.setAttribute('download', title.match(/.+\..+/) ? title : title + '.' + extension)
162 | a.setAttribute('href', await makeURL(withCredentials, title))
163 | a.click()
164 |
165 | requestAnimationFrame((e) => a.remove())
166 | }
167 | }
168 |
169 | const AUTOSAVE_INTERVAL = 2718
170 | let autosaveInterval = setInterval(updateAutosave, AUTOSAVE_INTERVAL)
171 | if (module.hot) {
172 | module.hot.dispose(function() {
173 | clearInterval(autosaveInterval)
174 | })
175 | }
176 |
--------------------------------------------------------------------------------
/src/db/bridge.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import _ from 'lodash'
3 |
4 | import { getDB } from './configure'
5 | import { addCell, isEmpty, addTrash } from '../notebook'
6 |
7 | import * as State from '../state'
8 | import * as U from '../state/update'
9 |
10 | import { UnmountClosed } from 'react-collapse'
11 |
12 | const RETRY_INTERVAL = 1000
13 | const BRIDGE_URL = 'ws://localhost:14645'
14 |
15 | let clientConnectorSocket = null
16 | let checkConnectorInterval
17 |
18 | var replyQueue = []
19 | var rejectQueue = []
20 | var connectQueue = []
21 | var messageCounter = 0
22 | var bridgeAlive = true
23 |
24 | function tryOpenBridge() {
25 | try {
26 | clientConnectorSocket = new WebSocket(BRIDGE_URL)
27 | } catch (err) {
28 | State.apply('connect', 'bridge_status', U.replace('mixed_fail'))
29 | }
30 |
31 | if (!clientConnectorSocket) return
32 |
33 | clientConnectorSocket.onopen = (e) => {
34 | // console.log('socket opened')
35 | if (getDB().requires_bridge) {
36 | State.apply('connect', 'bridge_status', U.replace('connected'))
37 | getDB().bridgeConnected(clientConnectorSocket)
38 |
39 | while (connectQueue.length > 0) {
40 | let callback = connectQueue.shift()
41 | try {
42 | callback()
43 | } catch (err) {
44 | console.error(err)
45 | }
46 | }
47 | }
48 | }
49 |
50 | clientConnectorSocket.onmessage = (e) => {
51 | // console.log('got message', e.data)
52 | const data = JSON.parse(e.data)
53 | if (data.error) {
54 | if (data.id in rejectQueue) {
55 | rejectQueue[data.id](new Error(data.error))
56 | } else {
57 | console.error(new Error(data.error))
58 | }
59 | } else {
60 | if (data.id in replyQueue) {
61 | replyQueue[data.id](data)
62 | } else {
63 | console.log('Missing response handler: ', data)
64 | }
65 | }
66 | }
67 |
68 | clientConnectorSocket.onclose = (e) => {
69 | rejectQueue = []
70 | replyQueue = []
71 | messageCounter = 0
72 |
73 | // console.log('socket closed')
74 | if (getDB().requires_bridge) {
75 | if (State.get('connect', 'bridge_status') != 'disconnected') {
76 | State.apply('connect', 'bridge_status', U.replace('disconnected'))
77 | }
78 |
79 | getDB().bridgeDisconnected(clientConnectorSocket)
80 | setTimeout(() => {
81 | if (getDB().requires_bridge && bridgeAlive) {
82 | tryOpenBridge()
83 | } else {
84 | clientConnectorSocket = null
85 | }
86 | }, RETRY_INTERVAL)
87 | }
88 | }
89 | }
90 |
91 | export function disconnectBridge() {
92 | if (clientConnectorSocket) {
93 | clientConnectorSocket.close()
94 | }
95 | }
96 |
97 | export function FranchiseClientConnector({ connect }) {
98 | if (connect.bridge_status == 'mixed_fail') {
99 | return (
100 |
101 |
102 |
Browser Compatibility
103 |
104 | The Franchise web interface connects to a local bridge application to
105 | mediate connections to external databases.
106 |
107 |
108 | Unfortunately , your browser does not support connections between
109 | secure HTTPS websites and desktop applications.
110 |
111 |
112 | We're actively looking into workarounds, but in the mean time, try using{' '}
113 | Google Chrome .
114 |
115 |
116 |
117 | )
118 | }
119 |
120 | return (
121 |
122 |
123 |
Connect the Database Bridge
124 |
125 |
126 | Run npx franchise-client@0.3.0 in your terminal to start the
127 | franchise database bridge.
128 |
129 |
136 |
137 |
138 | These instructions will automatically collapse as soon as the bridge is
139 | detected.
140 |
141 |
142 |
143 | )
144 | }
145 |
146 | export async function sendRequest(packet) {
147 | if (isElectron()) {
148 | return await runElectronQueryCore(packet)
149 | } else {
150 | await blockUntilBridgeSocketReady()
151 | return await sendRequestSocketCore(packet)
152 | }
153 | }
154 |
155 | function sendRequestSocketCore(packet) {
156 | return new Promise((resolve, reject) => {
157 | packet.id = ++messageCounter
158 | replyQueue[packet.id] = resolve
159 | rejectQueue[packet.id] = reject
160 | clientConnectorSocket.send(JSON.stringify(packet))
161 | })
162 | }
163 |
164 | async function blockUntilBridgeSocketReady() {
165 | let isBridgeReady = clientConnectorSocket && clientConnectorSocket.readyState === 1
166 | if (!isBridgeReady) {
167 | // TODO: have some sort of timeout
168 | await new Promise((accept, reject) => connectQueue.push(accept))
169 | }
170 | }
171 |
172 | function isElectron() {
173 | return (
174 | (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') ||
175 | (typeof process !== 'undefined' && process.versions && !!process.versions.electron)
176 | )
177 | }
178 |
179 | // stuff that runs
180 |
181 | if (isElectron()) {
182 | var runElectronQueryCore = window.require('franchise-client')
183 | setTimeout(function() {
184 | State.apply('connect', 'bridge_status', U.replace('connected'))
185 | }, 100)
186 | } else {
187 | checkConnectorInterval = setInterval(function() {
188 | if (!clientConnectorSocket && getDB().requires_bridge) {
189 | tryOpenBridge()
190 | }
191 | }, 100)
192 | }
193 |
194 | if (module.hot) {
195 | module.hot.dispose(function() {
196 | bridgeAlive = false
197 | clearInterval(checkConnectorInterval)
198 | disconnectBridge()
199 | })
200 | }
201 |
--------------------------------------------------------------------------------
/src/app.less:
--------------------------------------------------------------------------------
1 | @import '~normalize.css/normalize.css';
2 | @import '~@blueprintjs/core/dist/blueprint.css';
3 | @import '~@blueprintjs/table/dist/table.css';
4 | @import '~@blueprintjs/labs/dist/blueprint-labs.css';
5 |
6 | @import '~rc-tabs/assets/index.css';
7 | @import '~sweetalert2/dist/sweetalert2.css';
8 |
9 | @import '~codemirror/lib/codemirror.css';
10 | // @import '~codemirror/theme/eclipse.css';
11 | @import './cell/hipster.less';
12 |
13 | @import './bread.css';
14 | @import '~font-awesome/css/font-awesome.css';
15 | @import './cell/cell.less';
16 | @import './db/configure.less';
17 | @import './cell/visualizer.less';
18 |
19 | html,
20 | body {
21 | height: 100%;
22 | }
23 |
24 | body {
25 | // background: #ededf3;
26 | background: hsla(0, 0%, 97%, 1);
27 | -webkit-app-region: drag;
28 | }
29 |
30 | .header {
31 | overflow: auto;
32 |
33 | h1 {
34 | font-weight: 100;
35 | padding: 0;
36 | display: inline;
37 | color: #151515;
38 | }
39 | a:hover {
40 | text-decoration: none;
41 | }
42 | .slogan {
43 | // color: gray;
44 | color: #8c8c8c;
45 | font-weight: 200;
46 | margin-left: 10px;
47 | user-select: none;
48 | cursor: default;
49 | }
50 | // margin: 30px 0;
51 | margin-left: auto;
52 | margin-right: auto;
53 | }
54 |
55 | .header-wrap {
56 | padding: 35px 0;
57 | }
58 |
59 | .export-btn {
60 | float: right;
61 | background: #b9b9b9;
62 | border: none;
63 | padding: 9px 17px;
64 | color: white;
65 | border-radius: 5px;
66 | margin: 0;
67 | cursor: pointer;
68 |
69 | i {
70 | margin-right: 7px;
71 | }
72 | }
73 |
74 | .export-btn:hover {
75 | background: #aaa;
76 | }
77 |
78 | .bread-row.insert-top .divider.divider-top,
79 | .bread-row.insert-bottom .divider.divider-bottom,
80 | .bread-col.insert-left .vertical-divider.divider-left,
81 | .bread-col.insert-right .vertical-divider.divider-right {
82 | background: #7786dc;
83 | opacity: 1;
84 | }
85 |
86 | .fake-slice {
87 | border: 2px dashed #d8d8d8;
88 | border-radius: 4px;
89 | min-height: 30px;
90 | padding-bottom: 20px;
91 | text-align: center;
92 | font-size: 100px;
93 | color: #d8d8d8;
94 | -webkit-user-select: none;
95 | cursor: pointer;
96 | flex-grow: 1;
97 | transition: all 200ms ease-in;
98 | margin: 0 18px;
99 | overflow: hidden;
100 | }
101 |
102 | .fake-slice:hover {
103 | border: 2px dashed gray;
104 | color: gray;
105 | }
106 |
107 | .archived-slice {
108 | .bread-col {
109 | padding: 10px 17px;
110 | }
111 | }
112 |
113 | .toggler {
114 | margin: 0px 20px;
115 | margin-top: 20px;
116 | margin-bottom: 5px;
117 |
118 | -moz-user-select: none;
119 | -webkit-user-select: none;
120 | cursor: default;
121 | }
122 |
123 | .bottom-spacer {
124 | height: 100px;
125 | }
126 |
127 | .pt-tooltip {
128 | box-shadow: none;
129 | }
130 |
131 | .write-queue {
132 | position: fixed;
133 | bottom: 0;
134 | left: 0;
135 | right: 0;
136 | background: #fbf2e9;
137 |
138 | color: #dd9044;
139 | z-index: 10;
140 | border-top: 1px solid #e9b684;
141 |
142 | .toggle-delta {
143 | user-select: none;
144 | padding: 15px 20px;
145 | cursor: pointer;
146 | }
147 | }
148 |
149 | .changes button {
150 | background: none;
151 | border: none;
152 | color: #a50000;
153 | padding-right: 10px;
154 | cursor: pointer;
155 | }
156 |
157 | .collapse {
158 | overflow: auto;
159 | padding: 15px 20px;
160 | padding-bottom: 0;
161 |
162 | .changes {
163 | margin: 10px 0;
164 | font-family: monospace;
165 | background: #fff;
166 | border-radius: 3px;
167 | border: 1px solid #ddd;
168 | padding: 10px;
169 |
170 | .change {
171 | margin-left: 20px;
172 | }
173 | }
174 | }
175 |
176 | .clippy-wrap {
177 | margin: 20px auto;
178 | padding: 0 18px;
179 |
180 | .clippy {
181 | // background: rgba(255,255,255,.3);
182 | // padding: 20px;
183 | border-radius: 3px;
184 | opacity: 0.8;
185 | h2 {
186 | font-size: inherit;
187 | }
188 | // h2 {
189 | // margin-top: 40px;
190 | // &:first-of-type {
191 | // margin-top: 0;
192 | // }
193 | // }
194 |
195 | pre {
196 | word-wrap: break-word;
197 | white-space: pre-wrap;
198 | }
199 |
200 | column-count: 2;
201 | column-width: 300px;
202 | }
203 |
204 | .clippy-contents {
205 | }
206 | }
207 |
208 | .clippy section {
209 | -webkit-column-break-inside: avoid;
210 | overflow: auto;
211 | }
212 |
213 | .first-page {
214 | min-height: 100vh;
215 | }
216 |
217 | .help-page {
218 | background: white;
219 | margin-top: -5px;
220 | border-top: 1px solid #d7d8da;
221 | }
222 |
223 | .help-content {
224 | padding: 20px 0;
225 | }
226 |
227 | .help-tab {
228 | padding: 10px 20px;
229 | background: white;
230 | width: 200px;
231 | width: fit-content;
232 |
233 | text-align: center;
234 | margin-left: auto;
235 | margin-right: auto;
236 | position: relative;
237 | border: 1px solid #d7d8da;
238 | top: -40px;
239 | height: 40px;
240 | border-top-right-radius: 10px;
241 | border-top-left-radius: 10px;
242 | color: #4e4e4e;
243 | border-bottom: none;
244 |
245 | user-select: none;
246 | -webkit-user-select: none;
247 | -moz-user-select: none;
248 |
249 | cursor: pointer;
250 |
251 | .pt-icon-help {
252 | color: #aaa;
253 | margin-right: 7px;
254 | }
255 | }
256 |
257 | .configure {
258 | margin: 0 18px;
259 | }
260 |
261 | .header {
262 | padding: 0 18px;
263 | }
264 |
265 | @media (max-width: 700px) {
266 | .bread-row > div {
267 | display: block;
268 | }
269 |
270 | .vertical-divider {
271 | display: none;
272 | }
273 |
274 | .header-wrap {
275 | padding: 25px 0;
276 | }
277 |
278 | .header h1 {
279 | font-size: 30px;
280 | }
281 |
282 | .slogan {
283 | display: none;
284 | }
285 |
286 | .bread-row .bread-col,
287 | .fake-slice,
288 | .configure {
289 | margin: 0 10px;
290 | }
291 | .clippy-wrap,
292 | .header {
293 | padding: 0 10px;
294 | }
295 | }
296 |
297 | @media (min-width: 800px) {
298 | .row-1,
299 | .configure-wrap,
300 | .clippy-wrap,
301 | .header {
302 | max-width: 90vw;
303 | margin-left: auto;
304 | margin-right: auto;
305 | }
306 | }
307 |
308 | @media (min-width: 1000px) {
309 | .row-1,
310 | .configure-wrap,
311 | .clippy-wrap,
312 | .header {
313 | max-width: 70vw;
314 | margin-left: auto;
315 | margin-right: auto;
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/src/delta.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import React from 'react'
3 |
4 | import { addTrash } from './notebook'
5 | import { runCell } from './cell'
6 |
7 | import * as State from './state'
8 | import * as U from './state/update'
9 |
10 | import { DB, getDB } from './db/configure'
11 | import { UnmountClosed } from 'react-collapse'
12 |
13 | export default function DeltaPane({ deltas, connect }) {
14 | if (deltas.changes.length === 0) return null
15 | if (connect.status !== 'connected') return null
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 | {deltas.loading ? (
24 |
25 |
26 | {' '}
27 |
28 | Applying changes...
29 |
30 | ) : (
31 |
State.apply('deltas', 'open', U.toggle)}
34 | >
35 | {deltas.open ? (
36 |
37 | ▲ Collapse Pending Changes ({deltas.changes.length})
38 |
39 | ) : (
40 |
41 | ▶ Review Pending Changes ({deltas.changes.length})
42 |
43 | )}
44 |
45 | )}
46 |
47 | )
48 | }
49 |
50 | function DeltaReview({ deltas }) {
51 | return (
52 |
53 |
applyDeltas()}
60 | >
61 | Apply Changes
62 |
63 |
64 |
65 |
66 | {deltas.error ?
{deltas.error}
: null}
67 |
68 | {_.map(
69 | _.groupBy(
70 | deltas.changes.map((k, i) => ({ ...k, index: i })),
71 | (k) => k.tableName
72 | ),
73 | (changes, table) => (
74 |
75 |
{table}
76 | {_.map(
77 | _.groupBy(changes, (k) => k.rowPredicate),
78 | (changes, rowPredicate) => (
79 |
80 |
81 | UPDATE "{table}" SET{' '}
82 |
83 | {_.sortBy(changes, 'column').map((change, i) => (
84 |
85 | {
87 | State.apply(
88 | 'deltas',
89 | 'changes',
90 | U.removeIndex(change.index)
91 | )
92 | if (
93 | State.get('deltas', 'changes').length ==
94 | 0
95 | )
96 | State.apply(
97 | 'deltas',
98 | U.merge({
99 | error: null,
100 | open: false,
101 | })
102 | )
103 | }}
104 | >
105 |
106 |
107 | "{change.column}" = '{change.newValue}'
108 | {i < changes.length - 1 ? (
109 | ,
110 | ) : null}
111 |
112 | ))}
113 |
114 | {'\n'}
115 | WHERE {rowPredicate};
116 |
117 |
118 | )
119 | )}
120 |
121 | )
122 | )}
123 |
124 |
125 | )
126 | }
127 |
128 | export async function applyDeltas() {
129 | State.apply('deltas', U.merge({ error: null, open: false, loading: true }))
130 |
131 | let affectedTables = _.uniq(State.get('deltas', 'changes').map((k) => k.tableName))
132 | let affectedCells = State.getAll(
133 | 'notebook',
134 | 'layout',
135 | U.each,
136 | 'items',
137 | U.match((k) => k.result && affectedTables.includes(k.result.tableName)),
138 | 'id'
139 | )
140 |
141 | let db = getDB()
142 | let query = db.assembleDeltaQuery(State.get('deltas'), { useLegacySql: false })
143 | try {
144 | await db.run(query)
145 | } catch (err) {
146 | State.apply('deltas', U.merge({ error: err.message, open: true, loading: false }))
147 | return
148 | }
149 |
150 | await State.batch(async () => {
151 | State.apply('deltas', U.merge({ changes: [], loading: false }))
152 | addTrash({ query: query })
153 | await Promise.all(affectedCells.map((cellId) => runCell(cellId)))
154 | })
155 | }
156 |
--------------------------------------------------------------------------------
/src/db/graphql/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import _ from 'lodash'
3 | import { GraphQLClient } from 'graphql-request'
4 | import * as graphql from 'graphql'
5 |
6 | import * as State from '../../state'
7 | import * as U from '../../state/update'
8 |
9 | import { connectHelper, disconnectDB } from '../generic'
10 | export { disconnectDB, getStagingValue } from '../generic'
11 | import { GraphQLDocs } from './graphql-docs'
12 |
13 | import CodeMirror from 'codemirror'
14 | import 'codemirror/addon/lint/lint'
15 | import 'codemirror-graphql/hint'
16 | import 'codemirror-graphql/lint'
17 | import 'codemirror-graphql/mode'
18 |
19 | export const key = 'graphql'
20 | export const name = 'GraphQL'
21 | export const syntax = 'graphql'
22 |
23 | export class Configure extends React.Component {
24 | render() {
25 | const { connect, config } = this.props
26 |
27 | const credentialHints = {
28 | endpoint: 'endpoint address',
29 | token: 'authorization token (optional)',
30 | }
31 |
32 | let credentials = (config.credentials && config.credentials.graphql) || {}
33 |
34 | const Field = (type, icon, className = '') => (
35 |
36 | {icon ? : null}
37 |
43 | State.apply(
44 | 'config',
45 | 'graphql',
46 | 'credentials',
47 | type,
48 | U.replace(e.target.value)
49 | )
50 | }
51 | placeholder={credentialHints[type]}
52 | />
53 |
54 | )
55 |
56 | return (
57 |
58 |
59 |
{Field('endpoint', 'globe')}
60 |
{Field('token', 'lock')}
61 |
62 | {connect.status != 'connected' ? (
63 | connect.status == 'connecting' ? (
64 | connectDB()}
69 | >
70 | Connect
71 |
72 |
73 | ) : (
74 | connectDB()}
78 | >
79 | Connect
80 |
81 |
82 | )
83 | ) : (
84 | disconnectDB()}
88 | >
89 | Disconnect
90 |
91 |
92 | )}
93 |
94 | {connect.status != 'connected' && (
95 |
96 | Or connect to a sample endpoint{' '}
97 | connectEndpoint('https://graphql-pokemon.now.sh/')}
100 | >
101 | Pokemon Example
102 |
103 |
104 | )}
105 |
106 | )
107 | }
108 | }
109 |
110 | function connectEndpoint(endpoint) {
111 | State.apply('config', 'credentials', 'graphql', 'token', U.replace(''))
112 | State.apply('config', 'credentials', 'graphql', 'endpoint', U.replace(endpoint))
113 | connectDB()
114 | }
115 |
116 | export async function run(query) {
117 | var db = State.get('connect', '_db')
118 | const results = await fetcher({ query })
119 | let result = {}
120 | const data = results.data[Object.keys(results.data)[0]]
121 |
122 | if (data != null) {
123 | result = formatResults(data)
124 | }
125 | result.query = query
126 |
127 | State.apply('connect', 'graphqlschema', U.replace(await getSchema()))
128 | return result
129 | }
130 |
131 | export function reference(name) {
132 | return '#' + name
133 | }
134 |
135 | const database = () => {
136 | const { endpoint, token } = State.get('config', 'credentials', 'graphql')
137 | const options = token ? { headers: { Authorization: `Bearer ${token}` } } : {}
138 | return new GraphQLClient(endpoint, options)
139 | }
140 |
141 | const fetcher = ({ query, variables, operationName, context }) => {
142 | const db = State.get('connect', '_db')
143 |
144 | return db.request(query, variables).then((data) => {
145 | return { data }
146 | })
147 | }
148 |
149 | const getSchema = async () => {
150 | return await fetcher({
151 | query: graphql.introspectionQuery,
152 | })
153 | }
154 |
155 | function formatResults(data) {
156 | if (Array.isArray(data)) {
157 | return {
158 | object: data,
159 | columns: Object.keys(data[0]),
160 | values: data.map((d) => Object.values(d)),
161 | }
162 | } else {
163 | return {
164 | object: data,
165 | columns: Object.keys(data),
166 | values: [Object.values(data)],
167 | }
168 | }
169 | }
170 |
171 | export const buildGQLSchema = _.memoize((result) => {
172 | if (!result) return null
173 | return graphql.buildClientSchema(result.data)
174 | })
175 |
176 | export function Clippy(props) {
177 | return (
178 |
179 |
180 | {props.connect.graphqlschema ? (
181 |
182 | ) : null}
183 |
184 |
185 | )
186 | }
187 |
188 | export async function connectDB() {
189 | console.log('connectDB started')
190 | await connectHelper(async function() {
191 | State.apply('connect', '_db', U.replace(database()))
192 | State.apply('connect', 'graphqlschema', U.replace(await getSchema()))
193 | })
194 | }
195 |
196 | export function CodeMirrorOptions(connect, virtualSchema) {
197 | return {
198 | mode: 'graphql',
199 |
200 | hintOptions: {
201 | hint: CodeMirror.hint.graphql,
202 | schema: buildGQLSchema(connect.graphqlschema),
203 | },
204 | lint: buildGQLSchema(connect.graphqlschema),
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/src/cell/bar_fixed.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import Helpers from 'victory-core/es/victory-util/helpers'
4 | import Collection from 'victory-core/es/victory-util/collection'
5 | import { assign } from 'lodash'
6 | import CommonProps from 'victory-core/es/victory-primitives/common-props.js'
7 | import * as d3Shape from 'd3-shape'
8 |
9 | export default class Bar extends React.Component {
10 | static propTypes = {
11 | ...CommonProps,
12 | datum: PropTypes.object,
13 | horizontal: PropTypes.bool,
14 | padding: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
15 | width: PropTypes.number,
16 | x: PropTypes.number,
17 | y: PropTypes.number,
18 | y0: PropTypes.number,
19 | }
20 |
21 | componentWillMount() {
22 | const { style, path } = this.calculateAttributes(this.props)
23 | this.style = style
24 | this.path = path
25 | }
26 |
27 | shouldComponentUpdate(nextProps) {
28 | const { style, path } = this.calculateAttributes(nextProps)
29 | const { className, datum, horizontal, x, y, y0 } = this.props
30 | if (
31 | !Collection.allSetsEqual([
32 | [className, nextProps.className],
33 | [x, nextProps.x],
34 | [y, nextProps.y],
35 | [y0, nextProps.y0],
36 | [horizontal, nextProps.horizontal],
37 | [path, this.path],
38 | [style, this.style],
39 | [datum, nextProps.datum],
40 | ])
41 | ) {
42 | this.style = style
43 | this.path = path
44 | return true
45 | }
46 | return false
47 | }
48 |
49 | calculateAttributes(props) {
50 | const { datum, active, polar } = props
51 | const stroke = (props.style && props.style.fill) || 'black'
52 | const baseStyle = { fill: 'black', stroke }
53 | const style = Helpers.evaluateStyle(assign(baseStyle, props.style), datum, active)
54 | const width = this.getBarWidth(props, style)
55 | const path = polar ? this.getPolarBarPath(props, width) : this.getBarPath(props, width)
56 | return { style, path }
57 | }
58 |
59 | getPosition(props, width) {
60 | const size = width / 2
61 | const { x, y, y0 } = props
62 | return {
63 | y0: Math.round(y0),
64 | y1: Math.round(y),
65 | x0: Math.round(x - size),
66 | x1: Math.round(x + size),
67 | }
68 | }
69 |
70 | getVerticalBarPath(props, width) {
71 | const { x0, x1, y0, y1 } = this.getPosition(props, width)
72 | return `M ${x0}, ${y0}
73 | L ${x0}, ${y1}
74 | L ${x1}, ${y1}
75 | L ${x1}, ${y0}
76 | L ${x0}, ${y0}
77 | z`
78 | }
79 |
80 | getHorizontalBarPath(props, width) {
81 | const { x0, x1, y0, y1 } = this.getPosition(props, width)
82 | return `M ${y0}, ${x0}
83 | L ${y0}, ${x1}
84 | L ${y1}, ${x1}
85 | L ${y1}, ${x0}
86 | L ${y0}, ${x0}
87 | z`
88 | }
89 |
90 | transformAngle(angle) {
91 | return -1 * angle + Math.PI / 2
92 | }
93 |
94 | getAngularWidth(props, width) {
95 | const { scale } = props
96 | const range = scale.y.range()
97 | const r = Math.max(...range)
98 | const angularRange = Math.abs(scale.x.range()[1] - scale.x.range()[0])
99 | return (width / (2 * Math.PI * r)) * angularRange
100 | }
101 |
102 | getAngle(props, index) {
103 | const { data, scale } = props
104 | const x = data[index]._x1 === undefined ? '_x' : '_x1'
105 | return scale.x(data[index][x])
106 | }
107 |
108 | getStartAngle(props, index) {
109 | const { data, scale } = props
110 | const currentAngle = this.getAngle(props, index)
111 | const angularRange = Math.abs(scale.x.range()[1] - scale.x.range()[0])
112 | const previousAngle =
113 | index === 0
114 | ? this.getAngle(props, data.length - 1) - Math.PI * 2
115 | : this.getAngle(props, index - 1)
116 | return index === 0 && angularRange < 2 * Math.PI
117 | ? scale.x.range()[0]
118 | : (currentAngle + previousAngle) / 2
119 | }
120 |
121 | getEndAngle(props, index) {
122 | const { data, scale } = props
123 | const currentAngle = this.getAngle(props, index)
124 | const angularRange = Math.abs(scale.x.range()[1] - scale.x.range()[0])
125 | const lastAngle =
126 | scale.x.range()[1] === 2 * Math.PI
127 | ? this.getAngle(props, 0) + Math.PI * 2
128 | : scale.x.range()[1]
129 | const nextAngle =
130 | index === data.length - 1
131 | ? this.getAngle(props, 0) + Math.PI * 2
132 | : this.getAngle(props, index + 1)
133 | return index === data.length - 1 && angularRange < 2 * Math.PI
134 | ? lastAngle
135 | : (currentAngle + nextAngle) / 2
136 | }
137 |
138 | getVerticalPolarBarPath(props) {
139 | const { datum, scale, style, index } = props
140 | const r1 = scale.y(datum._y0 || 0)
141 | const r2 = scale.y(datum._y1 !== undefined ? datum._y1 : datum._y)
142 | const currentAngle = scale.x(datum._x1 !== undefined ? datum._x1 : datum._x)
143 | let start
144 | let end
145 | if (style.width) {
146 | const width = this.getAngularWidth(props, style.width)
147 | start = currentAngle - width / 2
148 | end = currentAngle + width / 2
149 | } else {
150 | start = this.getStartAngle(props, index)
151 | end = this.getEndAngle(props, index)
152 | }
153 | const path = d3Shape
154 | .arc()
155 | .innerRadius(r1)
156 | .outerRadius(r2)
157 | .startAngle(this.transformAngle(start))
158 | .endAngle(this.transformAngle(end))
159 | return path()
160 | }
161 |
162 | getBarPath(props, width) {
163 | return this.props.horizontal
164 | ? this.getHorizontalBarPath(props, width)
165 | : this.getVerticalBarPath(props, width)
166 | }
167 |
168 | getPolarBarPath(props) {
169 | // TODO Radial bars
170 | return this.getVerticalPolarBarPath(props)
171 | }
172 |
173 | getBarWidth(props, style) {
174 | if (style.width) {
175 | return style.width
176 | }
177 |
178 | const { scale, data } = props
179 | const range = scale.x.range()
180 | const extent = Math.abs(range[1] - range[0])
181 | const bars = data.length + 2
182 | const barRatio = 0.5
183 | // eslint-disable-next-line no-magic-numbers
184 | const defaultWidth = data.length < 2 ? 8 : (barRatio * extent) / bars
185 | return Math.max(1, Math.round(defaultWidth))
186 | }
187 |
188 | // Overridden in victory-core-native
189 | renderBar(path, style, events) {
190 | const { role, shapeRendering, className, origin, polar } = this.props
191 | const transform = polar && origin ? `translate(${origin.x}, ${origin.y})` : undefined
192 | return (
193 |
202 | )
203 | }
204 |
205 | render() {
206 | return this.renderBar(this.path, this.style, this.props.events)
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/state/README.md:
--------------------------------------------------------------------------------
1 | # Franchise State Management
2 |
3 | ## Why?
4 |
5 | React's native state system works well for simple components with transient state (e.g. whether you're hovering over a button), but it becomes less useful when you want to be able to do things like coordinate state between multiple components or to export/serialize and load back the state. In Franchise's case, we often want to persist the contents of our cells, the results we're visualizing, and etc.
6 |
7 |
8 | A strategy that works for that case is to have a single source of truth at the root of the component hierarchy, and to simply different pieces of that truth to the sub-components through props. We don't want to generally pass all the information to all components, because that eliminates our ability to deliberately not re-render particular subtrees (we don't want to re-render components that don't change for performance reasons).
9 |
10 | In addition, for updates— we could choose to pass each component updater methods for all the pieces of information that they have access to. However, often event handlers and other functions need to be able to modify fairly different parts of the tree than they necessarily need to view.
11 |
12 | This is where the Franchise state management system differs from Redux. To update the state in Redux, you first create an action directly, or with an action creator, that then gets handled by a hierarchy of reducers. For Franchise's system, we just get access to the global state updater and update it however we choose to.
13 |
14 | Also, since we accomplish persistence by serializing the application state— we try to keep it made out of plain objects instead of class values and other tricky stuff.
15 |
16 | For example, the `Cell` component is passed props for view, connect, and deltas — so that when adjacent cells are updated, it doesn't bother re-rendering. Within a render function, no react component has access to state outside of that which it's being passed through its props. Additionally, within a render function we're not allowed to write to the state store (this is just a manifestation of the React no-render-side-effects principle).
17 |
18 | However, within event handlers and other function calls that happen outside of the render loop, we have full access (both read and write) to the entire application state.
19 |
20 | One example of this, is we sometimes need to get access to the current database connector (e.g. SQLite, Postgres, GraphQL, etc). If we're trying to get access within a render method, you'd need to ensure that you're being passed the `.connect.active` part of the state tree and pass it into the `DB()` function (which simply looks up the instance corresponding to that ID). However, if you're in some event handler, or some other piece of code that isn't explicitly in the render domain, you can simply call getDB() (which reads `.connect.active` from the global state store).
21 |
22 |
23 | ## State Overview
24 |
25 | ```
26 | config
27 | graphql
28 | credentials
29 | endpoint: "http://whatever"
30 | sqlite
31 | credentials
32 | etc...
33 | open: true (whether or not the config bar at the top is expanded)
34 |
35 | connect
36 | active: "sqlite" (the ID of the database connector that is currently in use)
37 | status: "connected"
38 | error: null
39 |
40 | deltas
41 | changes: [] (a list of changes that that the user has created with the WYSIWYG interface, that can be committed to the database with a synthesized query)
42 | open: false (whether or not the dialog is expanded)
43 |
44 | notebook
45 | layout[]
46 | items[]
47 | error: null (errors associated with running this particular query)
48 | id: "sdfe8"
49 | query: "sdf"
50 | selected: "table" (this is the name of the selected view widget)
51 | suggestedName: (this is the name of the query that was automatically generated— used for referencing this query from other queries)
52 | name: (this is the user-input name of a query— used for referencing this query from other queries)
53 | result
54 | columns: (array of columns)
55 | values: (array of rows, each containing an array of values for each column)
56 |
57 | trash
58 | open: false (whether or not the trash view/archived cells pane is expanded)
59 | cells
60 | (the same stuff as within notebook.layout.items)
61 | ```
62 |
63 | If you're developing with franchise, you can open up the developer console and run STATE.get() to access the current global application state. Outside of the developer console, you'd access with with "State" (and you'll need to import it first)
64 |
65 |
66 | ## Combinator System
67 |
68 | The particular way we retrieve and modify state is with a little combinator language— you don't generally need to understand the complete mechanics of it in order to use it (but it's a pretty neat and powerful way of accessing stuff— someone who knows math/haskell better would probably call it a variation of a "Lens").
69 |
70 | In cell/index.js we have 3 notable functions: cellById, updateCell, and getCell. Note that these functions can only be used outside a render function (event handlers are allowed— as it's not actually being called in render time). You can usually just get by with updateCell(cellId, fieldsToChange) — this basically works the way this.setState does in react components (and it'll automatically trigger a re-render of the app). And likewise, you can use getCell(cellId) if you want to read the contents without explicitly writing to it.
71 |
72 | ```
73 | export function cellById(cellId){
74 | return ['notebook', 'layout', U.each, 'items', U.id(cellId)]
75 | }
76 |
77 | export function updateCell(cellId, update){
78 | State.apply(...cellById(cellId), U.merge(update))
79 | }
80 |
81 | export function getCell(cellId){
82 | return State.get(...cellById(cellId))
83 | }
84 | ```
85 |
86 |
87 | Essentially we have three methods— `State.getAll`, `State.get` and `State.apply`. `State.get` is just shorthand for `State.getAll()[0]`. Each of those arguments is a little function (a "combinator") that chains together to select 0 or more bits of state based on the previous matches. For example, if we look at how cellById works— we start off with "notebook" which is just a shorthand for U.safe_key("notebook").
88 |
89 | U.safe_key("notebook") returns a function where— if the input is an object, it'll return .notebook— and otherwise null. When we chain U.safe_key("notebook"), U.safe_key("layout") — that's essentially equivalent to .notebook.layout (with the notable difference that it doesn't throw "can not read property of undefined" if notebook doesn't exist). However, chaining doesn't just have to pass a single item forward— in fact it can pass an unlimited number of possibilities forward. So when it gets to U.each— that means at this point we've selected all the different rows of the layout. Then for each of these rows, we select all the columns with U.safe_key("items"). And finally we filter by objects which have an "id" property that matches cellId.
90 |
91 | When using `State.apply` your last argument can be something like U.merge(obj), or U.replace(obj) — where merge merges those fields into the elements at the cursor, and `replace` replaces it altogether. In fact you can also do a custom function which is just an arbitrary reducer: if you've selected a set of numbers that you want to increment, just pass in x => x + 1 as your last argument to increment them all.
92 |
93 | It's a rather powerful system. For instance, if you'd like to prepend the word "hello" to every cell on every other row in the notebook, you could run
94 |
95 | ```
96 | State.apply("notebook", "layout", U.match((k, i) => i % 2 == 0), "items", "query", k => "hello " + k)
97 | ```
98 |
99 | You can see the definition of the combinator system at (state/update.js). The core of it is only 10 lines.
--------------------------------------------------------------------------------
/data/geo_states.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS geo_states;
2 | CREATE TABLE "geo_states" (
3 | "name" text,
4 | "abv" text,
5 | "country" text,
6 | "is_state" text,
7 | "is_lower48" text,
8 | "slug" text,
9 | "latitude" real,
10 | "longitude" real,
11 | "population" integer,
12 | "area" real,
13 | PRIMARY KEY ("abv", "country")
14 | );
15 |
16 |
17 | INSERT INTO "geo_states" ("name","abv","country","is_state","is_lower48","slug","latitude","longitude","population","area") VALUES
18 | ('Alabama','AL','US','y','y','alabama',32.806671,-86.791130,4779736,50744.00),
19 | ('Alaska','AK','US','y','n','alaska',61.370716,-152.404419,710231,571951.25),
20 | ('Arizona','AZ','US','y','y','arizona',33.729759,-111.431221,6392017,113634.57),
21 | ('Arkansas','AR','US','y','y','arkansas',34.969704,-92.373123,2915918,52068.17),
22 | ('California','CA','US','y','y','california',36.116203,-119.681564,37253956,155939.52),
23 | ('Colorado','CO','US','y','y','colorado',39.059811,-105.311104,5029196,103717.53),
24 | ('Connecticut','CT','US','y','y','connecticut',41.597782,-72.755371,3574097,4844.80),
25 | ('Delaware','DE','US','y','y','delaware',39.318523,-75.507141,897934,1953.56),
26 | ('District of Columbia','DC','US','n','n','district-of-columbia',38.897438,-77.026817,601723,68.34),
27 | ('Florida','FL','US','y','y','florida',27.766279,-81.686783,18801310,53926.82),
28 | ('Georgia','GA','US','y','y','georgia',33.040619,-83.643074,9687653,57906.14),
29 | ('Hawaii','HI','US','y','n','hawaii',21.094318,-157.498337,1360301,6422.62),
30 | ('Idaho','ID','US','y','y','idaho',44.240459,-114.478828,1567582,82747.21),
31 | ('Illinois','IL','US','y','y','illinois',40.349457,-88.986137,12830632,55583.58),
32 | ('Indiana','IN','US','y','y','indiana',39.849426,-86.258278,6483802,35866.90),
33 | ('Iowa','IA','US','y','y','iowa',42.011539,-93.210526,3046355,55869.36),
34 | ('Kansas','KS','US','y','y','kansas',38.526600,-96.726486,2853118,81814.88),
35 | ('Kentucky','KY','US','y','y','kentucky',37.668140,-84.670067,4339367,39728.18),
36 | ('Louisiana','LA','US','y','y','louisiana',31.169546,-91.867805,4533372,43561.85),
37 | ('Maine','ME','US','y','y','maine',44.693947,-69.381927,1328361,30861.55),
38 | ('Maryland','MD','US','y','y','maryland',39.063946,-76.802101,5773552,9773.82),
39 | ('Massachusetts','MA','US','y','y','massachusetts',42.230171,-71.530106,6547629,7840.02),
40 | ('Michigan','MI','US','y','y','michigan',43.326618,-84.536095,9883640,56803.82),
41 | ('Minnesota','MN','US','y','y','minnesota',45.694454,-93.900192,5303925,79610.08),
42 | ('Mississippi','MS','US','y','y','mississippi',32.741646,-89.678696,2967297,46906.96),
43 | ('Missouri','MO','US','y','y','missouri',38.456085,-92.288368,5988927,68885.93),
44 | ('Montana','MT','US','y','y','montana',46.921925,-110.454353,989415,145552.44),
45 | ('Nebraska','NE','US','y','y','nebraska',41.125370,-98.268082,1826341,76872.41),
46 | ('Nevada','NV','US','y','y','nevada',38.313515,-117.055374,2700551,109825.99),
47 | ('New Hampshire','NH','US','y','y','new-hampshire',43.452492,-71.563896,1316470,8968.10),
48 | ('New Jersey','NJ','US','y','y','new-jersey',40.298904,-74.521011,8791894,7417.34),
49 | ('New Mexico','NM','US','y','y','new-mexico',34.840515,-106.248482,2059179,121355.53),
50 | ('New York','NY','US','y','y','new-york',42.165726,-74.948051,19378102,47213.79),
51 | ('North Carolina','NC','US','y','y','north-carolina',35.630066,-79.806419,9535483,48710.88),
52 | ('North Dakota','ND','US','y','y','north-dakota',47.528912,-99.784012,672591,68975.93),
53 | ('Ohio','OH','US','y','y','ohio',40.388783,-82.764915,11536504,40948.38),
54 | ('Oklahoma','OK','US','y','y','oklahoma',35.565342,-96.928917,3751351,68667.06),
55 | ('Oregon','OR','US','y','y','oregon',44.572021,-122.070938,3831074,95996.79),
56 | ('Pennsylvania','PA','US','y','y','pennsylvania',40.590752,-77.209755,12702379,44816.61),
57 | ('Rhode Island','RI','US','y','y','rhode-island',41.680893,-71.511780,1052567,1044.93),
58 | ('South Carolina','SC','US','y','y','south-carolina',33.856892,-80.945007,4625364,30109.47),
59 | ('South Dakota','SD','US','y','y','south-dakota',44.299782,-99.438828,814180,75884.64),
60 | ('Tennessee','TN','US','y','y','tennessee',35.747845,-86.692345,6346105,41217.12),
61 | ('Texas','TX','US','y','y','texas',31.054487,-97.563461,25145561,261797.12),
62 | ('Utah','UT','US','y','y','utah',40.150032,-111.862434,2763885,82143.65),
63 | ('Vermont','VT','US','y','y','vermont',44.045876,-72.710686,625741,9249.56),
64 | ('Virginia','VA','US','y','y','virginia',37.769337,-78.169968,8001024,39594.07),
65 | ('Washington','WA','US','y','y','washington',47.400902,-121.490494,6724540,66544.06),
66 | ('West Virginia','WV','US','y','y','west-virginia',38.491226,-80.954453,1852994,24077.73),
67 | ('Wisconsin','WI','US','y','y','wisconsin',44.268543,-89.616508,5686986,54310.10),
68 | ('Wyoming','WY','US','y','y','wyoming',42.755966,-107.302490,563626,97100.40),
69 | ('Aguascalientes','AG','MX',NULL,NULL,'aguascalientes',NULL,NULL,NULL,NULL),
70 | ('Baja California','BC','MX',NULL,NULL,'baja-california',NULL,NULL,NULL,NULL),
71 | ('Baja California Sur','BS','MX',NULL,NULL,'baja-california-sur',NULL,NULL,NULL,NULL),
72 | ('Campeche','CM','MX',NULL,NULL,'campeche',NULL,NULL,NULL,NULL),
73 | ('Chiapas','CS','MX',NULL,NULL,'chiapas',NULL,NULL,NULL,NULL),
74 | ('Chihuahua','CH','MX',NULL,NULL,'chihuahua',NULL,NULL,NULL,NULL),
75 | ('Coahuila','CO','MX',NULL,NULL,'coahuila',NULL,NULL,NULL,NULL),
76 | ('Colima','CL','MX',NULL,NULL,'colima',NULL,NULL,NULL,NULL),
77 | ('Durango','DG','MX',NULL,NULL,'durango',NULL,NULL,NULL,NULL),
78 | ('Federal District','DF','MX',NULL,NULL,'federal-district',NULL,NULL,NULL,NULL),
79 | ('Guanajuato','GT','MX',NULL,NULL,'guanajuato',NULL,NULL,NULL,NULL),
80 | ('Guerrero','GR','MX',NULL,NULL,'guerrero',NULL,NULL,NULL,NULL),
81 | ('Hidalgo','HG','MX',NULL,NULL,'hidalgo',NULL,NULL,NULL,NULL),
82 | ('Jalisco','JA','MX',NULL,NULL,'jalisco',NULL,NULL,NULL,NULL),
83 | ('Mexico State','ME','MX',NULL,NULL,'mexico-state',NULL,NULL,NULL,NULL),
84 | ('Michoacán','MI','MX',NULL,NULL,'michoacan',NULL,NULL,NULL,NULL),
85 | ('Morelos','MO','MX',NULL,NULL,'morelos',NULL,NULL,NULL,NULL),
86 | ('Nayarit','NA','MX',NULL,NULL,'nayarit',NULL,NULL,NULL,NULL),
87 | ('Nuevo León','NL','MX',NULL,NULL,'nuevo-leon',NULL,NULL,NULL,NULL),
88 | ('Oaxaca','OA','MX',NULL,NULL,'oaxaca',NULL,NULL,NULL,NULL),
89 | ('Puebla','PB','MX',NULL,NULL,'puebla',NULL,NULL,NULL,NULL),
90 | ('Querétaro','QE','MX',NULL,NULL,'queretaro',NULL,NULL,NULL,NULL),
91 | ('Quintana Roo','QR','MX',NULL,NULL,'quintana-roo',NULL,NULL,NULL,NULL),
92 | ('San Luis Potosí','SL','MX',NULL,NULL,'san-luis-potosi',NULL,NULL,NULL,NULL),
93 | ('Sinaloa','SI','MX',NULL,NULL,'sinaloa',NULL,NULL,NULL,NULL),
94 | ('Sonora','SO','MX',NULL,NULL,'sonora',NULL,NULL,NULL,NULL),
95 | ('Tabasco','TB','MX',NULL,NULL,'tabasco',NULL,NULL,NULL,NULL),
96 | ('Tamaulipas','TM','MX',NULL,NULL,'tamaulipas',NULL,NULL,NULL,NULL),
97 | ('Tlaxcala','TL','MX',NULL,NULL,'tlaxcala',NULL,NULL,NULL,NULL),
98 | ('Veracruz','VE','MX',NULL,NULL,'veracruz',NULL,NULL,NULL,NULL),
99 | ('Yucatán','YU','MX',NULL,NULL,'yucatan',NULL,NULL,NULL,NULL),
100 | ('Zacatecas','ZA','MX',NULL,NULL,'zacatecas',NULL,NULL,NULL,NULL),
101 | ('Alberta','AB','CA',NULL,NULL,'alberta',NULL,NULL,NULL,NULL),
102 | ('British Columbia','BC','CA',NULL,NULL,'british-columbia',NULL,NULL,NULL,NULL),
103 | ('Manitoba','MB','CA',NULL,NULL,'manitoba',NULL,NULL,NULL,NULL),
104 | ('New Brunswick','NB','CA',NULL,NULL,'new-brunswick',NULL,NULL,NULL,NULL),
105 | ('Newfoundland and Labrador','NL','CA',NULL,NULL,'newfoundland-and-labrador',NULL,NULL,NULL,NULL),
106 | ('Northwest Territories','NT','CA',NULL,NULL,'northwest-territories',NULL,NULL,NULL,NULL),
107 | ('Nova Scotia','NS','CA',NULL,NULL,'nova-scotia',NULL,NULL,NULL,NULL),
108 | ('Nunavut','NU','CA',NULL,NULL,'nunavut',NULL,NULL,NULL,NULL),
109 | ('Ontario','ON','CA',NULL,NULL,'ontario',NULL,NULL,NULL,NULL),
110 | ('Prince Edward Island','PE','CA',NULL,NULL,'prince-edward-island',NULL,NULL,NULL,NULL),
111 | ('Quebec','QC','CA',NULL,NULL,'quebec',NULL,NULL,NULL,NULL),
112 | ('Saskatchewan','SK','CA',NULL,NULL,'saskatchewan',NULL,NULL,NULL,NULL),
113 | ('Yukon','YT','CA',NULL,NULL,'yukon',NULL,NULL,NULL,NULL);
--------------------------------------------------------------------------------
/src/cell/cell.less:
--------------------------------------------------------------------------------
1 | .pt-popover-content {
2 | max-width: 400px;
3 | }
4 |
5 | .input-wrap {
6 | display: flex;
7 | flex-shrink: 0;
8 | position: relative;
9 | // background: whitesmoke;
10 |
11 | .name {
12 | position: absolute;
13 | white-space: nowrap;
14 | right: 5px;
15 | bottom: 5px;
16 | display: none;
17 | color: rgb(101, 124, 160);
18 | z-index: 11;
19 | &.shown {
20 | display: block;
21 | }
22 | .pt-editable-editing {
23 | color: black;
24 | }
25 | }
26 |
27 | .ReactCodeMirror {
28 | flex-grow: 1;
29 | width: 0;
30 | background: white;
31 | }
32 |
33 | .CodeMirror-lines {
34 | padding: 10px 0;
35 | min-height: 90px;
36 | }
37 |
38 | .CodeMirror-scroll {
39 | max-height: 600px;
40 | }
41 |
42 | .CodeMirror pre.CodeMirror-placeholder {
43 | // color: #999;
44 | // font-family: Avenir, Helvetica, sans-serif;
45 | // padding-top: 3px;
46 |
47 | color: #999;
48 | font-family: Avenir, Helvetica, sans-serif;
49 | /* padding-top: 9px; */
50 | text-align: center;
51 | top: 5px;
52 | font-size: 15px;
53 | }
54 |
55 | .CodeMirror pre {
56 | padding: 0 15px;
57 | box-shadow: none;
58 | }
59 |
60 | .CodeMirror {
61 | height: auto;
62 | border-radius: 4px;
63 | font-family: Menlo, Monaco, monospace;
64 | }
65 |
66 | .cm-matchhighlight {
67 | border: 1px solid #b9b9b9;
68 | border-radius: 2px;
69 | margin: -1px;
70 | }
71 |
72 | .CodeMirror-completion {
73 | color: #aaa;
74 | // .prefix {
75 | // text-decoration: underline;
76 | // }
77 | }
78 |
79 | .CodeMirror-placeholder {
80 | color: #aaa;
81 | }
82 |
83 | .CodeMirror-matchingbracket {
84 | color: black !important;
85 | border-bottom: 1px solid gray;
86 | outline: none !important;
87 | }
88 |
89 | .cm-s-eclipse .CodeMirror-activeline-background {
90 | background: rgba(237, 237, 243, 0.45);
91 | }
92 |
93 | // .CodeMirror-activeline-background {
94 | // background: none;
95 | // }
96 |
97 | // .CodeMirror-activeline-background.CodeMirror-linebackground:before {
98 | // content: "▸";
99 | // margin-left: -3px;
100 | // color: #d8d8d8;
101 | // }
102 |
103 | .cm-m-sql.cm-keyword {
104 | text-transform: uppercase;
105 | font-weight: bold;
106 | }
107 | }
108 |
109 | .stale .CodeMirror-lines {
110 | background: repeating-linear-gradient(
111 | 130deg,
112 | #ffffff,
113 | #ffffff 0.3em /* black stripe */,
114 | #f7f7f7 0,
115 | #f7f7f7 0.6em /* blue stripe */
116 | );
117 | }
118 |
119 | .stale .CodeMirror-focused .CodeMirror-lines {
120 | background: white;
121 | }
122 |
123 | .slice {
124 | border: 1px solid #d8d8d8;
125 | border-radius: 4px;
126 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
127 | -webkit-app-region: no-drag;
128 | }
129 |
130 | .bread-col.dragging * {
131 | // opacity: 0.7;
132 | cursor: move;
133 | }
134 |
135 | .slice .controls {
136 | /* position: absolute; */
137 | text-align: center;
138 |
139 | border-top-right-radius: 4px;
140 | border-bottom-right-radius: 4px;
141 |
142 | cursor: move;
143 | flex-shrink: 0;
144 | /* right: 10px; */
145 | /* top: 6px; */
146 | border-left: 1px solid #dfdfdf;
147 | width: 40px;
148 | padding: 4px;
149 |
150 | background: #f5f8fa;
151 |
152 | display: flex;
153 | flex-direction: column;
154 |
155 | &.stale {
156 | // background: repeating-linear-gradient(130deg, #eaeaea, #eaeaea 0.3em /* black stripe */, #f5f8fa 0, #f5f8fa 0.6em /* blue stripe */ );
157 | // border-left: 1px dashed #dfdfdf;
158 | }
159 | .spacer {
160 | flex-grow: 1;
161 | }
162 |
163 | .runnable.fa-play {
164 | color: #82b982;
165 | }
166 |
167 | .pt-icon-code {
168 | color: #377fea;
169 | }
170 |
171 | .fa-exclamation-triangle {
172 | color: orange;
173 | }
174 |
175 | button:hover .fa-play {
176 | color: green;
177 | }
178 |
179 | button:hover .fa-close {
180 | color: maroon;
181 | }
182 |
183 | button:hover .fa-clone {
184 | color: purple;
185 | }
186 | }
187 |
188 | .slice.markdown .controls {
189 | background: white;
190 | border-left: 1px solid white;
191 | transition: all 0.1s linear;
192 | }
193 |
194 | .slice.markdown .controls:hover,
195 | .bread-col.dragging .controls {
196 | background: #f5faf6;
197 | border-left: 1px solid #dfdfdf;
198 | }
199 |
200 | i.fa {
201 | outline: none;
202 | }
203 |
204 | .slice .controls button {
205 | background: transparent;
206 | border: 0;
207 | /* margin-left: 5px; */
208 | cursor: pointer;
209 | vertical-align: middle;
210 | color: #aaa;
211 | padding: 8px;
212 | outline: none;
213 | text-align: center;
214 | }
215 |
216 | .slice .header .button-toggle {
217 | font-size: 11px;
218 | margin-top: 5px;
219 | display: inline-block;
220 | position: relative;
221 | cursor: pointer;
222 | color: gray;
223 | margin-right: 5px;
224 | -webkit-user-select: none;
225 | }
226 |
227 | .slice .header .button-toggle.active {
228 | color: purple;
229 | }
230 |
231 | .slice .header .button:hover {
232 | color: black;
233 | }
234 |
235 | .slice .header .button.active {
236 | color: purple;
237 | }
238 |
239 | .schema-token {
240 | background: rgba(0, 172, 193, 0.05);
241 | border-radius: 2px;
242 | padding: 0 2px;
243 | color: #0015c4;
244 | border: 1px solid rgba(0, 0, 255, 0.1);
245 | -webkit-user-select: none;
246 | user-select: none;
247 | cursor: pointer;
248 | }
249 |
250 | .slice-editor-widget {
251 | position: absolute;
252 | bottom: 3px;
253 | left: 0;
254 | text-align: center;
255 | width: 100%;
256 | z-index: 5;
257 | opacity: 1;
258 | line-height: 35px;
259 |
260 | transition: opacity 0.3s linear;
261 |
262 | .token {
263 | border: 1px solid #56ccf2;
264 | background: #e4f8ff;
265 | border-radius: 20px;
266 | display: inline;
267 | color: #56ccf2;
268 | font-family: sans-serif;
269 | padding: 5px 10px;
270 | margin: 0 5px;
271 | cursor: pointer;
272 | }
273 |
274 | .token.create-table {
275 | border: 1px solid #ff7373;
276 | background: #ffeddb;
277 | color: #ff7373;
278 | }
279 |
280 | .token.create-text {
281 | border: 1px solid #828282;
282 | background: #ececec;
283 | color: #909090;
284 | }
285 |
286 | &.slice-hidden {
287 | opacity: 0;
288 | z-index: -1;
289 | pointer-events: none;
290 | }
291 | }
292 |
293 | .pt-menu-item-label {
294 | max-width: 200px;
295 | text-overflow: ellipsis;
296 | overflow: hidden;
297 | }
298 |
299 | // .stale .fa-play, .bouncing {
300 | // animation-duration: .5s;
301 | // animation-fill-mode: both;
302 | // animation-timing-function: linear;
303 | // animation-iteration-count: infinite;
304 | // animation-name: bounce;
305 | // }
306 |
307 | // @keyframes bounce {
308 | // 0%, 100% {
309 | // transform: translateY(1px);
310 | // }
311 | // 50% {
312 | // transform: translateY(-2px);
313 | // }
314 | // }
315 |
316 | .archived-slice {
317 | .CodeMirror-lines {
318 | background: #f5f4ed;
319 | }
320 |
321 | .controls {
322 | background: #f3edea;
323 | cursor: default;
324 | }
325 | button:hover .fa-trash {
326 | color: maroon;
327 | }
328 | button:hover .fa-level-up {
329 | color: green;
330 | }
331 | }
332 |
333 | .controls button.selected .Halo,
334 | .controls button.selected .point {
335 | fill: #8e8ec1;
336 | }
337 |
338 | .controls button:hover .Halo,
339 | .controls button:hover .point {
340 | fill: #f24440;
341 | }
342 |
--------------------------------------------------------------------------------
/src/db/img/mongodb.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/db/img/sqlite.svg:
--------------------------------------------------------------------------------
1 |
2 | SQLite370
3 | Created using Figma
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/db/img/mysql.svg:
--------------------------------------------------------------------------------
1 |
2 | mysql
3 | Created using Figma
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/response.js:
--------------------------------------------------------------------------------
1 | let createClient = (() => {
2 | var _ref3 = _asyncToGenerator(function*(db, credentials) {
3 | if (db === 'postgres') return yield createPostgresClient(credentials)
4 | if (db === 'bigquery') return yield createBigQueryClient(credentials)
5 | if (db === 'mysql') return yield createMySQLClient(credentials)
6 | throw new Error('database ' + db + ' not recognized')
7 | })
8 |
9 | return function createClient(_x4, _x5) {
10 | return _ref3.apply(this, arguments)
11 | }
12 | })()
13 |
14 | let createMySQLClient = (() => {
15 | var _ref4 = _asyncToGenerator(function*(credentials) {
16 | const client = yield mysql.createConnection(credentials)
17 | return {
18 | query(sql) {
19 | return _asyncToGenerator(function*() {
20 | const [rows, fields] = yield client.execute(sql)
21 | console.log(rows, fields)
22 | if (fields) {
23 | const field_list = fields.map(function(k) {
24 | return k.name
25 | })
26 | return {
27 | columns: field_list,
28 | values: rows.map(function(row) {
29 | return field_list.map(function(k) {
30 | return row[k]
31 | })
32 | }),
33 | }
34 | } else {
35 | return {
36 | columns: ['result'],
37 | values: [[rows]],
38 | }
39 | }
40 | })()
41 | },
42 | close() {
43 | return _asyncToGenerator(function*() {
44 | return yield client.end()
45 | })()
46 | },
47 | }
48 | })
49 |
50 | return function createMySQLClient(_x6) {
51 | return _ref4.apply(this, arguments)
52 | }
53 | })()
54 |
55 | let createPostgresClient = (() => {
56 | var _ref5 = _asyncToGenerator(function*(credentials) {
57 | const client = new PostgresClient(credentials)
58 | ;[1082, 1114, 1184].forEach(function(oid) {
59 | return client.setTypeParser(oid, function(val) {
60 | return val
61 | })
62 | })
63 | yield client.connect()
64 | return {
65 | query(sql) {
66 | return _asyncToGenerator(function*() {
67 | let results = yield client.query({
68 | text: sql,
69 | rowMode: 'array',
70 | })
71 | if (Array.isArray(results)) {
72 | results = results[results.length - 1]
73 | }
74 | // console.log(results.rows, results)
75 | if (results.rows.length > 10000)
76 | throw new Error(
77 | 'Too many result rows to serialize: Try using a LIMIT statement.'
78 | )
79 | return results
80 | })()
81 | },
82 | close: client.end.bind(client),
83 | }
84 | })
85 |
86 | return function createPostgresClient(_x7) {
87 | return _ref5.apply(this, arguments)
88 | }
89 | })()
90 |
91 | function _asyncToGenerator(fn) {
92 | return function() {
93 | var gen = fn.apply(this, arguments)
94 | return new Promise(function(resolve, reject) {
95 | function step(key, arg) {
96 | try {
97 | var info = gen[key](arg)
98 | var value = info.value
99 | } catch (error) {
100 | reject(error)
101 | return
102 | }
103 | if (info.done) {
104 | resolve(value)
105 | } else {
106 | return Promise.resolve(value).then(
107 | function(value) {
108 | step('next', value)
109 | },
110 | function(err) {
111 | step('throw', err)
112 | }
113 | )
114 | }
115 | }
116 | return step('next')
117 | })
118 | }
119 | }
120 |
121 | const { Client: PostgresClient } = require('pg')
122 | const mysql = require('mysql2/promise')
123 | const BigQueryClient = require('@google-cloud/bigquery')
124 | const tmp = require('tmp')
125 | const fs = require('fs')
126 |
127 | const credentials = require('./credentials.js')
128 |
129 | const localCtx = {}
130 | module.exports = (() => {
131 | var _ref = _asyncToGenerator(function*(message, ctx = localCtx) {
132 | const { action, id } = message
133 |
134 | try {
135 | if (action === 'open') {
136 | const { credentials, db } = message
137 |
138 | ctx.client = yield createClient(db, credentials)
139 | return { ready: true }
140 | } else if (action === 'exec') {
141 | const { sql } = message
142 |
143 | const results = yield ctx.client.query(sql, message)
144 | return { results }
145 | } else if (action === 'close') {
146 | yield ctx.client.close()
147 |
148 | return { closed: true }
149 | } else if (action == 'get_postgres_credentials') {
150 | return credentials
151 | } else if (action == 'get_bigquery_schema') {
152 | const get = (() => {
153 | var _ref2 = _asyncToGenerator(function*(o, prop, ...rest) {
154 | return typeof prop === 'undefined'
155 | ? o
156 | : typeof o[prop] === 'function'
157 | ? get(yield o[prop](), ...rest)
158 | : Array.isArray(o[prop])
159 | ? Promise.all(
160 | o[prop].map(function(sub) {
161 | return get(sub, ...rest)
162 | })
163 | )
164 | : typeof prop === 'function'
165 | ? get(yield prop(o), ...rest)
166 | : new Error('not found: ' + o + ' ' + prop)
167 | })
168 |
169 | return function get(_x2, _x3) {
170 | return _ref2.apply(this, arguments)
171 | }
172 | })()
173 |
174 | const flatten = function(arr, result = []) {
175 | arr.forEach(function(value) {
176 | return Array.isArray(value) ? flatten(value, result) : result.push(value)
177 | })
178 | return result
179 | }
180 |
181 | const raw = yield get(
182 | ctx.client,
183 | 'getDatasets',
184 | 0,
185 | 'getTables',
186 | 0,
187 | 'getMetadata',
188 | function(metadata) {
189 | return metadata[0]
190 | }
191 | )
192 |
193 | const schema = flatten(raw).map(function(table) {
194 | return {
195 | schema: table.tableReference.datasetId,
196 | name: table.tableReference.tableId,
197 | columns: table.schema.fields.map(function(f) {
198 | return f.name
199 | }),
200 | }
201 | })
202 |
203 | return { schema }
204 | } else {
205 | throw new Error('Unknown action: ' + action)
206 | }
207 | } catch (e) {
208 | console.log(e)
209 | return { error: e.message || e.stack.split('\n')[0] }
210 | }
211 | })
212 |
213 | function response(_x) {
214 | return _ref.apply(this, arguments)
215 | }
216 |
217 | return response
218 | })()
219 |
220 | function createBigQueryClient(credentials) {
221 | if (credentials.keyFile) {
222 | const { name, data } = credentials.keyFile
223 |
224 | const { name: keyFilename, fd } = tmp.fileSync({ postfix: name })
225 | fs.writeFileSync(fd, Buffer.from(data, 'hex'))
226 |
227 | credentials.keyFilename = keyFilename
228 | }
229 | console.log(credentials)
230 | const client = new BigQueryClient(credentials)
231 | return {
232 | query: (sql, { useLegacySql }) => client.query({ query: sql, useLegacySql }),
233 | getDatasets: () => client.getDatasets(),
234 | close() {
235 | console.log('no bigquery close method')
236 | },
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/db/sqlite/usage.txt:
--------------------------------------------------------------------------------
1 | drop table if exists _VariableName;
2 | create temporary table _VariableName as
3 | select `Row Labels`, `Count of item_no` from `Sheet1`;
4 | select * from _VariableName;
5 |
6 |
7 |
8 |
9 | WITH RECURSIVE
10 | xaxis(x) AS (VALUES(-2.3) UNION ALL SELECT x+0.05 FROM xaxis WHERE x<1.2),
11 | yaxis(y) AS (VALUES(-1.2) UNION ALL SELECT y+0.1 FROM yaxis WHERE y<1.0)
12 | select * from xaxis, yaxis where (x + y)*y*abs(x*7) > 1
13 |
14 |
15 |
16 |
17 |
18 | CREATE TABLE `geo_states` (
19 | `name` text,
20 | `abv` text,
21 | `country` text,
22 | `is_state` text,
23 | `is_lower48` text,
24 | `slug` text,
25 | `latitude` real,
26 | `longitude` real,
27 | `population` integer,
28 | `area` real,
29 | PRIMARY KEY (`abv`,`country`)
30 | );
31 |
32 | INSERT INTO `geo_states` (`name`,`abv`,`country`,`is_state`,`is_lower48`,`slug`,`latitude`,`longitude`,`population`,`area`) VALUES
33 | ('Alabama','AL','US','y','y','alabama',32.806671,-86.791130,4779736,50744.00),
34 | ('Alaska','AK','US','y','n','alaska',61.370716,-152.404419,710231,571951.25),
35 | ('Arizona','AZ','US','y','y','arizona',33.729759,-111.431221,6392017,113634.57),
36 | ('Arkansas','AR','US','y','y','arkansas',34.969704,-92.373123,2915918,52068.17),
37 | ('California','CA','US','y','y','california',36.116203,-119.681564,37253956,155939.52),
38 | ('Colorado','CO','US','y','y','colorado',39.059811,-105.311104,5029196,103717.53),
39 | ('Connecticut','CT','US','y','y','connecticut',41.597782,-72.755371,3574097,4844.80),
40 | ('Delaware','DE','US','y','y','delaware',39.318523,-75.507141,897934,1953.56),
41 | ('District of Columbia','DC','US','n','n','district-of-columbia',38.897438,-77.026817,601723,68.34),
42 | ('Florida','FL','US','y','y','florida',27.766279,-81.686783,18801310,53926.82),
43 | ('Georgia','GA','US','y','y','georgia',33.040619,-83.643074,9687653,57906.14),
44 | ('Hawaii','HI','US','y','n','hawaii',21.094318,-157.498337,1360301,6422.62),
45 | ('Idaho','ID','US','y','y','idaho',44.240459,-114.478828,1567582,82747.21),
46 | ('Illinois','IL','US','y','y','illinois',40.349457,-88.986137,12830632,55583.58),
47 | ('Indiana','IN','US','y','y','indiana',39.849426,-86.258278,6483802,35866.90),
48 | ('Iowa','IA','US','y','y','iowa',42.011539,-93.210526,3046355,55869.36),
49 | ('Kansas','KS','US','y','y','kansas',38.526600,-96.726486,2853118,81814.88),
50 | ('Kentucky','KY','US','y','y','kentucky',37.668140,-84.670067,4339367,39728.18),
51 | ('Louisiana','LA','US','y','y','louisiana',31.169546,-91.867805,4533372,43561.85),
52 | ('Maine','ME','US','y','y','maine',44.693947,-69.381927,1328361,30861.55),
53 | ('Maryland','MD','US','y','y','maryland',39.063946,-76.802101,5773552,9773.82),
54 | ('Massachusetts','MA','US','y','y','massachusetts',42.230171,-71.530106,6547629,7840.02),
55 | ('Michigan','MI','US','y','y','michigan',43.326618,-84.536095,9883640,56803.82),
56 | ('Minnesota','MN','US','y','y','minnesota',45.694454,-93.900192,5303925,79610.08),
57 | ('Mississippi','MS','US','y','y','mississippi',32.741646,-89.678696,2967297,46906.96),
58 | ('Missouri','MO','US','y','y','missouri',38.456085,-92.288368,5988927,68885.93),
59 | ('Montana','MT','US','y','y','montana',46.921925,-110.454353,989415,145552.44),
60 | ('Nebraska','NE','US','y','y','nebraska',41.125370,-98.268082,1826341,76872.41),
61 | ('Nevada','NV','US','y','y','nevada',38.313515,-117.055374,2700551,109825.99),
62 | ('New Hampshire','NH','US','y','y','new-hampshire',43.452492,-71.563896,1316470,8968.10),
63 | ('New Jersey','NJ','US','y','y','new-jersey',40.298904,-74.521011,8791894,7417.34),
64 | ('New Mexico','NM','US','y','y','new-mexico',34.840515,-106.248482,2059179,121355.53),
65 | ('New York','NY','US','y','y','new-york',42.165726,-74.948051,19378102,47213.79),
66 | ('North Carolina','NC','US','y','y','north-carolina',35.630066,-79.806419,9535483,48710.88),
67 | ('North Dakota','ND','US','y','y','north-dakota',47.528912,-99.784012,672591,68975.93),
68 | ('Ohio','OH','US','y','y','ohio',40.388783,-82.764915,11536504,40948.38),
69 | ('Oklahoma','OK','US','y','y','oklahoma',35.565342,-96.928917,3751351,68667.06),
70 | ('Oregon','OR','US','y','y','oregon',44.572021,-122.070938,3831074,95996.79),
71 | ('Pennsylvania','PA','US','y','y','pennsylvania',40.590752,-77.209755,12702379,44816.61),
72 | ('Rhode Island','RI','US','y','y','rhode-island',41.680893,-71.511780,1052567,1044.93),
73 | ('South Carolina','SC','US','y','y','south-carolina',33.856892,-80.945007,4625364,30109.47),
74 | ('South Dakota','SD','US','y','y','south-dakota',44.299782,-99.438828,814180,75884.64),
75 | ('Tennessee','TN','US','y','y','tennessee',35.747845,-86.692345,6346105,41217.12),
76 | ('Texas','TX','US','y','y','texas',31.054487,-97.563461,25145561,261797.12),
77 | ('Utah','UT','US','y','y','utah',40.150032,-111.862434,2763885,82143.65),
78 | ('Vermont','VT','US','y','y','vermont',44.045876,-72.710686,625741,9249.56),
79 | ('Virginia','VA','US','y','y','virginia',37.769337,-78.169968,8001024,39594.07),
80 | ('Washington','WA','US','y','y','washington',47.400902,-121.490494,6724540,66544.06),
81 | ('West Virginia','WV','US','y','y','west-virginia',38.491226,-80.954453,1852994,24077.73),
82 | ('Wisconsin','WI','US','y','y','wisconsin',44.268543,-89.616508,5686986,54310.10),
83 | ('Wyoming','WY','US','y','y','wyoming',42.755966,-107.302490,563626,97100.40),
84 | ('Aguascalientes','AG','MX',NULL,NULL,'aguascalientes',NULL,NULL,NULL,NULL),
85 | ('Baja California','BC','MX',NULL,NULL,'baja-california',NULL,NULL,NULL,NULL),
86 | ('Baja California Sur','BS','MX',NULL,NULL,'baja-california-sur',NULL,NULL,NULL,NULL),
87 | ('Campeche','CM','MX',NULL,NULL,'campeche',NULL,NULL,NULL,NULL),
88 | ('Chiapas','CS','MX',NULL,NULL,'chiapas',NULL,NULL,NULL,NULL),
89 | ('Chihuahua','CH','MX',NULL,NULL,'chihuahua',NULL,NULL,NULL,NULL),
90 | ('Coahuila','CO','MX',NULL,NULL,'coahuila',NULL,NULL,NULL,NULL),
91 | ('Colima','CL','MX',NULL,NULL,'colima',NULL,NULL,NULL,NULL),
92 | ('Durango','DG','MX',NULL,NULL,'durango',NULL,NULL,NULL,NULL),
93 | ('Federal District','DF','MX',NULL,NULL,'federal-district',NULL,NULL,NULL,NULL),
94 | ('Guanajuato','GT','MX',NULL,NULL,'guanajuato',NULL,NULL,NULL,NULL),
95 | ('Guerrero','GR','MX',NULL,NULL,'guerrero',NULL,NULL,NULL,NULL),
96 | ('Hidalgo','HG','MX',NULL,NULL,'hidalgo',NULL,NULL,NULL,NULL),
97 | ('Jalisco','JA','MX',NULL,NULL,'jalisco',NULL,NULL,NULL,NULL),
98 | ('Mexico State','ME','MX',NULL,NULL,'mexico-state',NULL,NULL,NULL,NULL),
99 | ('Michoacán','MI','MX',NULL,NULL,'michoacan',NULL,NULL,NULL,NULL),
100 | ('Morelos','MO','MX',NULL,NULL,'morelos',NULL,NULL,NULL,NULL),
101 | ('Nayarit','NA','MX',NULL,NULL,'nayarit',NULL,NULL,NULL,NULL),
102 | ('Nuevo León','NL','MX',NULL,NULL,'nuevo-leon',NULL,NULL,NULL,NULL),
103 | ('Oaxaca','OA','MX',NULL,NULL,'oaxaca',NULL,NULL,NULL,NULL),
104 | ('Puebla','PB','MX',NULL,NULL,'puebla',NULL,NULL,NULL,NULL),
105 | ('Querétaro','QE','MX',NULL,NULL,'queretaro',NULL,NULL,NULL,NULL),
106 | ('Quintana Roo','QR','MX',NULL,NULL,'quintana-roo',NULL,NULL,NULL,NULL),
107 | ('San Luis Potosí','SL','MX',NULL,NULL,'san-luis-potosi',NULL,NULL,NULL,NULL),
108 | ('Sinaloa','SI','MX',NULL,NULL,'sinaloa',NULL,NULL,NULL,NULL),
109 | ('Sonora','SO','MX',NULL,NULL,'sonora',NULL,NULL,NULL,NULL),
110 | ('Tabasco','TB','MX',NULL,NULL,'tabasco',NULL,NULL,NULL,NULL),
111 | ('Tamaulipas','TM','MX',NULL,NULL,'tamaulipas',NULL,NULL,NULL,NULL),
112 | ('Tlaxcala','TL','MX',NULL,NULL,'tlaxcala',NULL,NULL,NULL,NULL),
113 | ('Veracruz','VE','MX',NULL,NULL,'veracruz',NULL,NULL,NULL,NULL),
114 | ('Yucatán','YU','MX',NULL,NULL,'yucatan',NULL,NULL,NULL,NULL),
115 | ('Zacatecas','ZA','MX',NULL,NULL,'zacatecas',NULL,NULL,NULL,NULL),
116 | ('Alberta','AB','CA',NULL,NULL,'alberta',NULL,NULL,NULL,NULL),
117 | ('British Columbia','BC','CA',NULL,NULL,'british-columbia',NULL,NULL,NULL,NULL),
118 | ('Manitoba','MB','CA',NULL,NULL,'manitoba',NULL,NULL,NULL,NULL),
119 | ('New Brunswick','NB','CA',NULL,NULL,'new-brunswick',NULL,NULL,NULL,NULL),
120 | ('Newfoundland and Labrador','NL','CA',NULL,NULL,'newfoundland-and-labrador',NULL,NULL,NULL,NULL),
121 | ('Northwest Territories','NT','CA',NULL,NULL,'northwest-territories',NULL,NULL,NULL,NULL),
122 | ('Nova Scotia','NS','CA',NULL,NULL,'nova-scotia',NULL,NULL,NULL,NULL),
123 | ('Nunavut','NU','CA',NULL,NULL,'nunavut',NULL,NULL,NULL,NULL),
124 | ('Ontario','ON','CA',NULL,NULL,'ontario',NULL,NULL,NULL,NULL),
125 | ('Prince Edward Island','PE','CA',NULL,NULL,'prince-edward-island',NULL,NULL,NULL,NULL),
126 | ('Quebec','QC','CA',NULL,NULL,'quebec',NULL,NULL,NULL,NULL),
127 | ('Saskatchewan','SK','CA',NULL,NULL,'saskatchewan',NULL,NULL,NULL,NULL),
128 | ('Yukon','YT','CA',NULL,NULL,'yukon',NULL,NULL,NULL,NULL);
129 |
130 |
131 |
132 |
133 |
134 | DROP TABLE IF EXISTS employees;
135 | CREATE TABLE employees( id integer, name text,
136 | designation text, manager integer,
137 | hired_on date, salary integer,
138 | commission float, dept integer);
139 |
140 | INSERT INTO employees VALUES (1,'JOHNSON','ADMIN',6,'1990-12-17',18000,NULL,4);
141 | INSERT INTO employees VALUES (2,'HARDING','MANAGER',9,'1998-02-02',52000,300,3);
142 | INSERT INTO employees VALUES (3,'TAFT','SALES I',2,'1996-01-02',25000,500,3);
143 | INSERT INTO employees VALUES (4,'HOOVER','SALES I',2,'1990-04-02',27000,NULL,3);
144 | INSERT INTO employees VALUES (5,'LINCOLN','TECH',6,'1994-06-23',22500,1400,4);
145 | INSERT INTO employees VALUES (6,'GARFIELD','MANAGER',9,'1993-05-01',54000,NULL,4);
146 | INSERT INTO employees VALUES (7,'POLK','TECH',6,'1997-09-22',25000,NULL,4);
147 | INSERT INTO employees VALUES (8,'GRANT','ENGINEER',10,'1997-03-30',32000,NULL,2);
148 | INSERT INTO employees VALUES (9,'JACKSON','CEO',NULL,'1990-01-01',75000,NULL,4);
149 | INSERT INTO employees VALUES (10,'FILLMORE','MANAGER',9,'1994-08-09',56000,NULL,2);
150 | INSERT INTO employees VALUES (11,'ADAMS','ENGINEER',10,'1996-03-15',34000,NULL,2);
151 | INSERT INTO employees VALUES (12,'WASHINGTON','ADMIN',6,'1998-04-16',18000,NULL,4);
152 | INSERT INTO employees VALUES (13,'MONROE','ENGINEER',10,'2000-12-03',30000,NULL,2);
153 | INSERT INTO employees VALUES (14,'ROOSEVELT','CPA',9,'1995-10-12',35000,NULL,1);
154 |
--------------------------------------------------------------------------------
/src/db/mongo.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import * as State from '../state'
4 | import * as U from '../state/update'
5 |
6 | import _ from 'lodash'
7 |
8 | export const key = 'mongo'
9 | export const name = 'MongoDB'
10 | export const syntax = 'javascript'
11 | import 'codemirror/mode/javascript/javascript'
12 |
13 | import { UnmountClosed } from 'react-collapse'
14 |
15 | import { connectHelper, disconnectHelper } from './generic'
16 | import { FranchiseClientConnector, sendRequest, disconnectBridge } from './bridge'
17 | import CV from '../util/codeviewer'
18 |
19 | export const requires_bridge = true
20 |
21 | export async function bridgeConnected() {
22 | console.log('bridge connect')
23 | // const credentials = await sendRequest({ action: 'get_credentials' })
24 | // const credentials = {
25 | // host: 'localhost',
26 | // port: '3306',
27 | // database: '',
28 | // user: 'root',
29 | // password: '',
30 | // }
31 | // State.apply('config', 'mysql', 'credentials', U.def({}), (old_credentials) => ({
32 | // ...credentials,
33 | // ...old_credentials,
34 | // autofilled: Object.keys(credentials)
35 | // .filter((k) => k != 'id') // don't include the message id
36 | // .some((k) => !(k in old_credentials)),
37 | // }))
38 | }
39 |
40 | export class Configure extends React.Component {
41 | // softUpdateCredentials(o){
42 | // const {credentials} = this.state
43 | // Object.keys(credentials).forEach(k => {
44 | // const credential = credentials[k]
45 | // if(credential.length > 0) o[k] = credential
46 | // })
47 | // console.log(o)
48 | // this.setState({credentials: o})
49 | // // this.setState({fields: {...this.state.fields, ...o}})
50 | // }
51 |
52 | render() {
53 | // Tried to connect {this.state.tries} times.
54 | const { connect, config } = this.props
55 |
56 | const credentialHints = {
57 | host: 'localhost',
58 | port: '27017',
59 | database: 'mydb',
60 | user: 'dbuser',
61 | password: 'password (optional)',
62 | }
63 | let credentials = (config.credentials && config.credentials.mongo) || {}
64 |
65 | const Field = (type, icon, className = '') => (
66 |
67 | {icon ? : null}
68 |
74 | State.apply(
75 | 'config',
76 | 'credentials',
77 | 'mongo',
78 | type,
79 | U.replace(e.target.value)
80 | )
81 | }
82 | placeholder={credentialHints[type]}
83 | />
84 |
85 | )
86 |
87 | return (
88 |
89 |
90 |
91 |
92 |
93 |
94 |
100 |
101 |
102 | Franchise auto-filled some of your credentials using your
103 | system's defaults.
104 |
105 |
106 |
107 |
108 | {Field('host', 'cloud')}
109 | {Field('port')}
110 |
111 |
112 |
113 | {Field('user', 'user')}
114 | {Field('password', 'lock')}
115 |
116 |
117 | {Field('database', 'database')}
118 |
119 | {/*
derp
*/}
120 |
121 |
122 |
123 |
124 | {connect.status != 'connected' ? (
125 |
connectDB()}
133 | >
134 | Connect
135 |
136 |
137 | ) : (
138 |
disconnectDB()}
142 | >
143 | Disconnect
144 |
145 |
146 | )}
147 |
148 |
149 | )
150 | }
151 | }
152 |
153 | export function create_table_snippet() {
154 | return `db.createCollection("new_table");`
155 | }
156 |
157 | export function select_table_snippet(table) {
158 | // return 'select ' + table.columns.map(e => '"' + e + '"').join(', ') + ' from "' + table.name + '"'
159 | }
160 |
161 | export async function connectDB() {
162 | await connectHelper(async function() {
163 | let result = await sendRequest({
164 | action: 'open',
165 | db: 'mongo',
166 | credentials: State.get('config', 'credentials', 'mongo'),
167 | })
168 | if (!result.ready) throw new Error(result.error)
169 |
170 | // State.apply('connect', 'schema', U.replace(await getSchema()))
171 | })
172 | }
173 |
174 | // export function reference(name) {
175 | // return '#' + name
176 | // }
177 |
178 | export function bridgeDisconnected() {
179 | if (State.get('connect', 'status') === 'connected') {
180 | State.apply('connect', 'status', U.replace('disconnected'))
181 | console.log('bridge disconnected')
182 | }
183 | }
184 |
185 | async function disconnectDB() {
186 | await disconnectHelper((e) => sendRequest({ action: 'close' }))
187 | }
188 |
189 | // export async function run(query) {
190 | // var db = State.get('connect', '_db')
191 | // let result = formatResults(eval(query))
192 | // result.query = query
193 | // // State.apply('connect', 'schema', U.replace(await getSchema()))
194 | // return result
195 | // }
196 |
197 | export async function run(query, cellId) {
198 | var response = await sendRequest({ action: 'exec', sql: query })
199 |
200 | // let expandedQuery = expandQueryRefs(query, cellId)
201 | // console.log(expandedQuery)
202 | // let result = await _runQuery(expandedQuery)
203 | // result.query = query
204 | // State.apply('connect', 'schema', U.replace(await getSchema()))
205 | // await extractEditableColumns(result)
206 | // await assignSuggestedName(result)
207 | // await new Promise(k => setTimeout(k, 3000));
208 | // console.log(response)
209 |
210 | let cols = _.uniq(_.flatten(response.results.map((k) => Object.keys(k))))
211 |
212 | return {
213 | id: response.id,
214 | query: query,
215 | object: response.results,
216 | columns: cols,
217 | values: response.results.map((k) => cols.map((j) => k[j])),
218 | }
219 | }
220 |
221 | function formatResults(data) {
222 | if (data === undefined) {
223 | return {}
224 | }
225 | return {
226 | columns: ['one', 'two', 'three'],
227 | values: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
228 | }
229 | }
230 |
231 | export function CodeMirrorOptions(connect, virtualSchema) {
232 | return {
233 | mode: 'javascript',
234 |
235 | // hintOptions: {
236 | // hint: CodeMirror.hint.javascript,
237 | // },
238 | }
239 | }
240 |
241 | export function Clippy(props) {
242 | return (
243 |
244 |
245 |
246 | db.collection.find(query, projection)
247 |
248 |
253 |
254 |
255 |
268 |
269 |
270 | )
271 | }
272 |
--------------------------------------------------------------------------------
/src/db/img/bigquery.svg:
--------------------------------------------------------------------------------
1 |
2 | bigquery
3 | Created using Figma
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------