mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
552 lines
14 KiB
Vue
552 lines
14 KiB
Vue
<template>
|
|
<div class="db-uploader-container" :style="{ width }">
|
|
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
|
<div v-if="type === 'illustrated'" class="drop-area-container">
|
|
<div
|
|
class="drop-area"
|
|
@dragover.prevent="state = 'dragover'"
|
|
@dragleave.prevent="state=''"
|
|
@drop.prevent="drop"
|
|
@click="browse"
|
|
>
|
|
<div class="text">
|
|
Drop the database or CSV file here or click to choose a file from your computer.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="type === 'illustrated'" id="img-container">
|
|
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
|
|
<img
|
|
id="left-arm-img"
|
|
:class="{'swing': state === 'dragover'}"
|
|
:src="require('@/assets/images/leftArm.svg')"
|
|
/>
|
|
<img
|
|
id="file-img"
|
|
ref="fileImg"
|
|
:class="{
|
|
'swing': state === 'dragover',
|
|
'fly': state === 'dropping',
|
|
'hidden': state === 'dropped'
|
|
}"
|
|
:src="require('@/assets/images/file.png')"
|
|
/>
|
|
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
|
|
<img id="body-img" :src="require('@/assets/images/body.svg')" />
|
|
<img
|
|
id="right-arm-img"
|
|
:class="{'swing': state === 'dragover'}"
|
|
:src="require('@/assets/images/rightArm.svg')"
|
|
/>
|
|
</div>
|
|
<div id="error" class="error"></div>
|
|
|
|
<!--Parse csv dialog -->
|
|
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
|
|
<div class="dialog-header">
|
|
Import CSV
|
|
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
|
</div>
|
|
<div class="dialog-body">
|
|
<div class="chars">
|
|
<delimiter-selector
|
|
v-model="delimiter"
|
|
width="210px"
|
|
class="char-input"
|
|
@input="previewCSV"
|
|
:disabled="disableDialog"
|
|
/>
|
|
<text-field
|
|
label="Quote char"
|
|
hint="The character used to quote fields."
|
|
v-model="quoteChar"
|
|
width="93px"
|
|
:disabled="disableDialog"
|
|
class="char-input"
|
|
id="quote-char"
|
|
/>
|
|
<text-field
|
|
label="Escape char"
|
|
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
|
|
max-hint-width="242px"
|
|
v-model="escapeChar"
|
|
width="93px"
|
|
:disabled="disableDialog"
|
|
class="char-input"
|
|
id="escape-char"
|
|
/>
|
|
</div>
|
|
<check-box
|
|
@click="header = $event"
|
|
:init="true"
|
|
label="Use first row as column headers"
|
|
:disabled="disableDialog"
|
|
/>
|
|
<sql-table
|
|
v-if="previewData"
|
|
:data-set="previewData"
|
|
height="160"
|
|
class="preview-table"
|
|
:preview="true"
|
|
/>
|
|
<div v-if="!previewData" class="no-data">No data</div>
|
|
<logs
|
|
class="import-csv-errors"
|
|
:messages="importCsvMessages"
|
|
/>
|
|
</div>
|
|
<div class="dialog-buttons-container">
|
|
<button
|
|
class="secondary"
|
|
:disabled="disableDialog"
|
|
@click="cancelCsvImport"
|
|
id="csv-cancel"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
v-show="!importCsvCompleted"
|
|
class="primary"
|
|
:disabled="disableDialog"
|
|
@click="loadFromCsv(file)"
|
|
id="csv-import"
|
|
>
|
|
Import
|
|
</button>
|
|
<button
|
|
v-show="importCsvCompleted"
|
|
class="primary"
|
|
:disabled="disableDialog"
|
|
@click="finish"
|
|
id="csv-finish"
|
|
>
|
|
Finish
|
|
</button>
|
|
</div>
|
|
</modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import fIo from '@/lib/utils/fileIo'
|
|
import csv from './csv'
|
|
import CloseIcon from '@/components/svg/close'
|
|
import TextField from '@/components/TextField'
|
|
import DelimiterSelector from './DelimiterSelector'
|
|
import CheckBox from '@/components/CheckBox'
|
|
import SqlTable from '@/components/SqlTable'
|
|
import Logs from '@/components/Logs'
|
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
|
import time from '@/lib/utils/time'
|
|
import database from '@/lib/database'
|
|
|
|
export default {
|
|
name: 'DbUploader',
|
|
props: {
|
|
type: {
|
|
type: String,
|
|
required: false,
|
|
default: 'small',
|
|
validator: (value) => {
|
|
return ['illustrated', 'small'].includes(value)
|
|
}
|
|
},
|
|
width: {
|
|
type: String,
|
|
required: false,
|
|
default: 'unset'
|
|
}
|
|
},
|
|
components: {
|
|
ChangeDbIcon,
|
|
TextField,
|
|
DelimiterSelector,
|
|
CloseIcon,
|
|
CheckBox,
|
|
SqlTable,
|
|
Logs
|
|
},
|
|
data () {
|
|
return {
|
|
state: '',
|
|
animationPromise: Promise.resolve(),
|
|
file: null,
|
|
schema: null,
|
|
delimiter: '',
|
|
quoteChar: '"',
|
|
escapeChar: '"',
|
|
header: true,
|
|
previewData: null,
|
|
importCsvMessages: [],
|
|
disableDialog: false,
|
|
importCsvCompleted: false,
|
|
newDb: null
|
|
}
|
|
},
|
|
mounted () {
|
|
if (this.type === 'illustrated') {
|
|
this.animationPromise = new Promise((resolve) => {
|
|
this.$refs.fileImg.addEventListener('animationend', event => {
|
|
if (event.animationName.startsWith('fly')) {
|
|
this.state = 'dropped'
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
},
|
|
watch: {
|
|
quoteChar () {
|
|
this.previewCSV()
|
|
},
|
|
|
|
escapeChar () {
|
|
this.previewCSV()
|
|
},
|
|
|
|
header () {
|
|
this.previewCSV()
|
|
}
|
|
},
|
|
methods: {
|
|
cancelCsvImport () {
|
|
if (!this.disableDialog) {
|
|
this.$modal.hide('parse')
|
|
if (this.newDb) {
|
|
this.newDb.shutDown()
|
|
this.newDb = null
|
|
}
|
|
}
|
|
},
|
|
|
|
async finish () {
|
|
this.$store.commit('setDb', this.newDb)
|
|
this.$store.commit('saveSchema', this.schema)
|
|
if (this.importCsvCompleted) {
|
|
this.$modal.hide('parse')
|
|
const stmt = [
|
|
'/*',
|
|
' * Your CSV file has been imported into csv_import table.',
|
|
' * You can run this SQL query to make all CSV records available for charting.',
|
|
' */',
|
|
'SELECT * FROM csv_import'
|
|
].join('\n')
|
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
|
this.$store.commit('setCurrentTabId', tabId)
|
|
this.importCsvCompleted = false
|
|
}
|
|
if (this.$route.path !== '/editor') {
|
|
this.$router.push('/editor')
|
|
}
|
|
},
|
|
|
|
async previewCSV () {
|
|
this.importCsvCompleted = false
|
|
const config = {
|
|
preview: 3,
|
|
quoteChar: this.quoteChar || '"',
|
|
escapeChar: this.escapeChar,
|
|
header: this.header,
|
|
delimiter: this.delimiter
|
|
}
|
|
try {
|
|
const start = new Date()
|
|
const parseResult = await csv.parse(this.file, config)
|
|
const end = new Date()
|
|
this.previewData = parseResult.data
|
|
this.delimiter = parseResult.delimiter
|
|
|
|
// In parseResult.messages we can get parse errors
|
|
this.importCsvMessages = parseResult.messages || []
|
|
|
|
if (!parseResult.hasErrors) {
|
|
this.importCsvMessages.push({
|
|
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
|
type: 'success'
|
|
})
|
|
}
|
|
} catch (err) {
|
|
this.importCsvMessages = [{
|
|
message: err,
|
|
type: 'error'
|
|
}]
|
|
}
|
|
},
|
|
|
|
loadDb (file) {
|
|
this.newDb = database.getNewDatabase()
|
|
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
|
|
.then(([schema]) => {
|
|
this.schema = schema
|
|
this.finish()
|
|
})
|
|
},
|
|
|
|
async loadFromCsv (file) {
|
|
this.disableDialog = true
|
|
const config = {
|
|
quoteChar: this.quoteChar || '"',
|
|
escapeChar: this.escapeChar,
|
|
header: this.header,
|
|
delimiter: this.delimiter
|
|
}
|
|
const parseCsvMsg = {
|
|
message: 'Parsing CSV...',
|
|
type: 'info'
|
|
}
|
|
this.importCsvMessages.push(parseCsvMsg)
|
|
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
|
|
|
const importMsg = {
|
|
message: 'Importing CSV into a SQLite database...',
|
|
type: 'info'
|
|
}
|
|
let importLoadingIndicator = null
|
|
|
|
const updateProgress = progress => {
|
|
this.$set(importMsg, 'progress', progress)
|
|
}
|
|
this.newDb = database.getNewDatabase()
|
|
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
|
|
|
|
try {
|
|
let start = new Date()
|
|
const parseResult = await csv.parse(this.file, config)
|
|
let end = new Date()
|
|
|
|
if (!parseResult.hasErrors) {
|
|
const rowCount = parseResult.data.values.length
|
|
let period = time.getPeriod(start, end)
|
|
parseCsvMsg.type = 'success'
|
|
|
|
if (parseResult.messages.length > 0) {
|
|
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
|
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
|
} else {
|
|
// Inform about csv parsing success
|
|
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
|
}
|
|
|
|
// Loading indicator for csv parsing is not needed anymore
|
|
clearTimeout(parseCsvLoadingIndicator)
|
|
|
|
// Add info about import start
|
|
this.importCsvMessages.push(importMsg)
|
|
|
|
// Show import progress after 1 second
|
|
importLoadingIndicator = setTimeout(() => {
|
|
importMsg.type = 'loading'
|
|
}, 1000)
|
|
|
|
// Create db with csv table and get schema
|
|
const name = file.name.replace(/\.[^.]+$/, '')
|
|
start = new Date()
|
|
this.schema = await this.newDb.importDb(name, parseResult.data, progressCounterId)
|
|
end = new Date()
|
|
|
|
// Inform about import success
|
|
period = time.getPeriod(start, end)
|
|
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
|
importMsg.type = 'success'
|
|
|
|
// Loading indicator for import is not needed anymore
|
|
clearTimeout(importLoadingIndicator)
|
|
|
|
this.importCsvCompleted = true
|
|
} else {
|
|
parseCsvMsg.message = 'Parsing ended with errors.'
|
|
parseCsvMsg.type = 'info'
|
|
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
|
}
|
|
} catch (err) {
|
|
if (parseCsvMsg.type === 'loading') {
|
|
parseCsvMsg.type = 'info'
|
|
}
|
|
|
|
if (importMsg.type === 'loading') {
|
|
importMsg.type = 'info'
|
|
}
|
|
|
|
this.importCsvMessages.push({
|
|
message: err,
|
|
type: 'error'
|
|
})
|
|
}
|
|
|
|
clearTimeout(parseCsvLoadingIndicator)
|
|
clearTimeout(importLoadingIndicator)
|
|
this.newDb.deleteProgressCounter(progressCounterId)
|
|
this.disableDialog = false
|
|
},
|
|
|
|
async checkFile (file) {
|
|
this.state = 'dropping'
|
|
if (fIo.isDatabase(file)) {
|
|
this.loadDb(file)
|
|
} else {
|
|
this.file = file
|
|
this.header = true
|
|
this.quoteChar = '"'
|
|
this.escapeChar = '"'
|
|
this.delimiter = ''
|
|
return Promise.all([this.previewCSV(), this.animationPromise])
|
|
.then(() => {
|
|
this.$modal.show('parse')
|
|
})
|
|
}
|
|
},
|
|
browse () {
|
|
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
|
.then(this.checkFile)
|
|
},
|
|
|
|
drop (event) {
|
|
this.checkFile(event.dataTransfer.files[0])
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.db-uploader-container {
|
|
position: relative;
|
|
}
|
|
.drop-area-container {
|
|
display: inline-block;
|
|
border: 1px dashed var(--color-border);
|
|
padding: 8px;
|
|
border-radius: var(--border-radius-big);
|
|
height: 100%;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.drop-area {
|
|
background-color: var(--color-bg-light-3);
|
|
border-radius: var(--border-radius-big);
|
|
color: var(--color-text-base);
|
|
font-size: 13px;
|
|
text-align: center;
|
|
box-sizing: border-box;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#img-container {
|
|
position: absolute;
|
|
top: 54px;
|
|
left: 50%;
|
|
transform: translate(-50%, 0);
|
|
width: 450px;
|
|
height: 338px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#drop-file-top-img {
|
|
width: 450px;
|
|
height: 175px;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
}
|
|
#drop-file-bottom-img {
|
|
width: 450px;
|
|
height: 167px;
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
}
|
|
#body-img {
|
|
width: 74px;
|
|
position: absolute;
|
|
top: 94.05px;
|
|
left: 46px;
|
|
}
|
|
#right-arm-img {
|
|
width: 106px;
|
|
position: absolute;
|
|
top: 110.05px;
|
|
left: 78px;
|
|
}
|
|
#left-arm-img {
|
|
width: 114px;
|
|
position: absolute;
|
|
top: 69.05px;
|
|
left: 69px;
|
|
}
|
|
#file-img {
|
|
width: 125px;
|
|
position: absolute;
|
|
top: 15.66px;
|
|
left: 152px;
|
|
}
|
|
|
|
.swing {
|
|
animation: swing ease-in-out 0.6s infinite alternate;
|
|
}
|
|
#left-arm-img.swing {
|
|
transform-origin: 9px 83px;
|
|
}
|
|
#right-arm-img.swing {
|
|
transform-origin: 0 56px;
|
|
}
|
|
#file-img.swing {
|
|
transform-origin: -74px 139px;
|
|
}
|
|
@keyframes swing {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(-7deg); }
|
|
}
|
|
|
|
#file-img.fly {
|
|
animation: fly ease-in-out 1s 1 normal;
|
|
transform-origin: center center;
|
|
}
|
|
@keyframes fly {
|
|
100% {
|
|
transform: rotate(360deg) scale(0.5);
|
|
top: 183px;
|
|
left: 225px;
|
|
}
|
|
}
|
|
|
|
#file-img.hidden {
|
|
display: none;
|
|
}
|
|
|
|
/* Parse CSV dialog */
|
|
.chars {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
margin-bottom: 20px;
|
|
}
|
|
.char-input {
|
|
margin-right: 44px;
|
|
}
|
|
.preview-table {
|
|
margin-top: 32px;
|
|
}
|
|
|
|
.import-csv-errors {
|
|
height: 160px;
|
|
margin-top: 32px;
|
|
}
|
|
.no-data {
|
|
margin-top: 32px;
|
|
background-color: white;
|
|
border-radius: 5px;
|
|
position: relative;
|
|
border: 1px solid var(--color-border-light);
|
|
box-sizing: border-box;
|
|
height: 160px;
|
|
font-size: 13px;
|
|
color: var(--color-text-base);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
</style>
|