mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
4 Commits
graph
...
4232f15c04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4232f15c04 | ||
|
|
9d562d11b8 | ||
|
|
54cdbbc8b9 | ||
|
|
1601514cca |
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
@@ -21,9 +21,8 @@ jobs:
|
|||||||
- name: Install browsers
|
- name: Install browsers
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
sudo add-apt-repository -y ppa:mozillateam/ppa
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y chromium-browser firefox-esr
|
sudo apt-get install -y chromium-browser firefox
|
||||||
|
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@10
|
run: npm install -g npm@10
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM node:12.22-bullseye
|
FROM node:12.22-buster
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
apt update; \
|
apt update; \
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
|||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
|
|
||||||
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
|
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
|
||||||
|
|
||||||
# Extension-functions
|
# Extension-functions
|
||||||
# ===================
|
# ===================
|
||||||
@@ -22,15 +22,15 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
|
|||||||
extension_urls = (
|
extension_urls = (
|
||||||
# Miscellaneous extensions
|
# Miscellaneous extensions
|
||||||
# ========================
|
# ========================
|
||||||
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
|
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
|
||||||
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_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/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||||
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
|
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
||||||
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
|
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
|
||||||
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
|
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
||||||
# Third-party extension
|
# Third-party extension
|
||||||
# =====================
|
# =====================
|
||||||
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||||
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||||
# Third-party extension with own dependencies
|
# Third-party extension with own dependencies
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
2
lib/sql-js/dist/sql-wasm.js
vendored
2
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Binary file not shown.
23776
package-lock.json
generated
23776
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.27.1",
|
"version": "0.26.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
50
src/App.vue
50
src/App.vue
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<modals-container />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
|
import { ModalsContainer } from 'vue-final-modal'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: { ModalsContainer },
|
||||||
computed: {
|
computed: {
|
||||||
inquiries() {
|
inquiries() {
|
||||||
return this.$store.state.inquiries
|
return this.$store.state.inquiries
|
||||||
@@ -23,16 +26,53 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
addEventListener('storage', event => {
|
|
||||||
if (event.key === storedInquiries.myInquiriesKey) {
|
|
||||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-Regular.woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-Bold.woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-Italic.woff2');
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
|
||||||
|
font-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
#app,
|
#app,
|
||||||
.dialog,
|
.dialog,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -4,10 +4,3 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-view-warning {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-Regular.woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-Bold.woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-Italic.woff2');
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
|
|
||||||
font-weight: 600;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Open Sans';
|
|
||||||
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
|
|
||||||
font-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: var(--color-accent-shade);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Field label="Scaling ratio">
|
<Field label="Adjust sizes">
|
||||||
<NumericInput
|
|
||||||
:value="modelValue.scalingRatio"
|
|
||||||
@update="update('scalingRatio', $event)"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Prevent overlapping">
|
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="booleanOptions"
|
:options="booleanOptions"
|
||||||
:activeOption="modelValue.adjustSizes"
|
:activeOption="modelValue.adjustSizes"
|
||||||
@@ -29,6 +22,13 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Gravity">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.gravity"
|
||||||
|
@update="update('gravity', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field label="Strong gravity mode">
|
<Field label="Strong gravity mode">
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="booleanOptions"
|
:options="booleanOptions"
|
||||||
|
|||||||
@@ -39,10 +39,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field v-if="modelValue.type !== 'constant'" label="Color as">
|
||||||
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
|
||||||
label="Color as"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="сolorAsOptions"
|
:options="сolorAsOptions"
|
||||||
:activeOption="modelValue.mode"
|
:activeOption="modelValue.mode"
|
||||||
@@ -50,10 +47,7 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
|
||||||
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
|
||||||
label="Colorscale direction"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="сolorscaleDirections"
|
:options="сolorscaleDirections"
|
||||||
:activeOption="modelValue.colorscaleDirection"
|
:activeOption="modelValue.colorscaleDirection"
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Gravity">
|
<Field label="Scaling ratio">
|
||||||
<NumericInput
|
<NumericInput
|
||||||
:value="modelValue.gravity"
|
:value="modelValue.scalingRatio"
|
||||||
@update="update('gravity', $event)"
|
@update="update('scalingRatio', $event)"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field v-if="modelValue.type === 'variable'">
|
<Field>
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="colorSourceUsageOptions"
|
:options="colorSourceUsageOptions"
|
||||||
:activeOption="modelValue.sourceUsage"
|
:activeOption="modelValue.sourceUsage"
|
||||||
@@ -45,10 +45,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field v-if="modelValue.type !== 'constant'" label="Color as">
|
||||||
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
|
||||||
label="Color as"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="сolorAsOptions"
|
:options="сolorAsOptions"
|
||||||
:activeOption="modelValue.mode"
|
:activeOption="modelValue.mode"
|
||||||
@@ -56,10 +53,7 @@
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
|
||||||
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
|
||||||
label="Colorscale direction"
|
|
||||||
>
|
|
||||||
<RadioBlocks
|
<RadioBlocks
|
||||||
:options="сolorscaleDirections"
|
:options="сolorscaleDirections"
|
||||||
:activeOption="modelValue.colorscaleDirection"
|
:activeOption="modelValue.colorscaleDirection"
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal
|
<modal
|
||||||
v-model="show"
|
:modalId="name"
|
||||||
class="dialog"
|
class="dialog"
|
||||||
:clickToClose="false"
|
:clickToClose="false"
|
||||||
:contentTransition="{ name: 'loading-dialog' }"
|
:contentTransition="{ name: 'loading-dialog' }"
|
||||||
:overlayTransition="{ name: 'loading-dialog' }"
|
:overlayTransition="{ name: 'loading-dialog' }"
|
||||||
@update:model-value="$emit('update:modelValue', $event)"
|
|
||||||
>
|
>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<close-icon :disabled="loading" @click="cancel" />
|
<close-icon :disabled="loading" @click="$emit('cancel')" />
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<div v-if="loading" class="loading-dialog-body">
|
<div v-if="loading" class="loading-dialog-body">
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
class="secondary"
|
class="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@click="cancel"
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -53,33 +52,24 @@ export default {
|
|||||||
name: 'LoadingDialog',
|
name: 'LoadingDialog',
|
||||||
components: { LoadingIndicator, CloseIcon },
|
components: { LoadingIndicator, CloseIcon },
|
||||||
props: {
|
props: {
|
||||||
modelValue: Boolean,
|
|
||||||
loadingMsg: String,
|
loadingMsg: String,
|
||||||
successMsg: String,
|
successMsg: String,
|
||||||
actionBtnName: String,
|
actionBtnName: String,
|
||||||
|
name: String,
|
||||||
title: String,
|
title: String,
|
||||||
loading: Boolean
|
loading: Boolean
|
||||||
},
|
},
|
||||||
emits: ['cancel', 'action', 'update:modelValue'],
|
emits: ['cancel', 'action'],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show: this.modelValue
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
modelValue() {
|
|
||||||
this.show = this.modelValue
|
|
||||||
},
|
|
||||||
loading() {
|
loading() {
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
this.$emit('update:modelValue', true)
|
this.$modal.show(this.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
this.$emit('update:modelValue', false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,39 +5,8 @@ const TYPE_NODE = 0
|
|||||||
const TYPE_EDGE = 1
|
const TYPE_EDGE = 1
|
||||||
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
|
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
|
||||||
|
|
||||||
export function dataSourceIsValid(dataSources) {
|
|
||||||
const docColumn = Object.keys(dataSources)[0]
|
|
||||||
if (!docColumn) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const records = dataSources[docColumn].slice(0, 10)
|
|
||||||
records.forEach(record => {
|
|
||||||
const parsedRec = JSON.parse(record)
|
|
||||||
if (Object.keys(parsedRec).length < 2) {
|
|
||||||
throw new Error('The records must have at least 2 keys')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const firstRecord = JSON.parse(records[0])
|
|
||||||
if (
|
|
||||||
!Object.keys(firstRecord).some(key => {
|
|
||||||
return records
|
|
||||||
.map(record => JSON.parse(record)[key])
|
|
||||||
.every(value => value === 0 || value === 1)
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'There must be a common key used as object type: 0 - node, 1 - edge'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} catch (err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildNodes(graph, dataSources, options) {
|
export function buildNodes(graph, dataSources, options) {
|
||||||
const docColumn = Object.keys(dataSources)[0]
|
const docColumn = Object.keys(dataSources)[0] || 'doc'
|
||||||
const { objectType, nodeId } = options.structure
|
const { objectType, nodeId } = options.structure
|
||||||
|
|
||||||
if (objectType && nodeId) {
|
if (objectType && nodeId) {
|
||||||
@@ -45,12 +14,10 @@ export function buildNodes(graph, dataSources, options) {
|
|||||||
.map(json => JSON.parse(json))
|
.map(json => JSON.parse(json))
|
||||||
.filter(item => item[objectType] === TYPE_NODE)
|
.filter(item => item[objectType] === TYPE_NODE)
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
if (node[nodeId]) {
|
graph.addNode(node[nodeId], {
|
||||||
graph.addNode(node[nodeId], {
|
data: node,
|
||||||
data: node,
|
labelColor: options.style.nodes.label.color
|
||||||
labelColor: options.style.nodes.label.color
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,23 +110,10 @@ function getUpdateSizeMethod(graph, sizeSettings) {
|
|||||||
if (type === 'constant') {
|
if (type === 'constant') {
|
||||||
return attributes => (attributes.size = value)
|
return attributes => (attributes.size = value)
|
||||||
} else if (type === 'variable') {
|
} else if (type === 'variable') {
|
||||||
return attributes => {
|
return getVariabledSizeMethod(mode, source, scale, min)
|
||||||
attributes.size = getVariabledSize(
|
|
||||||
mode,
|
|
||||||
attributes.data[source],
|
|
||||||
scale,
|
|
||||||
min
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return (attributes, nodeId) => {
|
return (attributes, nodeId) =>
|
||||||
attributes.size = getVariabledSize(
|
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
|
||||||
mode,
|
|
||||||
graph[method](nodeId),
|
|
||||||
scale,
|
|
||||||
min
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,13 +184,22 @@ function getUpdateEdgeColorMethod(graph, colorSettings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVariabledSize(mode, value, scale, min) {
|
function getVariabledSizeMethod(mode, source, scale, min) {
|
||||||
if (mode === 'diameter') {
|
if (mode === 'diameter') {
|
||||||
return Math.max((value / 2) * scale, min / 2)
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(
|
||||||
|
(attributes.data[source] / 2) * scale,
|
||||||
|
min / 2
|
||||||
|
))
|
||||||
} else if (mode === 'area') {
|
} else if (mode === 'area') {
|
||||||
return Math.max(Math.sqrt((value / 2) * scale), min / 2)
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(
|
||||||
|
Math.sqrt((attributes.data[source] / 2) * scale),
|
||||||
|
min / 2
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
return Math.max(value * scale, min)
|
return attributes =>
|
||||||
|
(attributes.size = Math.max(attributes.data[source] * scale, min))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import events from '@/lib/utils/events'
|
|||||||
import migration from './_migrations'
|
import migration from './_migrations'
|
||||||
|
|
||||||
const migrate = migration._migrate
|
const migrate = migration._migrate
|
||||||
const myInquiriesKey = 'myInquiries'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
version: 2,
|
version: 2,
|
||||||
myInquiriesKey,
|
|
||||||
getStoredInquiries() {
|
getStoredInquiries() {
|
||||||
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
|
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
|
||||||
if (!myInquiries) {
|
if (!myInquiries) {
|
||||||
const oldInquiries = localStorage.getItem('myQueries')
|
const oldInquiries = localStorage.getItem('myQueries')
|
||||||
if (oldInquiries) {
|
if (oldInquiries) {
|
||||||
@@ -28,8 +26,7 @@ export default {
|
|||||||
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
||||||
newInquiry.name = newInquiry.name + ' Copy'
|
newInquiry.name = newInquiry.name + ' Copy'
|
||||||
newInquiry.id = nanoid()
|
newInquiry.id = nanoid()
|
||||||
newInquiry.createdAt = new Date().toJSON()
|
newInquiry.createdAt = new Date()
|
||||||
newInquiry.updatedAt = new Date().toJSON()
|
|
||||||
delete newInquiry.isPredefined
|
delete newInquiry.isPredefined
|
||||||
|
|
||||||
return newInquiry
|
return newInquiry
|
||||||
@@ -41,7 +38,7 @@ export default {
|
|||||||
|
|
||||||
updateStorage(inquiries) {
|
updateStorage(inquiries) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
myInquiriesKey,
|
'myInquiries',
|
||||||
JSON.stringify({ version: this.version, inquiries })
|
JSON.stringify({ version: this.version, inquiries })
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ export default class Tab {
|
|||||||
|
|
||||||
this.isSaved = !!inquiry.id
|
this.isSaved = !!inquiry.id
|
||||||
this.state = state
|
this.state = state
|
||||||
this.updatedAt = inquiry.updatedAt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import store from '@/store'
|
|||||||
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
|
import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
|
||||||
|
|
||||||
import '@/assets/styles/variables.css'
|
import '@/assets/styles/variables.css'
|
||||||
import '@/assets/styles/typography.css'
|
|
||||||
import '@/assets/styles/buttons.css'
|
import '@/assets/styles/buttons.css'
|
||||||
import '@/assets/styles/tables.css'
|
import '@/assets/styles/tables.css'
|
||||||
import '@/assets/styles/dialogs.css'
|
import '@/assets/styles/dialogs.css'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import LoadView from '@/views/LoadView'
|
|||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
|
|
||||||
export const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Welcome',
|
name: 'Welcome',
|
||||||
|
|||||||
@@ -17,33 +17,28 @@ export default {
|
|||||||
},
|
},
|
||||||
async saveInquiry({ state }, { inquiryTab, newName }) {
|
async saveInquiry({ state }, { inquiryTab, newName }) {
|
||||||
const value = {
|
const value = {
|
||||||
id: inquiryTab.isPredefined || newName ? nanoid() : inquiryTab.id,
|
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||||
query: inquiryTab.query,
|
query: inquiryTab.query,
|
||||||
viewType: inquiryTab.dataView.mode,
|
viewType: inquiryTab.dataView.mode,
|
||||||
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||||
name: newName || inquiryTab.name,
|
name: newName || inquiryTab.name
|
||||||
updatedAt: new Date().toJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get inquiries from local storage
|
// Get inquiries from local storage
|
||||||
const myInquiries = state.inquiries
|
const myInquiries = state.inquiries
|
||||||
let inquiryIndex
|
|
||||||
// Set createdAt
|
// Set createdAt
|
||||||
if (newName) {
|
if (newName) {
|
||||||
value.createdAt = new Date().toJSON()
|
value.createdAt = new Date()
|
||||||
} else {
|
} else {
|
||||||
inquiryIndex = myInquiries.findIndex(
|
var inquiryIndex = myInquiries.findIndex(
|
||||||
oldInquiry => oldInquiry.id === inquiryTab.id
|
oldInquiry => oldInquiry.id === inquiryTab.id
|
||||||
)
|
)
|
||||||
|
value.createdAt = myInquiries[inquiryIndex].createdAt
|
||||||
value.createdAt =
|
|
||||||
inquiryIndex !== -1
|
|
||||||
? myInquiries[inquiryIndex].createdAt
|
|
||||||
: new Date().toJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert in inquiries list
|
// Insert in inquiries list
|
||||||
if (newName || inquiryIndex === -1) {
|
if (newName) {
|
||||||
myInquiries.push(value)
|
myInquiries.push(value)
|
||||||
} else {
|
} else {
|
||||||
myInquiries.splice(inquiryIndex, 1, value)
|
myInquiries.splice(inquiryIndex, 1, value)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateTab(state, { tab, newValues }) {
|
updateTab(state, { tab, newValues }) {
|
||||||
const { name, id, query, viewType, viewOptions, isSaved, updatedAt } =
|
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
||||||
newValues
|
|
||||||
const oldId = tab.id
|
const oldId = tab.id
|
||||||
|
|
||||||
if (id && state.currentTabId === oldId) {
|
if (id && state.currentTabId === oldId) {
|
||||||
@@ -37,9 +36,6 @@ export default {
|
|||||||
// Saved inquiry is not predefined
|
// Saved inquiry is not predefined
|
||||||
delete tab.isPredefined
|
delete tab.isPredefined
|
||||||
}
|
}
|
||||||
if (updatedAt) {
|
|
||||||
tab.updatedAt = updatedAt
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTab(state, tab) {
|
deleteTab(state, tab) {
|
||||||
|
|||||||
@@ -10,22 +10,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="nav-buttons">
|
<div id="nav-buttons">
|
||||||
<button
|
<button
|
||||||
v-show="currentInquiryTab && $route.path === '/workspace'"
|
v-show="currentInquiry && $route.path === '/workspace'"
|
||||||
id="save-btn"
|
id="save-btn"
|
||||||
class="primary"
|
class="primary"
|
||||||
:disabled="isSaved"
|
:disabled="isSaved"
|
||||||
@click="onSave(false)"
|
@click="checkInquiryBeforeSave"
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
v-show="currentInquiryTab && $route.path === '/workspace'"
|
|
||||||
id="save-as-btn"
|
|
||||||
class="primary"
|
|
||||||
@click="onSaveAs"
|
|
||||||
>
|
|
||||||
Save as
|
|
||||||
</button>
|
|
||||||
<button id="create-btn" class="primary" @click="createNewInquiry">
|
<button id="create-btn" class="primary" @click="createNewInquiry">
|
||||||
Create
|
Create
|
||||||
</button>
|
</button>
|
||||||
@@ -53,34 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||||
<button class="primary" @click="validateSaveFormAndSaveInquiry">
|
<button class="primary" @click="saveInquiry">Save</button>
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</modal>
|
|
||||||
|
|
||||||
<!-- Inquiery saving conflict dialog -->
|
|
||||||
<modal
|
|
||||||
modalId="inquiry-conflict"
|
|
||||||
class="dialog"
|
|
||||||
contentStyle="width: 560px;"
|
|
||||||
>
|
|
||||||
<div class="dialog-header">
|
|
||||||
Inquiry saving conflict
|
|
||||||
<close-icon @click="cancelSave" />
|
|
||||||
</div>
|
|
||||||
<div class="dialog-body">
|
|
||||||
<div id="save-note">
|
|
||||||
<img src="~@/assets/images/info.svg" />
|
|
||||||
This inquiry has been modified in the mean time. This can happen if an
|
|
||||||
inquiry is saved in another window or browser tab. Do you want to
|
|
||||||
overwrite that changes or save the current state as a new inquiry?
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-buttons-container">
|
|
||||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
|
||||||
<button class="primary" @click="onSave(true)">Overwrite</button>
|
|
||||||
<button class="primary" @click="onSaveAs">Save as new</button>
|
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -108,28 +73,25 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inquiries() {
|
currentInquiry() {
|
||||||
return this.$store.state.inquiries
|
|
||||||
},
|
|
||||||
currentInquiryTab() {
|
|
||||||
return this.$store.state.currentTab
|
return this.$store.state.currentTab
|
||||||
},
|
},
|
||||||
isSaved() {
|
isSaved() {
|
||||||
return this.currentInquiryTab && this.currentInquiryTab.isSaved
|
return this.currentInquiry && this.currentInquiry.isSaved
|
||||||
},
|
},
|
||||||
isPredefined() {
|
isPredefined() {
|
||||||
return this.currentInquiryTab && this.currentInquiryTab.isPredefined
|
return this.currentInquiry && this.currentInquiry.isPredefined
|
||||||
},
|
},
|
||||||
runDisabled() {
|
runDisabled() {
|
||||||
return (
|
return (
|
||||||
this.currentInquiryTab &&
|
this.currentInquiry &&
|
||||||
(!this.$store.state.db || !this.currentInquiryTab.query)
|
(!this.$store.state.db || !this.currentInquiry.query)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
eventBus.$on('createNewInquiry', this.createNewInquiry)
|
eventBus.$on('createNewInquiry', this.createNewInquiry)
|
||||||
eventBus.$on('saveInquiry', this.onSave)
|
eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
|
||||||
document.addEventListener('keydown', this._keyListener)
|
document.addEventListener('keydown', this._keyListener)
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
@@ -147,84 +109,63 @@ export default {
|
|||||||
events.send('inquiry.create', null, { auto: false })
|
events.send('inquiry.create', null, { auto: false })
|
||||||
},
|
},
|
||||||
cancelSave() {
|
cancelSave() {
|
||||||
this.errorMsg = null
|
|
||||||
this.name = ''
|
|
||||||
this.$modal.hide('save')
|
this.$modal.hide('save')
|
||||||
this.$modal.hide('inquiry-conflict')
|
|
||||||
eventBus.$off('inquirySaved')
|
eventBus.$off('inquirySaved')
|
||||||
},
|
},
|
||||||
onSave(skipConcurrentEditingCheck = false) {
|
checkInquiryBeforeSave() {
|
||||||
if (storedInquiries.isTabNeedName(this.currentInquiryTab)) {
|
|
||||||
this.openSaveModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipConcurrentEditingCheck) {
|
|
||||||
const inquiryInStore = this.inquiries.find(
|
|
||||||
inquiry => inquiry.id === this.currentInquiryTab.id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
inquiryInStore &&
|
|
||||||
inquiryInStore.updatedAt !== this.currentInquiryTab.updatedAt
|
|
||||||
) {
|
|
||||||
this.$modal.show('inquiry-conflict')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.saveInquiry()
|
|
||||||
},
|
|
||||||
onSaveAs() {
|
|
||||||
this.openSaveModal()
|
|
||||||
},
|
|
||||||
openSaveModal() {
|
|
||||||
this.$modal.hide('inquiry-conflict')
|
|
||||||
this.errorMsg = null
|
this.errorMsg = null
|
||||||
this.name = ''
|
this.name = ''
|
||||||
this.$modal.show('save')
|
|
||||||
|
if (storedInquiries.isTabNeedName(this.currentInquiry)) {
|
||||||
|
this.$modal.show('save')
|
||||||
|
} else {
|
||||||
|
this.saveInquiry()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
validateSaveFormAndSaveInquiry() {
|
async saveInquiry() {
|
||||||
if (!this.name) {
|
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
|
||||||
|
if (isNeedName && !this.name) {
|
||||||
this.errorMsg = "Inquiry name can't be empty"
|
this.errorMsg = "Inquiry name can't be empty"
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.saveInquiry()
|
const dataSet = this.currentInquiry.result
|
||||||
},
|
const tabView = this.currentInquiry.view
|
||||||
async saveInquiry() {
|
|
||||||
const eventName =
|
|
||||||
this.currentInquiryTab.name && this.name
|
|
||||||
? 'inquiry.saveAs'
|
|
||||||
: 'inquiry.save'
|
|
||||||
|
|
||||||
// Save inquiry
|
// Save inquiry
|
||||||
const value = await this.$store.dispatch('saveInquiry', {
|
const value = await this.$store.dispatch('saveInquiry', {
|
||||||
inquiryTab: this.currentInquiryTab,
|
inquiryTab: this.currentInquiry,
|
||||||
newName: this.name
|
newName: this.name
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update tab in store
|
// Update tab in store
|
||||||
this.$store.commit('updateTab', {
|
this.$store.commit('updateTab', {
|
||||||
tab: this.currentInquiryTab,
|
tab: this.currentInquiry,
|
||||||
newValues: {
|
newValues: {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
id: value.id,
|
id: value.id,
|
||||||
query: value.query,
|
query: value.query,
|
||||||
viewType: value.viewType,
|
viewType: value.viewType,
|
||||||
viewOptions: value.viewOptions,
|
viewOptions: value.viewOptions,
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: value.updatedAt
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Hide dialogs
|
// Restore data:
|
||||||
|
// e.g. if we save predefined inquiry the tab will be created again
|
||||||
|
// (because of new id) and
|
||||||
|
// it will be without sql result and has default view - table.
|
||||||
|
// That's why we need to restore data and view
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.currentInquiry.result = dataSet
|
||||||
|
this.currentInquiry.view = tabView
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hide dialog
|
||||||
this.$modal.hide('save')
|
this.$modal.hide('save')
|
||||||
this.$modal.hide('inquiry-conflict')
|
|
||||||
this.errorMsg = null
|
|
||||||
this.name = ''
|
|
||||||
|
|
||||||
// Signal about saving
|
// Signal about saving
|
||||||
eventBus.$emit('inquirySaved')
|
eventBus.$emit('inquirySaved')
|
||||||
events.send(eventName)
|
events.send('inquiry.save')
|
||||||
},
|
},
|
||||||
_keyListener(e) {
|
_keyListener(e) {
|
||||||
if (this.$route.path === '/workspace') {
|
if (this.$route.path === '/workspace') {
|
||||||
@@ -232,25 +173,19 @@ export default {
|
|||||||
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.runDisabled) {
|
if (!this.runDisabled) {
|
||||||
this.currentInquiryTab.execute()
|
this.currentInquiry.execute()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save inquiry Ctrl+S
|
// Save inquiry Ctrl+S
|
||||||
if (e.key === 's' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.isSaved) {
|
if (!this.isSaved) {
|
||||||
this.onSave()
|
this.checkInquiryBeforeSave()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Save inquiry as Ctrl+Shift+S
|
|
||||||
if (e.key === 'S' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.onSaveAs()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// New (blank) inquiry Ctrl+B
|
// New (blank) inquiry Ctrl+B
|
||||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="chartContainer" class="chart-container">
|
<div ref="chartContainer" class="chart-container">
|
||||||
<div v-show="!dataSources" class="warning data-view-warning">
|
<div v-show="!dataSources" class="warning chart-warning">
|
||||||
There is no data to build a chart. Run your SQL query and make sure the
|
There is no data to build a chart. Run your SQL query and make sure the
|
||||||
result is not empty.
|
result is not empty.
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
:useResizeHandler="useResizeHandler"
|
:useResizeHandler="useResizeHandler"
|
||||||
:debug="true"
|
:debug="true"
|
||||||
:advancedTraceTypeSelector="true"
|
:advancedTraceTypeSelector="true"
|
||||||
:hideControls="!showViewSettings"
|
|
||||||
@update="update"
|
@update="update"
|
||||||
@render="onRender"
|
@render="onRender"
|
||||||
/>
|
/>
|
||||||
@@ -48,8 +47,7 @@ export default {
|
|||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
exportToPngEnabled: Boolean,
|
exportToPngEnabled: Boolean,
|
||||||
exportToSvgEnabled: Boolean,
|
exportToSvgEnabled: Boolean,
|
||||||
forPivot: Boolean,
|
forPivot: Boolean
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'update:exportToSvgEnabled',
|
'update:exportToSvgEnabled',
|
||||||
@@ -87,9 +85,6 @@ export default {
|
|||||||
dereference.default(this.state.data, this.dataSources)
|
dereference.default(this.state.data, this.dataSources)
|
||||||
this.updatePlotly()
|
this.updatePlotly()
|
||||||
}
|
}
|
||||||
},
|
|
||||||
showViewSettings() {
|
|
||||||
this.handleResize()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -172,8 +167,11 @@ export default {
|
|||||||
'text/html'
|
'text/html'
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
prepareCopy(type = 'png') {
|
async prepareCopy(type = 'png') {
|
||||||
return chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
|
return await chartHelper.getImageDataUrl(
|
||||||
|
this.$refs.plotlyEditor.$el,
|
||||||
|
type
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,6 +182,13 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-warning {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
min-height: 242px;
|
min-height: 242px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="['plotly_editor', { with_controls: showViewSettings }]">
|
<div class="plotly_editor">
|
||||||
<GraphEditorControls v-show="showViewSettings">
|
<GraphEditorControls>
|
||||||
<PanelMenuWrapper>
|
<PanelMenuWrapper>
|
||||||
<Panel group="Structure" name="Graph">
|
<Panel group="Structure" name="Graph">
|
||||||
<Fold name="Graph">
|
<Fold name="Graph">
|
||||||
<Field>
|
<Field>Choose keys explanation...</Field>
|
||||||
Map your result set records to node and edge properties required
|
|
||||||
to build a graph. Learn more about result set requirements in the
|
|
||||||
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
|
|
||||||
documentation</a
|
|
||||||
>.
|
|
||||||
</Field>
|
|
||||||
<Field label="Object type">
|
<Field label="Object type">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
:options="keysOptions"
|
:options="keysOptions"
|
||||||
:value="settings.structure.objectType"
|
:value="settings.structure.objectType"
|
||||||
@change="updateStructure('objectType', $event)"
|
@change="updateStructure('objectType', $event)"
|
||||||
/>
|
/>
|
||||||
<Field>
|
<Field>0 - node; 1 - edge</Field>
|
||||||
A field indicating if the record is node (value 0) or edge
|
|
||||||
(value 1).
|
|
||||||
</Field>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Node Id">
|
<Field label="Node Id">
|
||||||
@@ -29,7 +20,6 @@
|
|||||||
:value="settings.structure.nodeId"
|
:value="settings.structure.nodeId"
|
||||||
@change="updateStructure('nodeId', $event)"
|
@change="updateStructure('nodeId', $event)"
|
||||||
/>
|
/>
|
||||||
<Field> A field keeping unique node identifier. </Field>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Edge source">
|
<Field label="Edge source">
|
||||||
@@ -38,9 +28,6 @@
|
|||||||
:value="settings.structure.edgeSource"
|
:value="settings.structure.edgeSource"
|
||||||
@change="updateStructure('edgeSource', $event)"
|
@change="updateStructure('edgeSource', $event)"
|
||||||
/>
|
/>
|
||||||
<Field>
|
|
||||||
A field keeping a node identifier where the edge starts.
|
|
||||||
</Field>
|
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Edge target">
|
<Field label="Edge target">
|
||||||
@@ -49,9 +36,6 @@
|
|||||||
:value="settings.structure.edgeTarget"
|
:value="settings.structure.edgeTarget"
|
||||||
@change="updateStructure('edgeTarget', $event)"
|
@change="updateStructure('edgeTarget', $event)"
|
||||||
/>
|
/>
|
||||||
<Field>
|
|
||||||
A field keeping a node identifier where the edge ends.
|
|
||||||
</Field>
|
|
||||||
</Field>
|
</Field>
|
||||||
</Fold>
|
</Fold>
|
||||||
</Panel>
|
</Panel>
|
||||||
@@ -180,7 +164,6 @@
|
|||||||
</Panel>
|
</Panel>
|
||||||
</PanelMenuWrapper>
|
</PanelMenuWrapper>
|
||||||
</GraphEditorControls>
|
</GraphEditorControls>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref="graph"
|
ref="graph"
|
||||||
:style="{
|
:style="{
|
||||||
@@ -226,7 +209,6 @@ import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
|
|||||||
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
|
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
|
||||||
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
|
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
|
||||||
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
|
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
|
||||||
import events from '@/lib/utils/events'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -253,8 +235,7 @@ export default {
|
|||||||
inject: ['tabLayout'],
|
inject: ['tabLayout'],
|
||||||
props: {
|
props: {
|
||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
},
|
||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
data() {
|
data() {
|
||||||
@@ -294,7 +275,7 @@ export default {
|
|||||||
nodes: {
|
nodes: {
|
||||||
size: {
|
size: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
value: 10
|
value: 4
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: 'constant',
|
type: 'constant',
|
||||||
@@ -378,14 +359,6 @@ export default {
|
|||||||
this.buildGraph()
|
this.buildGraph()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'settings.layout.type': {
|
|
||||||
immediate: true,
|
|
||||||
handler() {
|
|
||||||
events.send('viz_graph.render', null, {
|
|
||||||
layout: this.settings.layout.type
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tabLayout: {
|
tabLayout: {
|
||||||
deep: true,
|
deep: true,
|
||||||
handler() {
|
handler() {
|
||||||
@@ -638,7 +611,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.plotly_editor.with_controls > div {
|
.plotly_editor > div {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="graphContainer" class="graph-container">
|
<div ref="graphContainer" class="chart-container">
|
||||||
<div v-show="!dataSources" class="warning data-view-warning">
|
<div v-show="!dataSources" class="warning chart-warning">
|
||||||
There is no data to build a graph. Run your SQL query and make sure the
|
There is no data to build a graph. Run your SQL query and make sure the
|
||||||
result is not empty.
|
result is not empty.
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!dataSourceIsValid" class="warning data-view-warning">
|
|
||||||
Result set is invalid for graph visualisation. Learn more in
|
|
||||||
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
|
|
||||||
documentation</a
|
|
||||||
>.
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="graph"
|
class="graph"
|
||||||
:style="{
|
:style="{
|
||||||
height:
|
height: !dataSources ? 'calc(100% - 40px)' : '100%'
|
||||||
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<GraphEditor
|
<GraphEditor
|
||||||
ref="graphEditor"
|
ref="graphEditor"
|
||||||
:dataSources="dataSources"
|
:dataSources="dataSources"
|
||||||
:initOptions="initOptions"
|
:initOptions="initOptions"
|
||||||
:showViewSettings="showViewSettings"
|
|
||||||
@update="$emit('update')"
|
@update="$emit('update')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,8 +22,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
import GraphEditor from './GraphEditor.vue'
|
import GraphEditor from './GraphEditor.vue'
|
||||||
import { dataSourceIsValid } from '@/lib/graphHelper'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Graph',
|
name: 'Graph',
|
||||||
@@ -41,8 +33,7 @@ export default {
|
|||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
exportToPngEnabled: Boolean,
|
exportToPngEnabled: Boolean,
|
||||||
exportToSvgEnabled: Boolean,
|
exportToSvgEnabled: Boolean,
|
||||||
exportToHtmlEnabled: Boolean,
|
exportToHtmlEnabled: Boolean
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'update:exportToSvgEnabled',
|
'update:exportToSvgEnabled',
|
||||||
@@ -55,6 +46,7 @@ export default {
|
|||||||
resizeObserver: null
|
resizeObserver: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.$emit('update:exportToSvgEnabled', false)
|
this.$emit('update:exportToSvgEnabled', false)
|
||||||
this.$emit('update:exportToHtmlEnabled', false)
|
this.$emit('update:exportToHtmlEnabled', false)
|
||||||
@@ -66,17 +58,6 @@ export default {
|
|||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.resizeObserver.unobserve(this.$refs.graphContainer)
|
this.resizeObserver.unobserve(this.$refs.graphContainer)
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
async showViewSettings() {
|
|
||||||
await this.$nextTick()
|
|
||||||
this.handleResize()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
dataSourceIsValid() {
|
|
||||||
return !this.dataSources || dataSourceIsValid(this.dataSources)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getOptionsForSave() {
|
getOptionsForSave() {
|
||||||
return this.$refs.graphEditor.settings
|
return this.$refs.graphEditor.settings
|
||||||
@@ -92,7 +73,7 @@ export default {
|
|||||||
const renderer = this.$refs.graphEditor.renderer
|
const renderer = this.$refs.graphEditor.renderer
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer.refresh()
|
renderer.refresh()
|
||||||
renderer.getCamera().animatedReset({ duration: 600 })
|
renderer.getCamera().setState({ x: 0.5, y: 0.5 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,11 +81,18 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.graph-container {
|
.chart-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph {
|
.chart-warning {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
min-height: 242px;
|
min-height: 242px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +1,137 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="pivot-ui">
|
<div class="pivot-ui">
|
||||||
<div class="row">
|
<div :class="{ collapsed }">
|
||||||
<label>Columns</label>
|
<div class="row">
|
||||||
<multiselect
|
<label>Columns</label>
|
||||||
v-model="cols"
|
<multiselect
|
||||||
class="sqliteviz-select cols"
|
v-model="cols"
|
||||||
:options="colsToSelect"
|
class="sqliteviz-select cols"
|
||||||
:disabled="colsToSelect.length === 0"
|
:options="colsToSelect"
|
||||||
:multiple="true"
|
:disabled="colsToSelect.length === 0"
|
||||||
:hideSelected="true"
|
:multiple="true"
|
||||||
:closeOnSelect="true"
|
:hideSelected="true"
|
||||||
:showLabels="false"
|
:closeOnSelect="true"
|
||||||
:max="colsToSelect.length"
|
:showLabels="false"
|
||||||
openDirection="bottom"
|
:max="colsToSelect.length"
|
||||||
placeholder=""
|
openDirection="bottom"
|
||||||
>
|
placeholder=""
|
||||||
<template #maxElements>
|
>
|
||||||
<span class="no-results">No Results</span>
|
<template #maxElements>
|
||||||
</template>
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #placeholder>Choose columns</template>
|
<template #placeholder>Choose columns</template>
|
||||||
|
|
||||||
<template #noResult>
|
<template #noResult>
|
||||||
<span class="no-results">No Results</span>
|
<span class="no-results">No Results</span>
|
||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
|
<pivot-sort-btn v-model="colOrder" class="sort-btn" direction="col" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Rows</label>
|
<label>Rows</label>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="rows"
|
v-model="rows"
|
||||||
class="sqliteviz-select rows"
|
class="sqliteviz-select rows"
|
||||||
:options="rowsToSelect"
|
:options="rowsToSelect"
|
||||||
:disabled="rowsToSelect.length === 0"
|
:disabled="rowsToSelect.length === 0"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:hideSelected="true"
|
:hideSelected="true"
|
||||||
:closeOnSelect="true"
|
:closeOnSelect="true"
|
||||||
:showLabels="false"
|
:showLabels="false"
|
||||||
:max="rowsToSelect.length"
|
:max="rowsToSelect.length"
|
||||||
:optionHeight="29"
|
:optionHeight="29"
|
||||||
openDirection="bottom"
|
openDirection="bottom"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
>
|
>
|
||||||
<template #maxElements>
|
<template #maxElements>
|
||||||
<span class="no-results">No Results</span>
|
<span class="no-results">No Results</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #placeholder>Choose rows</template>
|
<template #placeholder>Choose rows</template>
|
||||||
|
|
||||||
<template #noResult>
|
<template #noResult>
|
||||||
<span class="no-results">No Results</span>
|
<span class="no-results">No Results</span>
|
||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
|
<pivot-sort-btn v-model="rowOrder" class="sort-btn" direction="row" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row aggregator">
|
<div class="row aggregator">
|
||||||
<label>Aggregator</label>
|
<label>Aggregator</label>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="aggregator"
|
v-model="aggregator"
|
||||||
class="sqliteviz-select short aggregator"
|
class="sqliteviz-select short aggregator"
|
||||||
:options="aggregators"
|
:options="aggregators"
|
||||||
label="name"
|
label="name"
|
||||||
trackBy="name"
|
trackBy="name"
|
||||||
:closeOnSelect="true"
|
:closeOnSelect="true"
|
||||||
:showLabels="false"
|
:showLabels="false"
|
||||||
:hideSelected="true"
|
:hideSelected="true"
|
||||||
:optionHeight="29"
|
:optionHeight="29"
|
||||||
openDirection="bottom"
|
openDirection="bottom"
|
||||||
placeholder="Choose a function"
|
placeholder="Choose a function"
|
||||||
>
|
>
|
||||||
<template #noResult>
|
<template #noResult>
|
||||||
<span class="no-results">No Results</span>
|
<span class="no-results">No Results</span>
|
||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
|
|
||||||
<multiselect
|
<multiselect
|
||||||
v-show="valCount > 0"
|
v-show="valCount > 0"
|
||||||
v-model="val1"
|
v-model="val1"
|
||||||
class="sqliteviz-select aggr-arg"
|
class="sqliteviz-select aggr-arg"
|
||||||
:options="keyNames"
|
:options="keyNames"
|
||||||
:disabled="keyNames.length === 0"
|
:disabled="keyNames.length === 0"
|
||||||
:closeOnSelect="true"
|
:closeOnSelect="true"
|
||||||
:showLabels="false"
|
:showLabels="false"
|
||||||
:hideSelected="true"
|
:hideSelected="true"
|
||||||
:optionHeight="29"
|
:optionHeight="29"
|
||||||
openDirection="bottom"
|
openDirection="bottom"
|
||||||
placeholder="Choose an argument"
|
placeholder="Choose an argument"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<multiselect
|
<multiselect
|
||||||
v-show="valCount > 1"
|
v-show="valCount > 1"
|
||||||
v-model="val2"
|
v-model="val2"
|
||||||
class="sqliteviz-select aggr-arg"
|
class="sqliteviz-select aggr-arg"
|
||||||
:options="keyNames"
|
:options="keyNames"
|
||||||
:disabled="keyNames.length === 0"
|
:disabled="keyNames.length === 0"
|
||||||
:closeOnSelect="true"
|
:closeOnSelect="true"
|
||||||
:showLabels="false"
|
:showLabels="false"
|
||||||
:hideSelected="true"
|
:hideSelected="true"
|
||||||
:optionHeight="29"
|
:optionHeight="29"
|
||||||
openDirection="bottom"
|
openDirection="bottom"
|
||||||
placeholder="Choose a second argument"
|
placeholder="Choose a second argument"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>View</label>
|
<label>View</label>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="renderer"
|
v-model="renderer"
|
||||||
class="sqliteviz-select short renderer"
|
class="sqliteviz-select short renderer"
|
||||||
:options="renderers"
|
:options="renderers"
|
||||||
label="name"
|
label="name"
|
||||||
trackBy="name"
|
trackBy="name"
|
||||||
:closeOnSelect="true"
|
:closeOnSelect="true"
|
||||||
:allowEmpty="false"
|
:allowEmpty="false"
|
||||||
:showLabels="false"
|
:showLabels="false"
|
||||||
:hideSelected="true"
|
:hideSelected="true"
|
||||||
:optionHeight="29"
|
:optionHeight="29"
|
||||||
openDirection="bottom"
|
openDirection="bottom"
|
||||||
placeholder="Choose a view"
|
placeholder="Choose a view"
|
||||||
>
|
>
|
||||||
<template #noResult>
|
<template #noResult>
|
||||||
<span class="no-results">No Results</span>
|
<span class="no-results">No Results</span>
|
||||||
</template>
|
</template>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="switcher" @click="collapsed = !collapsed">
|
||||||
|
{{ collapsed ? 'Show pivot settings' : 'Hide pivot settings' }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -158,6 +163,7 @@ export default {
|
|||||||
const rendererName =
|
const rendererName =
|
||||||
(this.modelValue && this.modelValue.rendererName) || 'Table'
|
(this.modelValue && this.modelValue.rendererName) || 'Table'
|
||||||
return {
|
return {
|
||||||
|
collapsed: false,
|
||||||
renderer: {
|
renderer: {
|
||||||
name: rendererName,
|
name: rendererName,
|
||||||
fun: $.pivotUtilities.renderers[rendererName]
|
fun: $.pivotUtilities.renderers[rendererName]
|
||||||
@@ -285,6 +291,9 @@ export default {
|
|||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.switcher {
|
.switcher {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
result is not empty.
|
result is not empty.
|
||||||
</div>
|
</div>
|
||||||
<pivot-ui
|
<pivot-ui
|
||||||
v-show="showViewSettings"
|
|
||||||
v-model="pivotOptions"
|
v-model="pivotOptions"
|
||||||
:keyNames="columns"
|
:keyNames="columns"
|
||||||
@update="$emit('update')"
|
@update="$emit('update')"
|
||||||
@@ -48,8 +47,7 @@ export default {
|
|||||||
dataSources: Object,
|
dataSources: Object,
|
||||||
initOptions: Object,
|
initOptions: Object,
|
||||||
exportToPngEnabled: Boolean,
|
exportToPngEnabled: Boolean,
|
||||||
exportToSvgEnabled: Boolean,
|
exportToSvgEnabled: Boolean
|
||||||
showViewSettings: Boolean
|
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'loadingImageCompleted',
|
'loadingImageCompleted',
|
||||||
@@ -127,9 +125,6 @@ export default {
|
|||||||
},
|
},
|
||||||
pivotOptions() {
|
pivotOptions() {
|
||||||
this.show()
|
this.show()
|
||||||
},
|
|
||||||
showViewSettings() {
|
|
||||||
this.handleResize()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -326,8 +321,4 @@ export default {
|
|||||||
.pivot-output:empty {
|
.pivot-output:empty {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.js-plotly-plot) {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
|
v-model:exportToHtmlEnabled="exportToHtmlEnabled"
|
||||||
:initOptions="initOptionsByMode[mode]"
|
:initOptions="initOptionsByMode[mode]"
|
||||||
:data-sources="dataSource"
|
:data-sources="dataSource"
|
||||||
:showViewSettings="showViewSettings"
|
|
||||||
@loading-image-completed="loadingImage = false"
|
@loading-image-completed="loadingImage = false"
|
||||||
@update="$emit('update')"
|
@update="$emit('update')"
|
||||||
/>
|
/>
|
||||||
@@ -43,18 +42,6 @@
|
|||||||
|
|
||||||
<div class="side-tool-bar-divider" />
|
<div class="side-tool-bar-divider" />
|
||||||
|
|
||||||
<icon-button
|
|
||||||
ref="settingsBtn"
|
|
||||||
:active="showViewSettings"
|
|
||||||
tooltip="Toggle visualisation settings visibility"
|
|
||||||
tooltipPosition="top-left"
|
|
||||||
@click="showViewSettings = !showViewSettings"
|
|
||||||
>
|
|
||||||
<settings-icon />
|
|
||||||
</icon-button>
|
|
||||||
|
|
||||||
<div class="side-tool-bar-divider" />
|
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!exportToPngEnabled || loadingImage"
|
:disabled="!exportToPngEnabled || loadingImage"
|
||||||
:loading="loadingImage"
|
:loading="loadingImage"
|
||||||
@@ -95,10 +82,10 @@
|
|||||||
</side-tool-bar>
|
</side-tool-bar>
|
||||||
|
|
||||||
<loading-dialog
|
<loading-dialog
|
||||||
v-model="showLoadingDialog"
|
|
||||||
loadingMsg="Rendering the visualisation..."
|
loadingMsg="Rendering the visualisation..."
|
||||||
successMsg="Image is ready"
|
successMsg="Image is ready"
|
||||||
actionBtnName="Copy"
|
actionBtnName="Copy"
|
||||||
|
name="prepareCopy"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
:loading="preparingCopy"
|
:loading="preparingCopy"
|
||||||
@action="copyToClipboard"
|
@action="copyToClipboard"
|
||||||
@@ -108,21 +95,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Chart from './Chart/index.vue'
|
import Chart from './Chart'
|
||||||
import Pivot from './Pivot/index.vue'
|
import Pivot from './Pivot'
|
||||||
import Graph from './Graph/index.vue'
|
import Graph from './Graph'
|
||||||
import SideToolBar from '../SideToolBar'
|
import SideToolBar from '../SideToolBar'
|
||||||
import IconButton from '@/components/IconButton'
|
import IconButton from '@/components/IconButton'
|
||||||
import ChartIcon from '@/components/svg/chart'
|
import ChartIcon from '@/components/svg/chart'
|
||||||
import PivotIcon from '@/components/svg/pivot'
|
import PivotIcon from '@/components/svg/pivot'
|
||||||
import GraphIcon from '@/components/svg/graph.vue'
|
import GraphIcon from '@/components/svg/graph.vue'
|
||||||
import SettingsIcon from '@/components/svg/settings.vue'
|
|
||||||
import HtmlIcon from '@/components/svg/html'
|
import HtmlIcon from '@/components/svg/html'
|
||||||
import ExportToSvgIcon from '@/components/svg/exportToSvg'
|
import ExportToSvgIcon from '@/components/svg/exportToSvg'
|
||||||
import PngIcon from '@/components/svg/png'
|
import PngIcon from '@/components/svg/png'
|
||||||
import ClipboardIcon from '@/components/svg/clipboard'
|
import ClipboardIcon from '@/components/svg/clipboard'
|
||||||
import cIo from '@/lib/utils/clipboardIo'
|
import cIo from '@/lib/utils/clipboardIo'
|
||||||
import loadingDialog from '@/components/LoadingDialog.vue'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
@@ -137,7 +123,6 @@ export default {
|
|||||||
ChartIcon,
|
ChartIcon,
|
||||||
PivotIcon,
|
PivotIcon,
|
||||||
GraphIcon,
|
GraphIcon,
|
||||||
SettingsIcon,
|
|
||||||
ExportToSvgIcon,
|
ExportToSvgIcon,
|
||||||
PngIcon,
|
PngIcon,
|
||||||
HtmlIcon,
|
HtmlIcon,
|
||||||
@@ -164,9 +149,7 @@ export default {
|
|||||||
chart: this.initMode === 'chart' ? this.initOptions : null,
|
chart: this.initMode === 'chart' ? this.initOptions : null,
|
||||||
pivot: this.initMode === 'pivot' ? this.initOptions : null,
|
pivot: this.initMode === 'pivot' ? this.initOptions : null,
|
||||||
graph: this.initMode === 'graph' ? this.initOptions : null
|
graph: this.initMode === 'graph' ? this.initOptions : null
|
||||||
},
|
}
|
||||||
showLoadingDialog: false,
|
|
||||||
showViewSettings: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -208,13 +191,14 @@ export default {
|
|||||||
async prepareCopy() {
|
async prepareCopy() {
|
||||||
if ('ClipboardItem' in window) {
|
if ('ClipboardItem' in window) {
|
||||||
this.preparingCopy = true
|
this.preparingCopy = true
|
||||||
this.showLoadingDialog = true
|
this.$modal.show('prepareCopy')
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
|
|
||||||
await time.sleep(0)
|
await time.sleep(0)
|
||||||
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
|
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
|
||||||
const t1 = performance.now()
|
const t1 = performance.now()
|
||||||
if (t1 - t0 < 950) {
|
if (t1 - t0 < 950) {
|
||||||
|
this.$modal.hide('prepareCopy')
|
||||||
this.copyToClipboard()
|
this.copyToClipboard()
|
||||||
} else {
|
} else {
|
||||||
this.preparingCopy = false
|
this.preparingCopy = false
|
||||||
@@ -227,13 +211,14 @@ export default {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
copyToClipboard() {
|
async copyToClipboard() {
|
||||||
cIo.copyImage(this.dataToCopy)
|
cIo.copyImage(this.dataToCopy)
|
||||||
this.showLoadingDialog = false
|
this.$modal.hide('prepareCopy')
|
||||||
this.exportSignal('clipboard')
|
this.exportSignal('clipboard')
|
||||||
},
|
},
|
||||||
cancelCopy() {
|
cancelCopy() {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
|
this.$modal.hide('prepareCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
saveAsSvg() {
|
saveAsSvg() {
|
||||||
@@ -254,9 +239,7 @@ export default {
|
|||||||
events.send(
|
events.send(
|
||||||
this.mode === 'chart' || this.plotlyInPivot
|
this.mode === 'chart' || this.plotlyInPivot
|
||||||
? 'viz_plotly.export'
|
? 'viz_plotly.export'
|
||||||
: this.mode === 'graph'
|
: 'viz_pivot.export',
|
||||||
? 'viz_graph.export'
|
|
||||||
: 'viz_pivot.export',
|
|
||||||
null,
|
null,
|
||||||
eventLabels
|
eventLabels
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,10 +80,10 @@
|
|||||||
</side-tool-bar>
|
</side-tool-bar>
|
||||||
|
|
||||||
<loading-dialog
|
<loading-dialog
|
||||||
v-model="showLoadingDialog"
|
|
||||||
loadingMsg="Building CSV..."
|
loadingMsg="Building CSV..."
|
||||||
successMsg="CSV is ready"
|
successMsg="CSV is ready"
|
||||||
actionBtnName="Copy"
|
actionBtnName="Copy"
|
||||||
|
name="prepareCSVCopy"
|
||||||
title="Copy to clipboard"
|
title="Copy to clipboard"
|
||||||
:loading="preparingCopy"
|
:loading="preparingCopy"
|
||||||
@action="copyToClipboard"
|
@action="copyToClipboard"
|
||||||
@@ -190,8 +190,7 @@ export default {
|
|||||||
viewRecord: false,
|
viewRecord: false,
|
||||||
defaultPage: 1,
|
defaultPage: 1,
|
||||||
defaultSelectedCell: null,
|
defaultSelectedCell: null,
|
||||||
enableTeleport: this.$store.state.isWorkspaceVisible,
|
enableTeleport: this.$store.state.isWorkspaceVisible
|
||||||
showLoadingDialog: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -265,13 +264,14 @@ export default {
|
|||||||
|
|
||||||
if ('ClipboardItem' in window) {
|
if ('ClipboardItem' in window) {
|
||||||
this.preparingCopy = true
|
this.preparingCopy = true
|
||||||
this.showLoadingDialog = true
|
this.$modal.show('prepareCSVCopy')
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
|
|
||||||
await time.sleep(0)
|
await time.sleep(0)
|
||||||
this.dataToCopy = csv.serialize(this.result)
|
this.dataToCopy = csv.serialize(this.result)
|
||||||
const t1 = performance.now()
|
const t1 = performance.now()
|
||||||
if (t1 - t0 < 950) {
|
if (t1 - t0 < 950) {
|
||||||
|
this.$modal.hide('prepareCSVCopy')
|
||||||
this.copyToClipboard()
|
this.copyToClipboard()
|
||||||
} else {
|
} else {
|
||||||
this.preparingCopy = false
|
this.preparingCopy = false
|
||||||
@@ -287,11 +287,12 @@ export default {
|
|||||||
|
|
||||||
copyToClipboard() {
|
copyToClipboard() {
|
||||||
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||||
this.showLoadingDialog = false
|
this.$modal.hide('prepareCSVCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelCopy() {
|
cancelCopy() {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
|
this.$modal.hide('prepareCSVCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleViewValuePanel() {
|
toggleViewValuePanel() {
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ export default {
|
|||||||
border-left: 1px solid var(--color-border-light);
|
border-left: 1px solid var(--color-border-light);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.side-tool-bar-divider {
|
.side-tool-bar-divider {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export default {
|
|||||||
) {
|
) {
|
||||||
const stmt = [
|
const stmt = [
|
||||||
'/*',
|
'/*',
|
||||||
' * Your database is empty. In order to start building data visualisations',
|
' * Your database is empty. In order to start building charts',
|
||||||
' * you should create tables and insert data into them.',
|
' * you should create a table and insert data into it.',
|
||||||
' */',
|
' */',
|
||||||
'CREATE TABLE house',
|
'CREATE TABLE house',
|
||||||
'(',
|
'(',
|
||||||
@@ -54,20 +54,7 @@ export default {
|
|||||||
"('Gryffindor', 100),",
|
"('Gryffindor', 100),",
|
||||||
"('Hufflepuff', 90),",
|
"('Hufflepuff', 90),",
|
||||||
"('Ravenclaw', 95),",
|
"('Ravenclaw', 95),",
|
||||||
"('Slytherin', 80);",
|
"('Slytherin', 80);"
|
||||||
'',
|
|
||||||
'CREATE TABLE student',
|
|
||||||
'(',
|
|
||||||
' id INTEGER,',
|
|
||||||
' name TEXT,',
|
|
||||||
' house TEXT',
|
|
||||||
');',
|
|
||||||
'INSERT INTO student VALUES',
|
|
||||||
"(1, 'Harry Potter', 'Gryffindor'),",
|
|
||||||
"(2, 'Ron Weasley', 'Gryffindor'),",
|
|
||||||
"(3, 'Draco Malfoy', 'Slytherin'),",
|
|
||||||
"(4, 'Luna Lovegood', 'Ravenclaw'),",
|
|
||||||
"(5, 'Cedric Diggory', 'Hufflepuff');"
|
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { shallowMount, mount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import { createStore } from 'vuex'
|
import { createStore } from 'vuex'
|
||||||
import App from '@/App.vue'
|
import App from '@/App'
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
import actions from '@/store/actions'
|
|
||||||
import mutations from '@/store/mutations'
|
import mutations from '@/store/mutations'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
|
||||||
import { routes } from '@/router'
|
|
||||||
|
|
||||||
describe('App.vue', () => {
|
describe('App.vue', () => {
|
||||||
let clock
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
clock = sinon.useFakeTimers()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
@@ -68,167 +59,4 @@ describe('App.vue', () => {
|
|||||||
{ id: 3, name: 'bar' }
|
{ id: 3, name: 'bar' }
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Updates store when inquirires change in local storage', async () => {
|
|
||||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 2, name: 'baz' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
predefinedInquiries: [],
|
|
||||||
inquiries: []
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations })
|
|
||||||
shallowMount(App, {
|
|
||||||
global: { stubs: ['router-view'], plugins: [store] }
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(state.inquiries).to.eql([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 2, name: 'baz' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
|
|
||||||
storedInquiries.getStoredInquiries.returns([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
window.dispatchEvent(new StorageEvent('storage', { key: 'myInquiries' }))
|
|
||||||
expect(state.inquiries).to.eql([
|
|
||||||
{ id: 1, name: 'foo' },
|
|
||||||
{ id: 3, name: 'bar' }
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Closes with saving and does not change the next tab', async () => {
|
|
||||||
const inquiries = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
createdAt: '2020-11-07T20:57:04.492Z'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
createdAt: '2020-11-07T20:57:04.492Z'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns(inquiries)
|
|
||||||
const tab1 = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'bottom',
|
|
||||||
dataView: 'hidden'
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
columns: ['name', 'points'],
|
|
||||||
values: {
|
|
||||||
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
|
|
||||||
points: [100, 90, 95, 80]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const tab2 = {
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'hidden',
|
|
||||||
dataView: 'bottom'
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
columns: ['id'],
|
|
||||||
values: {
|
|
||||||
id: [1, 2, 3]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
tabs: [tab1, tab2],
|
|
||||||
currentTabId: 1,
|
|
||||||
currentTab: tab1,
|
|
||||||
db: {},
|
|
||||||
inquiries
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(),
|
|
||||||
routes: routes
|
|
||||||
})
|
|
||||||
router.push('/workspace')
|
|
||||||
|
|
||||||
// After this line, router is ready
|
|
||||||
await router.isReady()
|
|
||||||
|
|
||||||
const wrapper = mount(App, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false,
|
|
||||||
schema: true,
|
|
||||||
AppDiagnosticInfo: true,
|
|
||||||
DataView: {
|
|
||||||
template: '<div></div>',
|
|
||||||
methods: { getOptionsForSave: sinon.stub() }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [store, router]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// click on the close icon of the first tab
|
|
||||||
const firstTabCloseIcon = wrapper.findAll('.tab')[0].find('.close-icon')
|
|
||||||
await firstTabCloseIcon.trigger('click')
|
|
||||||
|
|
||||||
// find 'Save and close' in the dialog
|
|
||||||
const closeBtn = wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save and close')
|
|
||||||
|
|
||||||
// click 'Save and close' in the dialog
|
|
||||||
await closeBtn.trigger('click')
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// check that tab is closed
|
|
||||||
expect(wrapper.findAllComponents({ name: 'Tab' })).to.have.lengthOf(1)
|
|
||||||
// check that the open tab didn't change
|
|
||||||
const firstTab = wrapper.findComponent({ name: 'Tab' })
|
|
||||||
expect(firstTab.props('tab').name).to.equal('bar')
|
|
||||||
expect(firstTab.props('tab').result).to.eql({
|
|
||||||
columns: ['id'],
|
|
||||||
values: {
|
|
||||||
id: [1, 2, 3]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(firstTab.props('tab')).to.eql(tab2)
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
await nextTick()
|
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
|
||||||
wrapper.unmount()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -413,7 +413,7 @@ describe('SQLite extensions', function () {
|
|||||||
WHERE ip.id <= p.id
|
WHERE ip.id <= p.id
|
||||||
) AS path
|
) AS path
|
||||||
FROM tmp, json_each(filename_array) AS p
|
FROM tmp, json_each(filename_array) AS p
|
||||||
WHERE p.key > 0 -- because the filenames start with the separator
|
WHERE p.id > 1 -- because the filenames start with the separator
|
||||||
`)
|
`)
|
||||||
expect(actual.values).to.eql({
|
expect(actual.values).to.eql({
|
||||||
path: [
|
path: [
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
createdAt: new Date(2021, 0, 1).toJSON(),
|
createdAt: new Date(2021, 0, 1),
|
||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +83,7 @@ describe('storedInquiries.js', () => {
|
|||||||
expect(copy).to.have.property('query').which.equal(base.query)
|
expect(copy).to.have.property('query').which.equal(base.query)
|
||||||
expect(copy).to.have.property('viewType').which.equal(base.viewType)
|
expect(copy).to.have.property('viewType').which.equal(base.viewType)
|
||||||
expect(copy).to.have.property('viewOptions').which.eql(base.viewOptions)
|
expect(copy).to.have.property('viewOptions').which.eql(base.viewOptions)
|
||||||
expect(copy).to.have.property('createdAt')
|
expect(copy).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||||
expect(new Date(copy.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(copy).to.not.have.property('isPredefined')
|
expect(copy).to.not.have.property('isPredefined')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ describe('tab.js', () => {
|
|||||||
query: undefined,
|
query: undefined,
|
||||||
viewOptions: undefined,
|
viewOptions: undefined,
|
||||||
isPredefined: undefined,
|
isPredefined: undefined,
|
||||||
updatedAt: undefined,
|
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
result: null,
|
result: null,
|
||||||
isGettingResults: false,
|
isGettingResults: false,
|
||||||
@@ -43,8 +42,7 @@ describe('tab.js', () => {
|
|||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
viewOptions: 'this is view options object',
|
viewOptions: 'this is view options object',
|
||||||
name: 'Foo inquiry',
|
name: 'Foo inquiry',
|
||||||
createdAt: '2022-12-05T18:30:30',
|
createdAt: '2022-12-05T18:30:30'
|
||||||
updatedAt: '2022-12-06T18:30:30'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = new Tab(state, inquiry)
|
const newTab = new Tab(state, inquiry)
|
||||||
@@ -55,7 +53,6 @@ describe('tab.js', () => {
|
|||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewOptions: 'this is view options object',
|
viewOptions: 'this is view options object',
|
||||||
isPredefined: undefined,
|
isPredefined: undefined,
|
||||||
updatedAt: '2022-12-06T18:30:30',
|
|
||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
result: null,
|
result: null,
|
||||||
isGettingResults: false,
|
isGettingResults: false,
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ describe('actions', () => {
|
|||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: undefined,
|
viewOptions: undefined,
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: undefined
|
|
||||||
})
|
})
|
||||||
expect(state.untitledLastIndex).to.equal(1)
|
expect(state.untitledLastIndex).to.equal(1)
|
||||||
|
|
||||||
@@ -31,8 +30,7 @@ describe('actions', () => {
|
|||||||
tempName: 'Untitled 1',
|
tempName: 'Untitled 1',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: undefined,
|
viewOptions: undefined,
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: undefined
|
|
||||||
})
|
})
|
||||||
expect(state.untitledLastIndex).to.equal(2)
|
expect(state.untitledLastIndex).to.equal(2)
|
||||||
})
|
})
|
||||||
@@ -42,16 +40,16 @@ describe('actions', () => {
|
|||||||
tabs: [],
|
tabs: [],
|
||||||
untitledLastIndex: 0
|
untitledLastIndex: 0
|
||||||
}
|
}
|
||||||
const inquiry = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'test',
|
name: 'test',
|
||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: 'an object with view options',
|
viewOptions: 'an object with view options',
|
||||||
updatedAt: '2025-05-16T20:15:00Z'
|
isSaved: true
|
||||||
}
|
}
|
||||||
await addTab({ state }, inquiry)
|
await addTab({ state }, tab)
|
||||||
expect(state.tabs[0]).to.include(inquiry)
|
expect(state.tabs[0]).to.include(tab)
|
||||||
expect(state.untitledLastIndex).to.equal(0)
|
expect(state.untitledLastIndex).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -168,26 +166,21 @@ describe('actions', () => {
|
|||||||
newName: 'foo'
|
newName: 'foo'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
expect(value.id).not.to.equal(tab.id)
|
expect(value.id).to.equal(tab.id)
|
||||||
expect(value.name).to.equal('foo')
|
expect(value.name).to.equal('foo')
|
||||||
expect(value.query).to.equal(tab.query)
|
expect(value.query).to.equal(tab.query)
|
||||||
expect(value.viewOptions).to.eql(['chart'])
|
expect(value.viewOptions).to.eql(['chart'])
|
||||||
expect(value).to.have.property('createdAt')
|
expect(value).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
|
||||||
expect(state.inquiries).to.eql([value])
|
expect(state.inquiries).to.eql([value])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveInquiry updates existing inquiry in the storage', async () => {
|
it('save updates existing inquiry in the storage', async () => {
|
||||||
const now = new Date()
|
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
name: 'foo',
|
name: null,
|
||||||
updatedAt: '2025-05-16T20:15:00Z',
|
|
||||||
dataView: {
|
dataView: {
|
||||||
getOptionsForSave() {
|
getOptionsForSave() {
|
||||||
return ['chart']
|
return ['chart']
|
||||||
@@ -196,34 +189,34 @@ describe('actions', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
inquiries: [
|
inquiries: [],
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
createdAt: '2025-05-15T16:30:00Z',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
tab.query = 'select * from bar'
|
const first = await saveInquiry(
|
||||||
await saveInquiry({ state }, { inquiryTab: tab, newName: '' })
|
{ state },
|
||||||
|
{
|
||||||
|
inquiryTab: tab,
|
||||||
|
newName: 'foo'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tab.name = 'foo'
|
||||||
|
tab.query = 'select * from foo'
|
||||||
|
await saveInquiry({ state }, { inquiryTab: tab })
|
||||||
const inquiries = state.inquiries
|
const inquiries = state.inquiries
|
||||||
const updatedTab = inquiries[0]
|
const second = inquiries[0]
|
||||||
expect(inquiries).has.lengthOf(1)
|
expect(inquiries).has.lengthOf(1)
|
||||||
expect(updatedTab.id).to.equal(updatedTab.id)
|
expect(second.id).to.equal(first.id)
|
||||||
expect(updatedTab.name).to.equal(updatedTab.name)
|
expect(second.name).to.equal(first.name)
|
||||||
expect(updatedTab.query).to.equal(tab.query)
|
expect(second.query).to.equal(tab.query)
|
||||||
expect(updatedTab.viewOptions).to.eql(['chart'])
|
expect(second.viewOptions).to.eql(['chart'])
|
||||||
expect(updatedTab.createdAt).to.equal('2025-05-15T16:30:00Z')
|
expect(new Date(second.createdAt).getTime()).to.equal(
|
||||||
expect(new Date(updatedTab.updatedAt)).to.be.within(now, nowPlusMinute)
|
first.createdAt.getTime()
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("saveInquiry adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
it("save adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||||
const tab = {
|
const tab = {
|
||||||
@@ -259,95 +252,6 @@ describe('actions', () => {
|
|||||||
expect(inquiries[0].name).to.equal('foo')
|
expect(inquiries[0].name).to.equal('foo')
|
||||||
expect(inquiries[0].query).to.equal(tab.query)
|
expect(inquiries[0].query).to.equal(tab.query)
|
||||||
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
||||||
expect(new Date(inquiries[0].updatedAt)).to.be.within(now, nowPlusMinute)
|
|
||||||
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveInquiry adds new inquiry if newName is provided', async () => {
|
|
||||||
const now = new Date()
|
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
|
||||||
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z',
|
|
||||||
dataView: {
|
|
||||||
getOptionsForSave() {
|
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const inquiry = {
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
createdAt: '2025-05-15T16:30:00Z',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z'
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
inquiries: [inquiry],
|
|
||||||
tabs: [tab]
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = await saveInquiry(
|
|
||||||
{ state },
|
|
||||||
{
|
|
||||||
inquiryTab: tab,
|
|
||||||
newName: 'foo_new'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(value.id).not.to.equal(tab.id)
|
|
||||||
expect(value.name).to.equal('foo_new')
|
|
||||||
expect(value.query).to.equal(tab.query)
|
|
||||||
expect(value.viewOptions).to.eql(['chart'])
|
|
||||||
expect(value).to.have.property('createdAt')
|
|
||||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
|
||||||
expect(state.inquiries).to.eql([inquiry, value])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('saveInquiry adds new inquiry if the inquiry is not in the storeage anymore', async () => {
|
|
||||||
const now = new Date()
|
|
||||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
|
||||||
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
name: 'foo',
|
|
||||||
updatedAt: '2025-05-16T20:15:00Z',
|
|
||||||
dataView: {
|
|
||||||
getOptionsForSave() {
|
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
inquiries: [],
|
|
||||||
tabs: [tab]
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = await saveInquiry(
|
|
||||||
{ state },
|
|
||||||
{
|
|
||||||
inquiryTab: tab,
|
|
||||||
newName: ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
expect(value.id).to.equal(tab.id)
|
|
||||||
expect(value.name).to.equal('foo')
|
|
||||||
expect(value.query).to.equal(tab.query)
|
|
||||||
expect(value.viewOptions).to.eql(['chart'])
|
|
||||||
expect(value).to.have.property('createdAt')
|
|
||||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
|
||||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
|
||||||
expect(state.inquiries).to.eql([value])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ describe('mutations', () => {
|
|||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: { here_are: 'chart settings' },
|
viewOptions: { here_are: 'chart settings' },
|
||||||
isSaved: false,
|
isSaved: false,
|
||||||
isPredefined: false,
|
isPredefined: false
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newValues = {
|
const newValues = {
|
||||||
@@ -44,7 +43,6 @@ describe('mutations', () => {
|
|||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
viewOptions: { here_are: 'pivot settings' },
|
viewOptions: { here_are: 'pivot settings' },
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
isSaved: true
|
isSaved: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +58,6 @@ describe('mutations', () => {
|
|||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
viewType: 'pivot',
|
viewType: 'pivot',
|
||||||
viewOptions: { here_are: 'pivot settings' },
|
viewOptions: { here_are: 'pivot settings' },
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
isSaved: true
|
isSaved: true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import MainMenu from '@/views/MainView/MainMenu'
|
|||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import eventBus from '@/lib/eventBus'
|
import eventBus from '@/lib/eventBus'
|
||||||
import actions from '@/store/actions'
|
|
||||||
import mutations from '@/store/mutations'
|
|
||||||
|
|
||||||
let wrapper = null
|
let wrapper = null
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ describe('MainMenu.vue', () => {
|
|||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Create, Save and Save as are visible only on /workspace page', async () => {
|
it('Create and Save are visible only on /workspace page', async () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: { query: '', execute: sinon.stub() },
|
currentTab: { query: '', execute: sinon.stub() },
|
||||||
tabs: [{}],
|
tabs: [{}],
|
||||||
@@ -47,8 +45,6 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#save-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#save-btn').isVisible()).to.equal(true)
|
||||||
expect(wrapper.find('#save-as-btn').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('#save-as-btn').isVisible()).to.equal(true)
|
|
||||||
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
@@ -69,7 +65,7 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Save and Save as are not visible if there is no tabs', () => {
|
it('Save is not visible if there is no tabs', () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: null,
|
currentTab: null,
|
||||||
tabs: [],
|
tabs: [],
|
||||||
@@ -87,8 +83,6 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#save-btn').isVisible()).to.equal(false)
|
expect(wrapper.find('#save-btn').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#save-as-btn').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('#save-as-btn').isVisible()).to.equal(false)
|
|
||||||
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
@@ -117,12 +111,10 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
const vm = wrapper.vm
|
const vm = wrapper.vm
|
||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#save-as-btn').element.disabled).to.equal(false)
|
|
||||||
|
|
||||||
store.state.tabs[0].isSaved = true
|
store.state.tabs[0].isSaved = true
|
||||||
await vm.$nextTick()
|
await vm.$nextTick()
|
||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#save-as-btn').element.disabled).to.equal(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a tab', async () => {
|
it('Creates a tab', async () => {
|
||||||
@@ -340,7 +332,7 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.vm.createNewInquiry.callCount).to.equal(4)
|
expect(wrapper.vm.createNewInquiry.callCount).to.equal(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Ctrl S calls onSave if the tab is unsaved and route path is /workspace', async () => {
|
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace', async () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
@@ -361,115 +353,42 @@ describe('MainMenu.vue', () => {
|
|||||||
plugins: [store]
|
plugins: [store]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
sinon.stub(wrapper.vm, 'onSave')
|
sinon.stub(wrapper.vm, 'checkInquiryBeforeSave')
|
||||||
|
|
||||||
const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
|
const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
|
||||||
const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true })
|
const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true })
|
||||||
// tab is unsaved and route is /workspace
|
// tab is unsaved and route is /workspace
|
||||||
document.dispatchEvent(ctrlS)
|
document.dispatchEvent(ctrlS)
|
||||||
expect(wrapper.vm.onSave.calledOnce).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledOnce).to.equal(true)
|
||||||
document.dispatchEvent(metaS)
|
document.dispatchEvent(metaS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
|
|
||||||
// tab is saved and route is /workspace
|
// tab is saved and route is /workspace
|
||||||
store.state.tabs[0].isSaved = true
|
store.state.tabs[0].isSaved = true
|
||||||
document.dispatchEvent(ctrlS)
|
document.dispatchEvent(ctrlS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
document.dispatchEvent(metaS)
|
document.dispatchEvent(metaS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
|
|
||||||
// tab is unsaved and route is not /workspace
|
// tab is unsaved and route is not /workspace
|
||||||
wrapper.vm.$route.path = '/inquiries'
|
wrapper.vm.$route.path = '/inquiries'
|
||||||
store.state.tabs[0].isSaved = false
|
store.state.tabs[0].isSaved = false
|
||||||
document.dispatchEvent(ctrlS)
|
document.dispatchEvent(ctrlS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
document.dispatchEvent(metaS)
|
document.dispatchEvent(metaS)
|
||||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Ctrl Shift S calls onSaveAs if route path is /workspace', async () => {
|
it('Saves the inquiry when no need the new name', async () => {
|
||||||
const tab = {
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const store = createStore({ state })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
|
|
||||||
wrapper = shallowMount(MainMenu, {
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: ['router-link'],
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
sinon.stub(wrapper.vm, 'onSaveAs')
|
|
||||||
|
|
||||||
const ctrlS = new KeyboardEvent('keydown', {
|
|
||||||
key: 'S',
|
|
||||||
ctrlKey: true,
|
|
||||||
shiftKey: true
|
|
||||||
})
|
|
||||||
const metaS = new KeyboardEvent('keydown', {
|
|
||||||
key: 'S',
|
|
||||||
metaKey: true,
|
|
||||||
shiftKey: true
|
|
||||||
})
|
|
||||||
// tab is unsaved and route is /workspace
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.calledOnce).to.equal(true)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.calledTwice).to.equal(true)
|
|
||||||
|
|
||||||
// tab is saved and route is /workspace
|
|
||||||
store.state.tabs[0].isSaved = true
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.calledThrice).to.equal(true)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
|
|
||||||
// tab is unsaved and route is not /workspace
|
|
||||||
wrapper.vm.$route.path = '/inquiries'
|
|
||||||
store.state.tabs[0].isSaved = false
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
|
|
||||||
// tab is saved and route is not /workspace
|
|
||||||
wrapper.vm.$route.path = '/inquiries'
|
|
||||||
store.state.tabs[0].isSaved = true
|
|
||||||
document.dispatchEvent(ctrlS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
document.dispatchEvent(metaS)
|
|
||||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Saves the inquiry when no need the new name and no update conflict', async () => {
|
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: tab,
|
currentTab: tab,
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
@@ -482,8 +401,7 @@ describe('MainMenu.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-16T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -528,8 +446,7 @@ describe('MainMenu.vue', () => {
|
|||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: '2025-05-16T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -539,396 +456,6 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Inquiry conflict: overwrite', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub().returns({
|
|
||||||
name: 'foo',
|
|
||||||
id: 1,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
// find Overwrite in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Overwrite')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was saved via saveInquiry (newName='')
|
|
||||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
|
||||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
|
||||||
inquiryTab: state.currentTab,
|
|
||||||
newName: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// check that the tab was updated
|
|
||||||
expect(
|
|
||||||
mutations.updateTab.calledOnceWith(
|
|
||||||
state,
|
|
||||||
sinon.match({
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: 'foo',
|
|
||||||
id: 1,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
isSaved: true,
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inquiry conflict after saving new inquiry: overwrite', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: null,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: undefined,
|
|
||||||
execute: sinon.stub(),
|
|
||||||
dataView: { getOptionsForSave: sinon.stub() },
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that Save dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Save inquiry'
|
|
||||||
)
|
|
||||||
|
|
||||||
// enter the name
|
|
||||||
await wrapper.find('.dialog-body input').setValue('foo')
|
|
||||||
|
|
||||||
// find Save in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that now there is one inquiry saved
|
|
||||||
expect(state.inquiries.length).to.equal(1)
|
|
||||||
expect(state.inquiries[0].name).to.equal('foo')
|
|
||||||
expect(state.tabs[0].name).to.equal('foo')
|
|
||||||
|
|
||||||
// change the inquiry in store (like it's updated in another tab)
|
|
||||||
store.state.inquiries[0].query = 'SELECT * FROM foo_updated_in_another_tab'
|
|
||||||
store.state.inquiries[0].updatedAt = '2025-05-15T00:00:10Z'
|
|
||||||
store.state.currentTab.query = 'SELECT * FROM foo_new'
|
|
||||||
store.state.currentTab.isSaved = false
|
|
||||||
await nextTick()
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
// find Overwrite in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Overwrite')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that it's still one inquiry saved
|
|
||||||
expect(state.inquiries.length).to.equal(1)
|
|
||||||
expect(state.inquiries[0].name).to.equal('foo')
|
|
||||||
expect(state.tabs[0].name).to.equal('foo')
|
|
||||||
expect(state.inquiries[0].query).to.equal('SELECT * FROM foo_new')
|
|
||||||
expect(state.tabs[0].query).to.equal('SELECT * FROM foo_new')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inquiry conflict: save as new', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab]
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub().returns({
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z',
|
|
||||||
createdAt: '2025-05-16T17:30:00Z'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
await clock.tick(100)
|
|
||||||
|
|
||||||
// find "Save as new" in the dialog and click
|
|
||||||
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save as new')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// Hiding any dialog is done with tiny animation. Give time to finish it:
|
|
||||||
await clock.tick(100)
|
|
||||||
// Note: don't call nextTick before clock.tick. That leads to extra trap in
|
|
||||||
// trapStack and the test fails with focus-trap error in afterEach hook
|
|
||||||
// when unmount the component
|
|
||||||
|
|
||||||
// check that only one dialog open
|
|
||||||
expect(wrapper.findAll('.dialog.vfm').length).to.equal(1)
|
|
||||||
// enter the new name
|
|
||||||
await wrapper.find('.dialog-body input').setValue('foo_new')
|
|
||||||
|
|
||||||
// find Save in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was saved via saveInquiry (newName='foo_new')
|
|
||||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
|
||||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
|
||||||
inquiryTab: state.currentTab,
|
|
||||||
newName: 'foo_new'
|
|
||||||
})
|
|
||||||
|
|
||||||
// check that the tab was updated
|
|
||||||
expect(
|
|
||||||
mutations.updateTab.calledOnceWith(
|
|
||||||
state,
|
|
||||||
sinon.match({
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
isSaved: true,
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Inquiry conflict: cancel', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub()
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that the conflict dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Inquiry saving conflict'
|
|
||||||
)
|
|
||||||
|
|
||||||
// find Cancel in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Cancel')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was not saved via storedInquiries.save
|
|
||||||
expect(actions.saveInquiry.called).to.equal(false)
|
|
||||||
|
|
||||||
// check that the tab was not updated
|
|
||||||
expect(mutations.updateTab.called).to.equal(false)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event is not listened on eventBus
|
|
||||||
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Shows en error when the new name is needed but not specifyied', async () => {
|
it('Shows en error when the new name is needed but not specifyied', async () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -936,8 +463,7 @@ describe('MainMenu.vue', () => {
|
|||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: tab,
|
currentTab: tab,
|
||||||
@@ -953,8 +479,7 @@ describe('MainMenu.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-16T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -997,15 +522,14 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Saves the new inquiry with a new name', async () => {
|
it('Saves the inquiry with a new name', async () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
execute: sinon.stub(),
|
execute: sinon.stub(),
|
||||||
isSaved: false,
|
isSaved: false
|
||||||
updatedAt: undefined
|
|
||||||
}
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: tab,
|
currentTab: tab,
|
||||||
@@ -1018,11 +542,10 @@ describe('MainMenu.vue', () => {
|
|||||||
const actions = {
|
const actions = {
|
||||||
saveInquiry: sinon.stub().returns({
|
saveInquiry: sinon.stub().returns({
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 2,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -1060,6 +583,8 @@ describe('MainMenu.vue', () => {
|
|||||||
.find(button => button.text() === 'Save')
|
.find(button => button.text() === 'Save')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
// check that the dialog is closed
|
// check that the dialog is closed
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
@@ -1079,12 +604,11 @@ describe('MainMenu.vue', () => {
|
|||||||
tab,
|
tab,
|
||||||
newValues: {
|
newValues: {
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
id: 2,
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1126,8 +650,7 @@ describe('MainMenu.vue', () => {
|
|||||||
id: 2,
|
id: 2,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: []
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -1168,6 +691,8 @@ describe('MainMenu.vue', () => {
|
|||||||
.find(button => button.text() === 'Save')
|
.find(button => button.text() === 'Save')
|
||||||
.trigger('click')
|
.trigger('click')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
// check that the dialog is closed
|
// check that the dialog is closed
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
@@ -1191,8 +716,7 @@ describe('MainMenu.vue', () => {
|
|||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
isSaved: true,
|
isSaved: true
|
||||||
updatedAt: '2025-05-15T15:30:00Z'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1200,6 +724,19 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
// check that 'inquirySaved' event was triggered on eventBus
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||||
|
|
||||||
|
// We saved predefined inquiry, so the tab will be created again
|
||||||
|
// (because of new id) and it will be without sql result and has default view - table.
|
||||||
|
// That's why we need to restore data and view.
|
||||||
|
// Check that result and view are preserved in the currentTab:
|
||||||
|
expect(state.currentTab.viewType).to.equal('chart')
|
||||||
|
expect(state.currentTab.result).to.eql({
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: [
|
||||||
|
[1, 'Harry Potter'],
|
||||||
|
[2, 'Drako Malfoy']
|
||||||
|
]
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Cancel saving', async () => {
|
it('Cancel saving', async () => {
|
||||||
@@ -1224,7 +761,7 @@ describe('MainMenu.vue', () => {
|
|||||||
name: 'bar',
|
name: 'bar',
|
||||||
id: 2,
|
id: 2,
|
||||||
query: 'SELECT * FROM foo',
|
query: 'SELECT * FROM foo',
|
||||||
viewType: 'chart'
|
chart: []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const store = createStore({ state, mutations, actions })
|
const store = createStore({ state, mutations, actions })
|
||||||
@@ -1272,110 +809,4 @@ describe('MainMenu.vue', () => {
|
|||||||
// check that 'inquirySaved' event is not listened on eventBus
|
// check that 'inquirySaved' event is not listened on eventBus
|
||||||
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Save the inquiry as new', async () => {
|
|
||||||
const tab = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
updatedAt: '2025-05-15T15:30:00Z',
|
|
||||||
execute: sinon.stub(),
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
const state = {
|
|
||||||
currentTab: tab,
|
|
||||||
inquiries: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'SELECT * FROM bar',
|
|
||||||
updatedAt: '2025-05-15T16:30:00Z',
|
|
||||||
createdAt: '2025-05-14T15:30:00Z'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
tabs: [tab],
|
|
||||||
db: {}
|
|
||||||
}
|
|
||||||
const mutations = {
|
|
||||||
updateTab: sinon.stub()
|
|
||||||
}
|
|
||||||
const actions = {
|
|
||||||
saveInquiry: sinon.stub().returns({
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z',
|
|
||||||
createdAt: '2025-05-16T17:30:00Z'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const store = createStore({ state, mutations, actions })
|
|
||||||
const $route = { path: '/workspace' }
|
|
||||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
|
||||||
|
|
||||||
wrapper = mount(MainMenu, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
mocks: { $route },
|
|
||||||
stubs: {
|
|
||||||
'router-link': true,
|
|
||||||
'app-diagnostic-info': true,
|
|
||||||
teleport: true,
|
|
||||||
transition: false
|
|
||||||
},
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.find('#save-as-btn').trigger('click')
|
|
||||||
|
|
||||||
// check that Save dialog is open
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Save inquiry'
|
|
||||||
)
|
|
||||||
|
|
||||||
// enter the new name
|
|
||||||
await wrapper.find('.dialog-body input').setValue('foo_new')
|
|
||||||
|
|
||||||
// find Save in the dialog and click
|
|
||||||
await wrapper
|
|
||||||
.findAll('.dialog-buttons-container button')
|
|
||||||
.find(button => button.text() === 'Save')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// check that the dialog is closed
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// check that the inquiry was saved via saveInquiry (newName='foo_new')
|
|
||||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
|
||||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
|
||||||
inquiryTab: state.currentTab,
|
|
||||||
newName: 'foo_new'
|
|
||||||
})
|
|
||||||
|
|
||||||
// check that the tab was updated
|
|
||||||
expect(
|
|
||||||
mutations.updateTab.calledOnceWith(
|
|
||||||
state,
|
|
||||||
sinon.match({
|
|
||||||
tab,
|
|
||||||
newValues: {
|
|
||||||
name: 'foo_new',
|
|
||||||
id: 2,
|
|
||||||
query: 'SELECT * FROM foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: [],
|
|
||||||
isSaved: true,
|
|
||||||
updatedAt: '2025-05-16T17:30:00Z'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on eventBus
|
|
||||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { mount } from '@vue/test-utils'
|
|||||||
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView'
|
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView'
|
||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import cIo from '@/lib/utils/clipboardIo'
|
|
||||||
|
|
||||||
describe('DataView.vue', () => {
|
describe('DataView.vue', () => {
|
||||||
const $store = { state: { isWorkspaceVisible: true } }
|
const $store = { state: { isWorkspaceVisible: true } }
|
||||||
@@ -65,7 +64,7 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// Find chart and spy the method
|
// Find chart and spy the method
|
||||||
const chart = wrapper.findComponent({ name: 'Chart' }).vm
|
const chart = wrapper.findComponent({ name: 'Chart' }).vm
|
||||||
sinon.stub(chart, 'saveAsSvg')
|
sinon.spy(chart, 'saveAsSvg')
|
||||||
|
|
||||||
// Export to svg
|
// Export to svg
|
||||||
const svgBtn = wrapper.findComponent({ ref: 'svgExportBtn' })
|
const svgBtn = wrapper.findComponent({ ref: 'svgExportBtn' })
|
||||||
@@ -78,7 +77,7 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// Find pivot and spy the method
|
// Find pivot and spy the method
|
||||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||||
sinon.stub(pivot, 'saveAsSvg')
|
sinon.spy(pivot, 'saveAsSvg')
|
||||||
|
|
||||||
// Switch to Custom Chart renderer
|
// Switch to Custom Chart renderer
|
||||||
pivot.pivotOptions.rendererName = 'Custom chart'
|
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||||
@@ -147,7 +146,6 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
it('copy to clipboard more than 1 sec', async () => {
|
it('copy to clipboard more than 1 sec', async () => {
|
||||||
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
||||||
sinon.stub(cIo, 'copyImage')
|
|
||||||
const clock = sinon.useFakeTimers()
|
const clock = sinon.useFakeTimers()
|
||||||
const wrapper = mount(DataView, {
|
const wrapper = mount(DataView, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
@@ -167,7 +165,7 @@ describe('DataView.vue', () => {
|
|||||||
await copyBtn.trigger('click')
|
await copyBtn.trigger('click')
|
||||||
|
|
||||||
// The dialog is shown...
|
// The dialog is shown...
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||||
'Copy to clipboard'
|
'Copy to clipboard'
|
||||||
)
|
)
|
||||||
@@ -182,10 +180,11 @@ describe('DataView.vue', () => {
|
|||||||
// Wait untill prepareCopy is finished
|
// Wait untill prepareCopy is finished
|
||||||
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// The dialog is shown...
|
// The dialog is shown...
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
|
|
||||||
// ... with Ready message...
|
// ... with Ready message...
|
||||||
expect(wrapper.find('.dialog-body').text()).to.equal('Image is ready')
|
expect(wrapper.find('.dialog-body').text()).to.equal('Image is ready')
|
||||||
@@ -197,13 +196,12 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('copy to clipboard less than 1 sec', async () => {
|
it('copy to clipboard less than 1 sec', async () => {
|
||||||
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
||||||
sinon.stub(cIo, 'copyImage')
|
|
||||||
const clock = sinon.useFakeTimers()
|
const clock = sinon.useFakeTimers()
|
||||||
const wrapper = mount(DataView, {
|
const wrapper = mount(DataView, {
|
||||||
attachTo: document.body,
|
attachTo: document.body,
|
||||||
@@ -228,9 +226,10 @@ describe('DataView.vue', () => {
|
|||||||
// Wait untill prepareCopy is finished
|
// Wait untill prepareCopy is finished
|
||||||
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
// copyToClipboard is called
|
// copyToClipboard is called
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
@@ -271,7 +270,7 @@ describe('DataView.vue', () => {
|
|||||||
|
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
// copyToClipboard is not called
|
// copyToClipboard is not called
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('RunResult.vue', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// The dialog is shown...
|
// The dialog is shown...
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||||
'Copy to clipboard'
|
'Copy to clipboard'
|
||||||
)
|
)
|
||||||
@@ -91,7 +91,7 @@ describe('RunResult.vue', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
// The dialog is shown...
|
// The dialog is shown...
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||||
|
|
||||||
// ... with Ready message...
|
// ... with Ready message...
|
||||||
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
||||||
@@ -104,7 +104,7 @@ describe('RunResult.vue', () => {
|
|||||||
|
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,10 +139,11 @@ describe('RunResult.vue', () => {
|
|||||||
|
|
||||||
// Switch to microtasks (let serialize run)
|
// Switch to microtasks (let serialize run)
|
||||||
await clock.tick(0)
|
await clock.tick(0)
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
// copyToClipboard is called
|
// copyToClipboard is called
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
@@ -187,7 +188,7 @@ describe('RunResult.vue', () => {
|
|||||||
.trigger('click')
|
.trigger('click')
|
||||||
// The dialog is not shown...
|
// The dialog is not shown...
|
||||||
await clock.tick(100)
|
await clock.tick(100)
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||||
// copyToClipboard is not called
|
// copyToClipboard is not called
|
||||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import mutations from '@/store/mutations'
|
|||||||
import { createStore } from 'vuex'
|
import { createStore } from 'vuex'
|
||||||
import Tabs from '@/views/MainView/Workspace/Tabs'
|
import Tabs from '@/views/MainView/Workspace/Tabs'
|
||||||
import eventBus from '@/lib/eventBus'
|
import eventBus from '@/lib/eventBus'
|
||||||
import { nextTick } from 'vue'
|
|
||||||
import cIo from '@/lib/utils/clipboardIo'
|
|
||||||
import csv from '@/lib/csv'
|
|
||||||
|
|
||||||
describe('Tabs.vue', () => {
|
describe('Tabs.vue', () => {
|
||||||
let clock
|
let clock
|
||||||
@@ -49,7 +46,7 @@ describe('Tabs.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -57,7 +54,7 @@ describe('Tabs.vue', () => {
|
|||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: '',
|
query: '',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -100,7 +97,7 @@ describe('Tabs.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,7 +105,7 @@ describe('Tabs.vue', () => {
|
|||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: '',
|
query: '',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -439,7 +436,7 @@ describe('Tabs.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -447,7 +444,7 @@ describe('Tabs.vue', () => {
|
|||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
query: '',
|
query: '',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -480,7 +477,7 @@ describe('Tabs.vue', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
name: 'foo',
|
name: 'foo',
|
||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
chart: [],
|
||||||
isSaved: true
|
isSaved: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -504,216 +501,4 @@ describe('Tabs.vue', () => {
|
|||||||
expect(event.preventDefault.calledOnce).to.equal(false)
|
expect(event.preventDefault.calledOnce).to.equal(false)
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Copy image to clipboard dialog works in the context of the tab', async () => {
|
|
||||||
// mock store state - 2 inquiries open
|
|
||||||
const state = {
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: undefined,
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'hidden',
|
|
||||||
dataView: 'bottom'
|
|
||||||
},
|
|
||||||
isSaved: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: null,
|
|
||||||
tempName: 'Untitled',
|
|
||||||
query: '',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: undefined,
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'hidden',
|
|
||||||
dataView: 'bottom'
|
|
||||||
},
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
currentTabId: 2
|
|
||||||
}
|
|
||||||
const store = createStore({ state })
|
|
||||||
sinon.stub(cIo, 'copyImage')
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Tabs, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
stubs: { teleport: true, transition: true, RouterLink: true },
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const firstTabDataView = wrapper
|
|
||||||
.findAllComponents({ name: 'Tab' })[0]
|
|
||||||
.findComponent({ name: 'DataView' })
|
|
||||||
|
|
||||||
const secondTabDataView = wrapper
|
|
||||||
.findAllComponents({ name: 'Tab' })[1]
|
|
||||||
.findComponent({ name: 'DataView' })
|
|
||||||
|
|
||||||
// Stub prepareCopy method so it takes long and copy dialog will be shown
|
|
||||||
sinon
|
|
||||||
.stub(firstTabDataView.vm.$refs.viewComponent, 'prepareCopy')
|
|
||||||
.callsFake(async () => {
|
|
||||||
await clock.tick(5000)
|
|
||||||
return 'prepareCopy result in tab 1'
|
|
||||||
})
|
|
||||||
|
|
||||||
sinon
|
|
||||||
.stub(secondTabDataView.vm.$refs.viewComponent, 'prepareCopy')
|
|
||||||
.callsFake(async () => {
|
|
||||||
await clock.tick(5000)
|
|
||||||
return 'prepareCopy result in tab 2'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Click Copy to clipboard button in the second tab
|
|
||||||
const copyBtn = secondTabDataView.findComponent({
|
|
||||||
ref: 'copyToClipboardBtn'
|
|
||||||
})
|
|
||||||
await copyBtn.trigger('click')
|
|
||||||
|
|
||||||
// The dialog is shown...
|
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Copy to clipboard'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Switch to microtasks (let prepareCopy run)
|
|
||||||
await clock.tick(0)
|
|
||||||
// Wait untill prepareCopy is finished
|
|
||||||
await secondTabDataView.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
|
||||||
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// Click copy button in the dialog
|
|
||||||
await wrapper
|
|
||||||
.find('.dialog-buttons-container button.primary')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// The dialog is not shown...
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// copyImage is called with prepare copy result calculated in tab 2, not null
|
|
||||||
// i.e. the dialog works in the tab 2 context
|
|
||||||
expect(
|
|
||||||
cIo.copyImage.calledOnceWith('prepareCopy result in tab 2')
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
wrapper.unmount()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Copy CSV to clipboard dialog works in the context of the tab', async () => {
|
|
||||||
// mock store state - 2 inquiries open
|
|
||||||
const state = {
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
query: 'select * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: undefined,
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'bottom',
|
|
||||||
dataView: 'hidden'
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [1, 2, 3],
|
|
||||||
name: ['Gryffindor', 'Hufflepuff']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSaved: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: null,
|
|
||||||
tempName: 'Untitled',
|
|
||||||
query: '',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: undefined,
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'bottom',
|
|
||||||
dataView: 'hidden'
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
columns: ['name', 'points'],
|
|
||||||
values: {
|
|
||||||
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
|
|
||||||
points: [100, 90, 95, 80]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isSaved: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
currentTabId: 2
|
|
||||||
}
|
|
||||||
const store = createStore({ state })
|
|
||||||
sinon.stub(cIo, 'copyText')
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Tabs, {
|
|
||||||
attachTo: document.body,
|
|
||||||
global: {
|
|
||||||
stubs: { teleport: true, transition: true, RouterLink: true },
|
|
||||||
plugins: [store]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const secondTabRunResult = wrapper
|
|
||||||
.findAllComponents({ name: 'Tab' })[1]
|
|
||||||
.findComponent({ name: 'RunResult' })
|
|
||||||
|
|
||||||
// Stub prepareCopy method so it takes long and copy dialog will be shown
|
|
||||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
|
||||||
clock.tick(5000)
|
|
||||||
return 'csv serialize result'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Click Copy to clipboard button in the second tab
|
|
||||||
const copyBtn = secondTabRunResult.findComponent({
|
|
||||||
ref: 'copyToClipboardBtn'
|
|
||||||
})
|
|
||||||
await copyBtn.trigger('click')
|
|
||||||
|
|
||||||
// The dialog is shown...
|
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
|
||||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
|
||||||
'Copy to clipboard'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Switch to microtasks (let prepareCopy run)
|
|
||||||
await clock.tick(0)
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// Click copy button in the dialog
|
|
||||||
await wrapper
|
|
||||||
.find('.dialog-buttons-container button.primary')
|
|
||||||
.trigger('click')
|
|
||||||
|
|
||||||
// The dialog is not shown...
|
|
||||||
await clock.tick(100)
|
|
||||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
|
||||||
|
|
||||||
// copyText is called with 'csv serialize result' calculated in tab 2, not null
|
|
||||||
// i.e. the dialog works in the tab 2 context
|
|
||||||
expect(
|
|
||||||
cIo.copyText.calledOnceWith(
|
|
||||||
'csv serialize result',
|
|
||||||
'CSV copied to clipboard successfully'
|
|
||||||
)
|
|
||||||
).to.equal(true)
|
|
||||||
|
|
||||||
wrapper.unmount()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user