1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

29 Commits

Author SHA1 Message Date
lana-k
c713c713b7 fix paths #97 2022-07-20 23:15:14 +02:00
lana-k
babf0074c0 add artifact with source map #97 2022-07-20 22:49:26 +02:00
lana-k
e71e6700c1 improve events 2022-07-20 22:47:40 +02:00
lana-k
84e66b8167 Update README.md 2022-07-10 23:18:52 +02:00
lana-k
9e84cf269e Merge branch 'master' of github.com:lana-k/sqliteviz 2022-07-10 22:54:09 +02:00
lana-k
e897b4913b cancel deploying to github pages; remove banner 2022-07-10 22:53:23 +02:00
saaj
0646f58ca0 Build SQLite 3.39.0 (#95) 2022-07-10 22:46:39 +02:00
lana-k
c674bf11e3 update version number 2022-07-10 22:29:20 +02:00
lana-k
2d8a91675e Remove console.log 2022-07-10 22:25:11 +02:00
lana-k
45b1021559 links to website 2022-07-10 21:26:26 +02:00
lana-k
7216e996d1 add banner 2022-07-10 21:25:51 +02:00
lana-k
6eae9a0f2d remove empty lines 2022-07-10 16:02:04 +02:00
lana-k
7486b32bd1 add head extention slot 2022-07-03 15:38:41 +02:00
lana-k
2c564767f8 Merge branch 'master' of github.com:lana-k/sqliteviz 2022-07-01 21:01:07 +02:00
lana-k
289a727cbe fix icon width 2022-07-01 21:01:02 +02:00
saaj
5f2b8ba5a9 Upgrade to SQLite 3.38.5 (#91)
* Update to SQLite 3.37 and latest generate_series extension

* Expect column types in upper case

* Rebuild newer SQLite, 3.38.5
2022-06-27 17:35:40 +02:00
lana-k
f0a4212e2b fix tests 2022-06-26 21:06:24 +02:00
lana-k
c8deff32c1 fix lint 2022-06-25 22:38:22 +02:00
lana-k
d56604a7d6 events refactor 2022-06-25 22:37:09 +02:00
lana-k
48e311bff8 minor change 2022-06-24 22:58:43 +02:00
lana-k
518b22b489 events 2022-06-24 21:29:40 +02:00
lana-k
a20dd7f849 forbid index minifying 2022-06-11 19:29:42 +02:00
lana-k
310a939109 #89 add tests 2021-12-24 16:13:42 +01:00
lana-k
bb9ba08902 #89 remove head and body 2021-12-22 20:42:53 +01:00
lana-k
c7c727ff78 fix lint 2021-12-21 22:15:21 +01:00
lana-k
8669a6a9e5 #89 export to html plolty charts and pivots 2021-12-21 22:13:02 +01:00
lana-k
c1cc5bb95e getHtml for chart #89 2021-12-20 22:31:08 +01:00
lana-k
9c55e76a41 update version 2021-12-19 16:00:11 +01:00
lana-k
70a9edf57e fix lint 2021-12-19 15:57:30 +01:00
39 changed files with 15387 additions and 12049 deletions

View File

@@ -1,4 +1,4 @@
name: Deploy to GitHub Pages and create release
name: Create release
on:
workflow_dispatch:
push:
@@ -7,7 +7,7 @@ on:
jobs:
deploy:
name: Deploy to GitHub Pages and create release
name: Create release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
@@ -24,10 +24,11 @@ jobs:
npm install
npm run build
- name: Create archive
- name: Create archives
run: |
cd dist
zip -9 -r dist.zip . -x "js/*.map" -x "/*.map"
zip -9 -r ../dist.zip . -x "js/*.map" -x "/*.map"
zip -9 -r ../dist_map.zip .
- name: Create Release Notes
run: |
@@ -39,16 +40,6 @@ jobs:
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/dist.zip"
artifacts: "dist.zip,dist_map.zip"
token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md"
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: build # The branch the action should deploy to.
folder: dist/ # The folder the action should deploy.
clean: true # Automatically remove deleted files from the deploy branch
clean-exclude: .nojekyll

View File

@@ -18,10 +18,10 @@ With sqliteviz you can:
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
## Quickstart
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
## Wiki
For user documentation, check out sqliteviz [Wiki][7].
For user documentation, check out sqliteviz [documentation][7].
## Motivation
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
@@ -34,8 +34,8 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[3]: https://github.com/plotly/react-chart-editor
[4]: https://github.com/sql-js/sql.js
[5]: https://github.com/vuejs/vue
[6]: https://lana-k.github.io/sqliteviz/
[7]: https://github.com/lana-k/sqliteviz/wiki
[6]: https://sqliteviz.com/app/
[7]: https://sqliteviz.com/docs
[8]: https://github.com/surmon-china/vue-codemirror#readme
[9]: https://www.papaparse.com/
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries

View File

@@ -1,4 +1,4 @@
FROM node:12-buster
FROM node:14-bullseye
RUN set -ex; \
echo 'deb http://deb.debian.org/debian unstable main' \

View File

@@ -1,21 +1,44 @@
#!/bin/bash -e
cleanup () {
rm -rf lib/dist $flag_file
}
trap cleanup EXIT
if [ ! -f sample.csv ]; then
wget --header="accept-encoding: gzip" -q -O- \
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
| gunzip -c > sample.csv
fi
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
test_ni=$(nice -n -1 nice)
if [ $test_ni == -1 ]; then
flag_file=$(mktemp)
fi
(
while [ -f $flag_file ]; do
root_pid=$(
docker ps -f status=running -f name='^sqljs-benchmark-' -q \
| xargs -r -I{} -- docker inspect -f '{{.State.Pid}}' {}
)
if [ ! -z $root_pid ]; then
procpath query -d $'\n' "$..children[?(@.stat.pid == $root_pid)]..pid" \
| xargs -I{} -- renice -n -1 -p {} > /dev/null
fi
sleep 1
done &
)
shopt -s nullglob
for d in lib/build-* ; do
rm -r lib/dist || true
rm -rf lib/dist
cp -r $d lib/dist
name=$(basename $d)
docker build -t sqliteviz/sqljs-benchmark:$name .
docker rm sqljs-benchmark-$name 2> /dev/null || true
docker run -it --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
docker run -it --cpus 2 --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
docker rm sqljs-benchmark-$name
done
rm -r lib/dist

View File

@@ -21,7 +21,7 @@
"metadata": {},
"outputs": [],
"source": [
"benchmark_path = Path('/path/to/sqliteviz/project/sqliteviz/lib/sql-js/benchmark')"
"benchmark_path = Path()"
]
},
{

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from urllib import request
amalgamation_url = 'https://sqlite.org/2021/sqlite-amalgamation-3360000.zip'
amalgamation_url = 'https://sqlite.org/2022/sqlite-amalgamation-3390000.zip'
# Extension-functions
# ===================
@@ -20,7 +20,7 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
extension_urls = (
# Miscellaneous extensions
# ========================
('https://sqlite.org/src/raw/c6bd5d24?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),

File diff suppressed because one or more lines are too long

Binary file not shown.

26814
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "0.17.0",
"version": "0.21.0",
"license": "Apache-2.0",
"private": true,
"scripts": {

View File

@@ -69,6 +69,8 @@
}
}
</style>
<!-- head extention slot start -->
<!-- head extention slot end -->
</head>
<body>
<noscript>
@@ -93,6 +95,8 @@
</svg>
</div>
</div>
<!-- extention slot start -->
<!-- extention slot end -->
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.8311 34.6554C25.4675 33.8178 24.177 32.8655 22.9735 31.8086V14.3616H30.5728V36.753C29.3146 36.0982 28.0673 35.399 26.8311 34.6554ZM41.4669 25.8486H33.8675V38.1514C36.3477 39.3055 38.884 40.3334 41.4669 41.2313V25.8486ZM22.9735 35.3046L22.4768 34.9051C21.7152 34.2725 21.0033 33.6232 20.3245 32.9739L2.2947 30.8763L5.60596 37.3024L28.7848 39.2002C26.7511 38.0537 24.8082 36.7513 22.9735 35.3046ZM41.0695 44.6441C38.4829 43.7946 35.9458 42.7997 33.4702 41.6641L32.543 41.198L17.2616 40.1825L19.8444 45.593L46.5 46.209C44.6788 45.7761 42.8411 45.2434 41.0695 44.6441ZM9.34768 14.3616C12.2649 19.4905 15.735 24.2807 19.6954 28.6455V11.2651L2.99007 2.99115L1.5 22.3859L18.702 31.2592C14.1919 26.5283 10.9703 20.7087 9.34768 14.3616Z" fill="#119DFF"/>
</svg>

After

Width:  |  Height:  |  Size: 862 B

View File

@@ -112,6 +112,7 @@ import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default {
name: 'CsvImport',
@@ -335,6 +336,7 @@ export default {
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
}
}
}

View File

@@ -58,6 +58,7 @@ import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database'
import CsvImport from '@/components/CsvImport'
import events from '@/lib/utils/events'
export default {
name: 'DbUploader',
@@ -127,6 +128,11 @@ export default {
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
events.send('database.import', file.size, {
from: 'csv',
new_db: true
})
this.file = file
await this.$nextTick()
const csvImport = this.$refs.addCsv

View File

@@ -0,0 +1,49 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.1626 10.0745L7.56641 10.8831V12.2322L3.68164 10.6501V9.4812L7.56641
7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023
13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192
12.2366V10.8918L14.1099 10.0613Z"
fill="#A2B1C6"
/>
<path
d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872
1.34541 0.0637207 2.17041 0.0637207Z"
fill="#A2B1C6"
/>
<path
d="M17.1704 0.0637207H15.3052V1.56372H17.1704V9.84163H18.6704V1.56372C18.6704 0.73872
17.9954 0.0637207 17.1704 0.0637207Z"
fill="#A2B1C6"
/>
<path
d="M2.17041 17.1098H15.8754V15.6098H2.17041V8.78486H0.67041V15.6098C0.67041 16.4348
1.34541 17.1098 2.17041 17.1098Z"
fill="#A2B1C6"
/>
<path
d="M17.1704 17.1098H15.3052V15.6098H17.1704V8.55939H18.6704V15.6098C18.6704 16.4348
17.9954 17.1098 17.1704 17.1098Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.1197 4.13787H1.76172V3.03787H18.1197V4.13787Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'HtmlIcon'
}
</script>

View File

@@ -1,5 +1,6 @@
import dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js'
import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) {
if (!dataSources) {
@@ -33,8 +34,43 @@ export async function getImageDataUrl (element, type) {
})
}
export function getChartData (element) {
const chartElement = element.querySelector('.js-plotly-plot')
return {
data: chartElement.data,
layout: chartElement.layout
}
}
export function getHtml (options) {
const chartId = nanoid()
return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
<div id="${chartId}"></div>
<script>
const el = document.getElementById("${chartId}")
let timeout
function debounceResize() {
clearTimeout(timeout)
timeout = setTimeout(() => {
var r = el.getBoundingClientRect()
Plotly.relayout(el, {width: r.width, height: r.height})
}, 200)
}
const resizeObserver = new ResizeObserver(debounceResize)
resizeObserver.observe(el)
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
</script>
`
}
export default {
getOptionsFromDataSources,
getOptionsForSave,
getImageDataUrl
getImageDataUrl,
getHtml,
getChartData
}

View File

@@ -7,6 +7,8 @@ import Worker from './_worker.js'
// https://github.com/nolanlawson/promise-worker
import PromiseWorker from 'promise-worker'
import events from '@/lib/utils/events'
function getNewDatabase () {
const worker = new Worker()
return new Database(worker)
@@ -76,6 +78,11 @@ class Database {
this.dbName = file ? fu.getFileName(file) : 'database'
this.refreshSchema()
events.send('database.import', file ? file.size : 0, {
from: file ? 'sqlite' : 'none',
new_db: true
})
}
async refreshSchema () {
@@ -114,6 +121,7 @@ class Database {
throw new Error(data.error)
}
fu.exportToFile(data, fileName)
events.send('database.export', data.byteLength, { to: 'sqlite' })
}
async validateTableName (name) {

View File

@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid'
import fu from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import migration from './_migrations'
const migrate = migration._migrate
@@ -103,9 +104,19 @@ export default {
importInquiries () {
return fu.importFile()
.then(str => {
return this.deserialiseInquiries(str)
const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
return inquires
})
},
export (inquiryList, fileName) {
const jsonStr = this.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName)
events.send('inquiry.export', inquiryList.length)
},
async readPredefinedInquiries () {
const res = await fu.readFile('./inquiries.json')

12
src/lib/utils/events.js Normal file
View File

@@ -0,0 +1,12 @@
export default {
send (name, value, labels) {
const event = new CustomEvent('sqliteviz-app-event', {
detail: {
name,
value,
labels: labels || {}
}
})
window.dispatchEvent(event)
}
}

View File

@@ -1,3 +1,4 @@
import events from '@/lib/utils/events'
let refresh = false
function invokeServiceWorkerUpdateFlow (registration) {
@@ -41,4 +42,8 @@ if ('serviceWorker' in navigator) {
}
})
})
window.addEventListener('appinstalled', () => {
events.send('pwa.install')
})
}

View File

@@ -62,6 +62,7 @@ export default {
<style scoped>
#app-info-icon {
cursor: pointer;
width: 24px;
}
#app-info-container {
display: flex;

View File

@@ -159,7 +159,6 @@ import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox'
import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries'
import fu from '@/lib/utils/fileIo'
export default {
name: 'Inquiries',
@@ -385,8 +384,7 @@ export default {
return this.$store.state.tabs.findIndex(tab => tab.id === id)
},
exportToFile (inquiryList, fileName) {
const jsonStr = storedInquiries.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName)
storedInquiries.export(inquiryList, fileName)
},
exportSelectedInquiries () {
const inquiryList = this.allInquiries.filter(

View File

@@ -1,9 +1,12 @@
<template>
<nav>
<div>
<div id="nav-links">
<a href="https://sqliteviz.com">
<img :src="require('@/assets/images/logo_simple.svg')">
</a>
<router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link>
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
<a href="https://sqliteviz.com/docs" target="_blank">Help</a>
</div>
<div id="nav-buttons">
<button
@@ -57,6 +60,7 @@ import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo'
import events from '@/lib/utils/events'
export default {
name: 'MainMenu',
@@ -110,6 +114,8 @@ export default {
this.$router.push('/workspace')
}
})
events.send('inquiry.create', null, { auto: false })
},
cancelSave () {
this.$modal.hide('save')
@@ -163,6 +169,7 @@ export default {
// Signal about saving
this.$root.$emit('inquirySaved')
events.send('inquiry.save')
},
_keyListener (e) {
if (this.$route.path === '/workspace') {
@@ -237,4 +244,12 @@ button {
#nav-buttons {
display: flex;
}
#nav-links {
display: flex;
align-items: center;
}
#nav-links img {
width: 32px;
}
</style>

View File

@@ -33,6 +33,7 @@
<script>
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import TableDescription from './TableDescription'
import TextField from '@/components/TextField'
import TreeChevron from '@/components/svg/treeChevron'
@@ -86,6 +87,11 @@ export default {
csvImport.reset()
await csvImport.previewCsv()
csvImport.open()
events.send('database.import', this.file.size, {
from: 'csv',
new_db: false
})
}
}
}

View File

@@ -31,10 +31,15 @@ import PlotlyEditor from 'react-chart-editor'
import chartHelper from '@/lib/chartHelper'
import dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default {
name: 'Chart',
props: ['dataSources', 'initOptions', 'importToPngEnabled', 'importToSvgEnabled'],
props: [
'dataSources', 'initOptions',
'importToPngEnabled', 'importToSvgEnabled',
'forPivot'
],
components: {
PlotlyEditor
},
@@ -57,9 +62,20 @@ export default {
},
created () {
// https://github.com/plotly/plotly.js/issues/4555
plotly.setPlotConfig ({
plotly.setPlotConfig({
notifyOnLogging: 1
})
this.$watch(
() => this.state.data.map(trace => `${trace.type}-${trace.mode}`)
.join(','),
(value) => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
})
},
{ deep: true }
)
},
mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize)
@@ -105,6 +121,13 @@ export default {
fIo.downloadFromUrl(url, 'chart')
},
saveAsHtml () {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy (type = 'png') {
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
}

View File

@@ -19,10 +19,11 @@ import $ from 'jquery'
import 'pivottable'
import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi'
import { getPivotCanvas } from './pivotHelper'
import pivotHelper from './pivotHelper'
import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart'
import chartHelper from '@/lib/chartHelper'
import Vue from 'vue'
import events from '@/lib/utils/events'
const ChartClass = Vue.extend(Chart)
export default {
@@ -87,8 +88,17 @@ export default {
'pivotOptions.rendererName': {
immediate: true,
handler () {
this.$emit('update:importToPngEnabled', this.pivotOptions.rendererName !== 'TSV Export')
this.$emit('update:importToSvgEnabled', this.viewStandartChart || this.viewCustomChart)
this.$emit(
'update:importToPngEnabled',
this.pivotOptions.rendererName !== 'TSV Export'
)
this.$emit(
'update:importToSvgEnabled',
this.viewStandartChart || this.viewCustomChart
)
events.send('viz_pivot.render', null, {
type: this.pivotOptions.rendererName
})
}
},
pivotOptions () {
@@ -169,7 +179,7 @@ export default {
} else {
const source = this.viewStandartChart
? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
: (await getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png')
: (await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png')
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(source, 'pivot')
@@ -179,11 +189,11 @@ export default {
async prepareCopy () {
if (this.viewCustomChart) {
return await this.pivotOptions.rendererOptions.customChartComponent.prepareCopy()
} else if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
} else {
return await getPivotCanvas(this.$refs.pivotOutput)
}
if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg () {
@@ -193,6 +203,28 @@ export default {
const url = await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'svg')
fIo.downloadFromUrl(url, 'pivot')
}
},
saveAsHtml () {
if (this.viewCustomChart) {
this.pivotOptions.rendererOptions.customChartComponent.saveAsHtml()
return
}
if (this.viewStandartChart) {
const chartState = chartHelper.getChartData(this.$refs.pivotOutput)
fIo.exportToFile(
chartHelper.getHtml(chartState),
'chart.html',
'text/html'
)
return
}
fIo.exportToFile(
pivotHelper.getPivotHtml(this.$refs.pivotOutput),
'pivot.html',
'text/html'
)
}
}
}

View File

@@ -51,6 +51,8 @@ export function _getDataSources (pivotData) {
function customChartRenderer (data, options) {
options.customChartComponent.dataSources = _getDataSources(data)
options.customChartComponent.forPivot = true
options.customChartComponent.$mount()
return $(options.customChartComponent.$el)
@@ -81,3 +83,40 @@ export async function getPivotCanvas (pivotOutput) {
const tableElement = pivotOutput.querySelector('.pvtTable')
return await html2canvas(tableElement, { logging: false })
}
export function getPivotHtml (pivotOutput) {
return `
<style>
table.pvtTable {
font-family: Arial, sans-serif;
font-size: 12px;
text-align: left;
border-collapse: collapse;
min-width: 100%;
}
table.pvtTable .pvtColLabel {
text-align: center;
}
table.pvtTable .pvtTotalLabel {
text-align: right;
}
table.pvtTable tbody tr td {
color: #506784;
border: 1px solid #DFE8F3;
text-align: right;
}
table.pvtTable thead tr th,
table.pvtTable tbody tr th {
background-color: #506784;
color: #fff;
border: 1px solid #DFE8F3;
}
</style>
${pivotOutput.outerHTML}
`
}
export default {
getPivotCanvas,
getPivotHtml
}

View File

@@ -51,6 +51,13 @@
<export-to-svg-icon />
</icon-button>
<icon-button
tooltip="Save as HTML"
tooltip-position="top-left"
@click="saveAsHtml"
>
<HtmlIcon />
</icon-button>
<icon-button
:loading="copyingImage"
tooltip="Copy visualisation to clipboard"
@@ -81,12 +88,14 @@ import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton'
import ChartIcon from '@/components/svg/chart'
import PivotIcon from '@/components/svg/pivot'
import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard'
import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/LoadingDialog'
import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
export default {
name: 'DataView',
@@ -100,6 +109,7 @@ export default {
PivotIcon,
ExportToSvgIcon,
PngIcon,
HtmlIcon,
ClipboardIcon,
loadingDialog
},
@@ -114,6 +124,11 @@ export default {
dataToCopy: null
}
},
computed: {
plotlyInPivot () {
return this.mode === 'pivot' && this.$refs.viewComponent.viewCustomChart
}
},
watch: {
mode () {
this.$emit('update')
@@ -139,6 +154,7 @@ export default {
*/
await time.sleep(0)
this.$refs.viewComponent.saveAsPng()
this.exportSignal('png')
},
getOptionsForSave () {
return this.$refs.viewComponent.getOptionsForSave()
@@ -169,6 +185,7 @@ export default {
async copyToClipboard () {
cIo.copyImage(this.dataToCopy)
this.$modal.hide('prepareCopy')
this.exportSignal('clipboard')
},
cancelCopy () {
this.dataToCopy = null
@@ -177,6 +194,26 @@ export default {
saveAsSvg () {
this.$refs.viewComponent.saveAsSvg()
this.exportSignal('svg')
},
saveAsHtml () {
this.$refs.viewComponent.saveAsHtml()
this.exportSignal('html')
},
exportSignal (to) {
const eventLabels = { type: to }
if (this.mode === 'chart' || this.plotlyInPivot) {
eventLabels.pivot = this.plotlyInPivot
}
events.send(
this.mode === 'chart' || this.plotlyInPivot
? 'viz_plotly.export'
: 'viz_pivot.export',
null,
eventLabels
)
}
}
}

View File

@@ -72,6 +72,7 @@ import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog'
import events from '@/lib/utils/events'
export default {
name: 'RunResult',
@@ -117,10 +118,24 @@ export default {
},
exportToCsv () {
if (this.result && this.result.values) {
events.send('resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'csv' }
)
}
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
},
async prepareCopy () {
if (this.result && this.result.values) {
events.send('resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'clipboard' }
)
}
if ('ClipboardItem' in window) {
this.preparingCopy = true
this.$modal.show('prepareCSVCopy')

View File

@@ -56,6 +56,7 @@ import DataView from './DataView'
import RunResult from './RunResult'
import time from '@/lib/utils/time'
import Teleport from 'vue2-teleport'
import events from '@/lib/utils/events'
export default {
name: 'Tab',
@@ -108,6 +109,8 @@ export default {
const fromPosition = this.layout[from]
this.layout[from] = this.layout[to]
this.layout[to] = fromPosition
events.send('inquiry.panel', null, { panel: to })
},
onDataViewUpdate () {
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
@@ -121,11 +124,21 @@ export default {
const start = new Date()
this.result = await state.db.execute(this.query + ';')
this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) {
events.send('resultset.create',
this.result.values[this.result.columns[0]].length
)
}
events.send('query.run', parseFloat(this.time), { status: 'success' })
} catch (err) {
this.error = {
type: 'error',
message: err
}
events.send('query.run', 0, { status: 'error' })
}
state.db.refreshSchema()
this.isGettingResults = false

View File

@@ -19,6 +19,7 @@
import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema'
import Tabs from './Tabs'
import events from '@/lib/utils/events'
export default {
name: 'Workspace',
@@ -49,6 +50,8 @@ export default {
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
events.send('inquiry.create', null, { auto: true })
}
}
}

View File

@@ -55,6 +55,7 @@ describe('Logs.vue', () => {
await wrapper.vm.$nextTick()
messages.push({ type: 'error', message: 'msg 5' })
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
const height = wrapper.find('.logs-container').element.scrollHeight
expect(wrapper.find('.logs-container').element.scrollTop)

View File

@@ -64,7 +64,40 @@ describe('chartHelper.js', () => {
expect(/^data:image\/png/.test(url)).to.equal(true)
url = await chartHelper.getImageDataUrl(element, 'svg')
console.log()
expect(/^data:image\/svg\+xml/.test(url)).to.equal(true)
})
it('getChartData returns plotly data and layout from element', async () => {
const element = document.createElement('div')
const child = document.createElement('div')
element.append(child)
child.classList.add('js-plotly-plot')
child.data = 'plotly data'
child.layout = 'plotly layout'
const chartData = chartHelper.getChartData(element)
expect(chartData).to.eql({
data: 'plotly data',
layout: 'plotly layout'
})
})
it('getHtml returns valid html', async () => {
const options = {
data: 'plotly data',
layout: 'plotly layout'
}
const html = chartHelper.getHtml(options)
const doc = document.createElement('div')
doc.innerHTML = html
expect(doc.innerHTML).to.equal(html)
expect(doc.children).to.have.lengthOf(3)
expect(doc.children[0].src).to.includes('plotly-latest.js')
expect(doc.children[1].id).to.have.lengthOf(21)
expect(doc.children[2].innerHTML).to.includes(doc.children[1].id)
expect(doc.children[2].innerHTML)
.to.includes('Plotly.newPlot(el, "plotly data", "plotly layout"')
})
})

View File

@@ -44,7 +44,7 @@ describe('database.js', () => {
expect(schema[0].columns[0].type).to.equal('N/A')
expect(schema[0].columns[1].name).to.equal('col2')
expect(schema[0].columns[1].type).to.equal('integer')
expect(schema[0].columns[1].type).to.equal('INTEGER')
})
it('creates schema with view', async () => {
@@ -68,12 +68,12 @@ describe('database.js', () => {
expect(schema[0].columns[1]).to.eql({
name: 'col2',
type: 'integer'
type: 'INTEGER'
})
expect(schema[1].columns).to.eql([{
name: 'amount',
type: 'integer'
type: 'INTEGER'
}])
})

View File

@@ -29,12 +29,12 @@ describe('time.js', () => {
})
it('sleep resolves after n ms', async () => {
let before = performance.now()
let before = Date.now()
await time.sleep(10)
expect(performance.now() - before).to.be.least(10)
expect(Date.now() - before).to.be.least(10)
before = performance.now()
before = Date.now()
await time.sleep(30)
expect(performance.now() - before).to.be.least(30)
expect(Date.now() - before).to.be.least(30)
})
})

View File

@@ -59,6 +59,31 @@ describe('DataView.vue', () => {
expect(pivot.saveAsSvg.calledOnce).to.equal(true)
})
it('method saveAsHtml calls the same method of the current view component', async () => {
const wrapper = mount(DataView)
// Find chart and spy the method
const chart = wrapper.findComponent({ name: 'Chart' }).vm
sinon.spy(chart, 'saveAsHtml')
// Export to html
const htmlBtn = createWrapper(wrapper.findComponent({ name: 'htmlIcon' }).vm.$parent)
await htmlBtn.trigger('click')
expect(chart.saveAsHtml.calledOnce).to.equal(true)
// Switch to pivot
const pivotBtn = createWrapper(wrapper.findComponent({ name: 'pivotIcon' }).vm.$parent)
await pivotBtn.trigger('click')
// Find pivot and spy the method
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.spy(pivot, 'saveAsHtml')
// Export to svg
await htmlBtn.trigger('click')
expect(pivot.saveAsHtml.calledOnce).to.equal(true)
})
it('shows alert when ClipboardItem is not supported', async () => {
const ClipboardItem = window.ClipboardItem
delete window.ClipboardItem

View File

@@ -5,6 +5,7 @@ import chartHelper from '@/lib/chartHelper'
import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
import sinon from 'sinon'
import pivotHelper from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
describe('Pivot.vue', () => {
let container
@@ -271,6 +272,41 @@ describe('Pivot.vue', () => {
expect(chartComponent.saveAsSvg.called).to.equal(true)
})
it('saveAsHtml calls chart method if renderer is Custom Chart', async () => {
const wrapper = mount(Pivot, {
propsData: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers['Custom chart'],
rendererName: 'Custom chart',
rendererOptions: {
customChartOptions: {
data: [],
layout: {},
frames: []
}
}
}
},
attachTo: container
})
const chartComponent = wrapper.vm.pivotOptions.rendererOptions.customChartComponent
sinon.stub(chartComponent, 'saveAsHtml')
await wrapper.vm.saveAsHtml()
expect(chartComponent.saveAsHtml.called).to.equal(true)
})
it('saveAsPng calls chart method if renderer is Custom Chart', async () => {
const wrapper = mount(Pivot, {
propsData: {
@@ -333,6 +369,66 @@ describe('Pivot.vue', () => {
expect(chartHelper.getImageDataUrl.calledOnce).to.equal(true)
})
it('saveAsHtml - standart chart', async () => {
sinon.spy(chartHelper, 'getChartData')
sinon.spy(chartHelper, 'getHtml')
const wrapper = mount(Pivot, {
propsData: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers['Bar Chart'],
rendererName: 'Bar Chart'
}
},
attachTo: container
})
await wrapper.vm.saveAsHtml()
expect(chartHelper.getChartData.calledOnce).to.equal(true)
const chartData = await chartHelper.getChartData.returnValues[0]
expect(chartHelper.getHtml.calledOnceWith(chartData)).to.equal(true)
})
it('saveAsHtml - table', async () => {
sinon.stub(pivotHelper, 'getPivotHtml')
sinon.stub(fIo, 'exportToFile')
const wrapper = mount(Pivot, {
propsData: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers.Table,
rendererName: 'Table'
}
},
attachTo: container
})
await wrapper.vm.saveAsHtml()
expect(pivotHelper.getPivotHtml.calledOnce).to.equal(true)
const html = pivotHelper.getPivotHtml.returnValues[0]
expect(fIo.exportToFile.calledOnceWith(html, 'pivot.html', 'text/html')).to.equal(true)
})
it('saveAsPng - standart chart', async () => {
sinon.stub(chartHelper, 'getImageDataUrl').returns('standat chart data url')
sinon.stub(fIo, 'downloadFromUrl')
@@ -365,6 +461,7 @@ describe('Pivot.vue', () => {
})
it('saveAsPng - table', async () => {
sinon.stub(pivotHelper, 'getPivotCanvas').returns(document.createElement('canvas'))
sinon.stub(HTMLCanvasElement.prototype, 'toDataURL').returns('canvas data url')
sinon.stub(fIo, 'downloadFromUrl')

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { _getDataSources, getPivotCanvas }
from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
import { _getDataSources, getPivotCanvas, getPivotHtml }
from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
describe('pivotHelper.js', () => {
it('_getDataSources returns data sources', () => {
@@ -63,4 +63,19 @@ describe('pivotHelper.js', () => {
expect(await getPivotCanvas(pivotOutput)).to.be.instanceof(HTMLCanvasElement)
})
it('getPivotHtml returns html with styles', async () => {
const pivotOutput = document.createElement('div')
pivotOutput.append('test')
const html = getPivotHtml(pivotOutput)
const doc = document.createElement('div')
doc.innerHTML = html
expect(doc.innerHTML).to.equal(html)
expect(doc.children).to.have.lengthOf(2)
expect(doc.children[0].tagName).to.equal('STYLE')
expect(doc.children[1].tagName).to.equal('DIV')
expect(doc.children[1].innerHTML).to.equal('test')
})
})

View File

@@ -41,5 +41,12 @@ module.exports = {
.end()
config.module.rule('js').exclude.add(/worker\.js$/)
config
.plugin('html')
.tap(args => {
args[0].minify = false
return args
})
}
}