1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2026-03-22 05:56:16 +08:00

Compare commits

...

17 Commits

Author SHA1 Message Date
lana-k
c2c376219f fix lint 2026-02-28 17:59:39 +01:00
lana-k
0199415dde 0.29.0 2026-02-28 17:44:17 +01:00
lana-k
534b186d76 tests for graph editor 2026-02-28 16:53:43 +01:00
lana-k
4f6efb5bda test for DataView 2026-02-26 22:32:24 +01:00
lana-k
2c0b8f9124 enable webgl in chromium 2026-02-25 22:58:28 +01:00
lana-k
5265f5493e splitpanes test 2026-02-24 21:28:11 +01:00
lana-k
c0e59f6fb8 update run result test 2026-02-24 21:02:06 +01:00
lana-k
7471744633 add tests for Value Viewer 2026-02-24 20:53:03 +01:00
lana-k
e6e5efa8c6 tests for graphHelper 2026-02-22 22:30:18 +01:00
lana-k
57c36b3900 tests for migration 2026-02-07 22:08:39 +01:00
lana-k
1e8c1761e6 #133 highlight nodes and edges 2026-02-07 21:18:49 +01:00
lana-k
dd30e17ff5 #133 Add node data viewer 2026-01-25 22:15:48 +01:00
lana-k
4e5adc147f #132 node opacity 2026-01-22 22:25:36 +01:00
lana-k
7edc196a02 change repo structure 2026-01-15 21:53:12 +01:00
lana-k
85b5a200e2 fix tinycolor2 bundle 2025-12-27 21:11:01 +01:00
lana-k
a0ef93921f #131 fix label color 2025-12-26 20:56:08 +01:00
lana-k
859cd2ccfc #129 fix icon 2025-12-25 12:29:28 +01:00
77 changed files with 2734 additions and 490 deletions

View File

@@ -92,11 +92,23 @@ module.exports = function (config) {
'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
}
},
ChromiumHeadlessWebGL: {
base: 'ChromiumHeadless',
flags: [
'--headless=new',
'--use-angle=swiftshader',
'--use-gl=angle',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--disable-gpu-sandbox',
'--no-sandbox'
]
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
browsers: ['ChromiumHeadlessWebGL', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits

18
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sqliteviz",
"version": "0.27.1",
"version": "0.28.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sqliteviz",
"version": "0.27.1",
"version": "0.28.2",
"license": "Apache-2.0",
"dependencies": {
"@sigma/export-image": "^3.0.0",
@@ -32,6 +32,7 @@
"sigma": "^3.0.1",
"sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",
@@ -13341,6 +13342,11 @@
"react": "^0.14.0 || ^15.0.0"
}
},
"node_modules/react-chart-editor/node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/react-color": {
"version": "2.19.3",
"license": "MIT",
@@ -15514,8 +15520,12 @@
"license": "MIT"
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"license": "MIT"
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
"integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==",
"engines": {
"node": "*"
}
},
"node_modules/tinyglobby": {
"version": "0.2.12",

View File

@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "0.28.0",
"version": "0.29.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
@@ -36,6 +36,7 @@
"sigma": "^3.0.1",
"sql.js": "file:./lib/sql-js",
"tiny-emitter": "^2.1.0",
"tinycolor2": "^1.4.2",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",

View File

@@ -27,7 +27,7 @@
<script>
import CloseIcon from '@/components/svg/close'
import { version } from '../../../package.json'
import { version } from '../../package.json'
export default {
name: 'AppDiagnosticInfo',

View File

@@ -23,7 +23,7 @@
<script>
import tooltipMixin from '@/tooltipMixin'
import LoadingIndicator from '@/components/LoadingIndicator'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default {
name: 'SideBarButton',

View File

@@ -46,7 +46,7 @@
</template>
<script>
import LoadingIndicator from '@/components/LoadingIndicator'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {

View File

@@ -18,7 +18,7 @@
</template>
<script>
import LoadingIndicator from '@/components/LoadingIndicator'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
export default {
name: 'Logs',

View File

@@ -13,16 +13,16 @@
:style="movableSplitterStyle"
/>
<div
v-show="!before.hidden"
ref="left"
class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before"
>
<slot name="left-pane" />
</div>
<!-- Splitter start-->
<div
v-show="!before.hidden && !after.hidden"
class="splitpanes-splitter"
@mousedown="bindEvents"
@touchstart="bindEvents"
@@ -64,7 +64,12 @@
</div>
</div>
<!-- splitter end -->
<div ref="right" class="splitpanes-pane" :style="styles.after">
<div
v-show="!after.hidden"
ref="right"
class="splitpanes-pane"
:style="styles.after"
>
<slot name="right-pane" />
</div>
</div>
@@ -114,10 +119,12 @@ export default {
styles() {
return {
before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
[this.horizontal ? 'height' : 'width']:
`${this.after.hidden ? 100 : this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
[this.horizontal ? 'height' : 'width']:
`${this.before.hidden ? 100 : this.paneAfter.size}%`
}
}
},

View File

@@ -102,11 +102,11 @@
<script>
import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import TextField from '@/components/Common/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import CheckBox from '@/components/Common/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'

View File

@@ -11,6 +11,7 @@
:initOptions="initOptionsByMode[mode]"
:data-sources="dataSource"
:showViewSettings="showViewSettings"
:showValueViewer="viewValuePanelVisible"
@loading-image-completed="loadingImage = false"
@update="$emit('update')"
/>
@@ -56,6 +57,17 @@
<settings-icon />
</icon-button>
<icon-button
v-if="mode === 'graph'"
ref="viewNodeOrEdgeBtn"
tooltip="View node or edge details"
tooltipPosition="top-left"
:active="viewValuePanelVisible"
@click="viewValuePanelVisible = !viewValuePanelVisible"
>
<view-cell-value-icon />
</icon-button>
<div class="side-tool-bar-divider" />
<icon-button
@@ -113,11 +125,11 @@
</template>
<script>
import Chart from './Chart/index.vue'
import Pivot from './Pivot/index.vue'
import Graph from './Graph/index.vue'
import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton'
import Chart from '@/components/Chart.vue'
import Pivot from '@/components/Pivot'
import Graph from '@/components/Graph/index.vue'
import SideToolBar from '@/components/SideToolBar'
import IconButton from '@/components/Common/IconButton'
import ChartIcon from '@/components/svg/chart'
import PivotIcon from '@/components/svg/pivot'
import GraphIcon from '@/components/svg/graph.vue'
@@ -126,8 +138,9 @@ import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue'
import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/LoadingDialog.vue'
import loadingDialog from '@/components/Common/LoadingDialog.vue'
import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
@@ -144,6 +157,7 @@ export default {
GraphIcon,
SettingsIcon,
ExportToSvgIcon,
ViewCellValueIcon,
PngIcon,
HtmlIcon,
ClipboardIcon,
@@ -172,7 +186,8 @@ export default {
graph: this.initMode === 'graph' ? this.initOptions : null
},
showLoadingDialog: false,
showViewSettings: true
showViewSettings: true,
viewValuePanelVisible: false
}
},
computed: {

View File

@@ -67,6 +67,15 @@
@color-change="settings.style.backgroundColor = $event"
/>
</Field>
<Field label="Highlight mode">
<Dropdown
:options="highlightModeOptions"
:value="settings.style.highlightMode"
className="test_highlight_mode_select"
@change="updateHighlightNodeMode"
/>
</Field>
</Fold>
</Panel>
<Panel group="Style" name="Nodes">
@@ -237,7 +246,9 @@ import {
buildNodes,
buildEdges,
updateNodes,
updateEdges
updateEdges,
reduceNodes,
reduceEdges
} from '@/lib/graphHelper'
import Graph from 'graphology'
import { circular, random, circlepack } from 'graphology-layout'
@@ -277,7 +288,7 @@ export default {
initOptions: Object,
showViewSettings: Boolean
},
emits: ['update'],
emits: ['update', 'selectItem', 'clearSelection'],
data() {
return {
graph: new Graph({ multi: true, allowSelfLoops: true }),
@@ -300,7 +311,10 @@ export default {
circlepack: CirclePackLayoutSettings,
forceAtlas2: ForceAtlasLayoutSettings
}),
selectedNodeId: undefined,
hoveredNodeId: undefined,
selectedEdgeId: undefined,
hoveredEdgeId: undefined,
settings: this.initOptions
? JSON.parse(JSON.stringify(this.initOptions))
: {
@@ -312,6 +326,7 @@ export default {
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: {
type: 'constant',
@@ -319,7 +334,8 @@ export default {
},
color: {
type: 'constant',
value: '#1F77B4'
value: '#1F77B4',
opacity: 100
},
label: {
source: null,
@@ -351,7 +367,15 @@ export default {
random: null,
circlepack: null,
forceAtlas2: null
}
},
highlightModeOptions: markRaw([
{ label: 'Node alone', value: 'node_alone' },
{ label: 'Node and neighbors', value: 'node_and_neighbors' },
{
label: 'Include edges between neighbors',
value: 'include_neighbor_edges'
}
])
}
},
computed: {
@@ -378,6 +402,46 @@ export default {
}, new Set())
return Array.from(keySet)
},
neighborsOfSelectedNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.selectedNodeId
? new Set(this.graph.neighbors(this.selectedNodeId))
: undefined
},
neighborsOfHoveredNode() {
if (this.settings.style.highlightMode === 'node_alone') {
return undefined
}
return this.hoveredNodeId
? new Set(this.graph.neighbors(this.hoveredNodeId))
: undefined
},
hoveredEdgeExtremities() {
return this.hoveredEdgeId
? this.graph.extremities(this.hoveredEdgeId)
: []
},
selectedEdgeExtremities() {
return this.selectedEdgeId
? this.graph.extremities(this.selectedEdgeId)
: []
},
interactionState() {
return {
selectedNodeId: this.selectedNodeId,
hoveredNodeId: this.hoveredNodeId,
selectedEdgeId: this.selectedEdgeId,
hoveredEdgeId: this.hoveredEdgeId,
neighborsOfSelectedNode: this.neighborsOfSelectedNode,
neighborsOfHoveredNode: this.neighborsOfHoveredNode,
hoveredEdgeExtremities: this.hoveredEdgeExtremities,
selectedEdgeExtremities: this.selectedEdgeExtremities
}
}
},
watch: {
@@ -422,6 +486,7 @@ export default {
},
methods: {
buildGraph() {
this.clearSelection()
if (this.renderer) {
this.renderer.kill()
}
@@ -437,12 +502,87 @@ export default {
this.updateLayout(this.settings.layout.type)
this.renderer = new Sigma(this.graph, this.$refs.graph, {
renderEdgeLabels: true,
allowInvalidContainer: true
allowInvalidContainer: true,
labelColor: { attribute: 'labelColor', color: '#444444' },
edgeLabelColor: { attribute: 'labelColor', color: '#a2b1c6' },
enableEdgeEvents: true,
zIndex: true,
nodeReducer: (node, data) =>
reduceNodes(node, data, this.interactionState, this.settings),
edgeReducer: (edge, data) =>
reduceEdges(
edge,
data,
this.interactionState,
this.settings,
this.graph
)
})
this.renderer.on('clickNode', ({ node }) => {
this.selectedNodeId = node
this.selectedEdgeId = undefined
this.$emit('selectItem', this.graph.getNodeAttributes(node).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickEdge', ({ edge }) => {
this.selectedEdgeId = edge
this.selectedNodeId = undefined
this.$emit('selectItem', this.graph.getEdgeAttributes(edge).data)
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('clickStage', () => {
this.clearSelection()
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterNode', ({ node }) => {
this.hoveredNodeId = node
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('enterEdge', ({ edge }) => {
this.hoveredEdgeId = edge
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveNode', () => {
this.hoveredNodeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
this.renderer.on('leaveEdge', () => {
this.hoveredEdgeId = undefined
this.renderer.refresh({
skipIndexation: true
})
})
if (this.settings.layout.type === 'forceAtlas2') {
this.autoRunFA2Layout()
}
},
clearSelection() {
this.selectedNodeId = undefined
this.selectedEdgeId = undefined
this.$emit('clearSelection')
},
updateHighlightNodeMode(mode) {
this.settings.style.highlightMode = mode
if (this.renderer) {
this.renderer.refresh({
skipIndexation: true
})
}
},
updateStructure(attributeName, value) {
this.settings.structure[attributeName] = value
},

View File

@@ -57,10 +57,20 @@
</template>
</Field>
<Field label="Opacity" fieldContainerClassName="test_node_opacity">
<NumericInput
:value="modelValue.opacity"
:showSlider="true"
:integerOnly="true"
:max="100"
:min="0"
units="%"
@update="updateSettings('opacity', $event)"
/>
</Field>
<Field
v-if="
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
"
v-if="modelValue.type === 'map_to' || modelValue.type === 'calculated'"
label="Color as"
fieldContainerClassName="test_node_color_as"
>
@@ -89,6 +99,7 @@
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
@@ -98,6 +109,7 @@ import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
@@ -134,19 +146,21 @@ export default {
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
constant: { value: '#1F77B4', opacity: 100 },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
colorscaleDirection: 'normal',
opacity: 100
},
calculated: {
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'normal'
colorscaleDirection: 'normal',
opacity: 100
}
}
}

View File

@@ -13,21 +13,38 @@
documentation</a
>.
</div>
<div
<splitpanes
:before="{ size: 70, max: 100 }"
:after="{ size: 30, max: 50, hidden: !showValueViewer }"
:default="{ before: 70, after: 30 }"
class="graph"
:style="{
height:
!dataSources || !dataSourceIsValid ? 'calc(100% - 40px)' : '100%'
}"
>
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
/>
</div>
<template #left-pane>
<div ref="graphEditorContainer" :style="{ height: '100%' }">
<GraphEditor
ref="graphEditor"
:dataSources="dataSources"
:initOptions="initOptions"
:showViewSettings="showViewSettings"
@update="$emit('update')"
@select-item="selectedItem = $event"
@clear-selection="selectedItem = null"
/>
</div>
</template>
<template v-if="showValueViewer" #right-pane>
<value-viewer
:empty="!selectedItem"
emptyMessage="No node or edge selected to view"
:value="JSON.stringify(selectedItem)"
defaultFormat="json"
/>
</template>
</splitpanes>
</div>
</template>
@@ -35,17 +52,20 @@
import 'react-chart-editor/lib/react-chart-editor.css'
import GraphEditor from '@/components/Graph/GraphEditor.vue'
import { dataSourceIsValid } from '@/lib/graphHelper'
import ValueViewer from '@/components/ValueViewer.vue'
import Splitpanes from '@/components/Common/Splitpanes'
export default {
name: 'Graph',
components: { GraphEditor },
components: { GraphEditor, ValueViewer, Splitpanes },
props: {
dataSources: Object,
initOptions: Object,
exportToPngEnabled: Boolean,
exportToSvgEnabled: Boolean,
exportToHtmlEnabled: Boolean,
showViewSettings: Boolean
showViewSettings: Boolean,
showValueViewer: Boolean
},
emits: [
'update:exportToSvgEnabled',
@@ -57,7 +77,8 @@ export default {
],
data() {
return {
resizeObserver: null
resizeObserver: null,
selectedItem: null
}
},
computed: {
@@ -83,10 +104,10 @@ export default {
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.graphContainer)
this.resizeObserver.observe(this.$refs.graphEditorContainer)
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.graphContainer)
this.resizeObserver.unobserve(this.$refs.graphEditorContainer)
},
methods: {
getOptionsForSave() {
@@ -100,7 +121,7 @@ export default {
return this.$refs.graphEditor.prepareCopy()
},
async handleResize() {
const renderer = this.$refs.graphEditor.renderer
const renderer = this.$refs.graphEditor?.renderer
if (renderer) {
renderer.refresh()
renderer.getCamera().animatedReset({ duration: 600 })

View File

@@ -87,7 +87,7 @@
</template>
<script>
import TextField from '@/components/TextField'
import TextField from '@/components/Common/TextField'
import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo'

View File

@@ -33,7 +33,7 @@ import 'pivottable'
import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi/index.vue'
import pivotHelper from './pivotHelper'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart'
import Chart from '@/components/Chart'
import chartHelper from '@/lib/chartHelper'
import events from '@/lib/utils/events'
import plotly from 'plotly.js'

View File

@@ -40,7 +40,7 @@
</template>
<script>
import IconButton from '@/components/IconButton'
import IconButton from '@/components/Common/IconButton'
import ArrowIcon from '@/components/svg/arrow'
import EdgeArrowIcon from '@/components/svg/edgeArrow'

View File

@@ -1,40 +1,66 @@
<template>
<div ref="runResultPanel" class="run-result-panel">
<component
:is="viewValuePanelVisible ? 'splitpanes' : 'div'"
<splitpanes
:before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100, hidden: !viewValuePanelVisible }"
:default="{ before: 50, after: 50 }"
class="run-result-panel-content"
>
<template #left-pane>
<div
:id="'run-result-left-pane-' + tab.id"
class="result-set-container"
/>
</template>
<div
:id="'run-result-result-set-' + tab.id"
class="result-set-container"
/>
<template v-if="viewValuePanelVisible" #right-pane>
<div class="value-viewer-container">
<value-viewer
v-show="selectedCell"
:cellValue="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
<div v-show="!selectedCell" class="table-preview">
No cell selected to view
<div class="result-set-container">
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selectedCellCoordinates="defaultSelectedCell"
class="straight"
@update-selected-cell="onUpdateSelectedCell"
/>
<record
v-if="result && viewRecord"
ref="recordView"
:data-set="result"
:time="time"
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@update-selected-cell="onUpdateSelectedCell"
/>
</div>
</template>
</component>
<template v-if="viewValuePanelVisible" #right-pane>
<value-viewer
:empty="!selectedCell"
emptyMessage="No cell selected to view"
:value="
selectedCell
? result.values[result.columns[selectedCell.dataset.col]][
selectedCell.dataset.row
]
: ''
"
/>
</template>
</splitpanes>
<side-tool-bar panel="table" @switch-to="$emit('switchTo', $event)">
<icon-button
@@ -89,69 +115,27 @@
@action="copyToClipboard"
@cancel="cancelCopy"
/>
<teleport defer :to="resultSetTeleportTarget" :disabled="!enableTeleport">
<div>
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-if="isGettingResults" class="table-preview result-in-progress">
<loading-indicator :size="30" />
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<logs v-if="error" :messages="[error]" />
<sql-table
v-if="result && !viewRecord"
:data-set="result"
:time="time"
:pageSize="pageSize"
:page="defaultPage"
:selectedCellCoordinates="defaultSelectedCell"
class="straight"
@update-selected-cell="onUpdateSelectedCell"
/>
<record
v-if="result && viewRecord"
ref="recordView"
:data-set="result"
:time="time"
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
@update-selected-cell="onUpdateSelectedCell"
/>
</div>
</teleport>
</div>
</template>
<script>
import Logs from '@/components/Logs'
import SqlTable from '@/components/SqlTable/index.vue'
import LoadingIndicator from '@/components/LoadingIndicator'
import SideToolBar from '../SideToolBar'
import Splitpanes from '@/components/Splitpanes'
import Logs from '@/components/Common/Logs'
import SqlTable from '@/components/SqlTable'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import SideToolBar from '@/components/SideToolBar'
import Splitpanes from '@/components/Common/Splitpanes'
import ExportToCsvIcon from '@/components/svg/exportToCsv'
import ClipboardIcon from '@/components/svg/clipboard'
import ViewCellValueIcon from '@/components/svg/viewCellValue'
import RowIcon from '@/components/svg/row'
import IconButton from '@/components/IconButton'
import IconButton from '@/components/Common/IconButton'
import csv from '@/lib/csv'
import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog'
import loadingDialog from '@/components/Common/LoadingDialog'
import events from '@/lib/utils/events'
import ValueViewer from './ValueViewer'
import ValueViewer from '@/components/ValueViewer'
import Record from './Record/index.vue'
export default {
@@ -172,7 +156,6 @@ export default {
Splitpanes
},
props: {
tab: Object,
result: Object,
isGettingResults: Boolean,
error: Object,
@@ -194,20 +177,6 @@ export default {
showLoadingDialog: false
}
},
computed: {
resultSetTeleportTarget() {
if (!this.enableTeleport) {
return undefined
}
const base = `#${
this.viewValuePanelVisible
? 'run-result-left-pane'
: 'run-result-result-set'
}`
const tabIdPostfix = `-${this.tab.id}`
return base + tabIdPostfix
}
},
watch: {
result() {
this.defaultSelectedCell = null
@@ -332,19 +301,12 @@ export default {
width: 0;
}
.result-set-container,
.result-set-container > div {
.result-set-container {
position: relative;
height: 100%;
width: 100%;
box-sizing: border-box;
}
.value-viewer-container {
height: 100%;
width: 100%;
background-color: var(--color-white);
position: relative;
}
.table-preview {
position: absolute;

View File

@@ -35,7 +35,7 @@
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import TableDescription from './TableDescription'
import TextField from '@/components/TextField'
import TextField from '@/components/Common/TextField'
import TreeChevron from '@/components/svg/treeChevron'
import DbUploader from '@/components/DbUploader'
import ExportIcon from '@/components/svg/export'

View File

@@ -37,7 +37,7 @@
</template>
<script>
import IconButton from '@/components/IconButton'
import IconButton from '@/components/Common/IconButton'
import TableIcon from '@/components/svg/table'
import SqlEditorIcon from '@/components/svg/sqlEditor'
import DataViewIcon from '@/components/svg/dataView'

View File

@@ -34,7 +34,7 @@ import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/autorefresh.js'
import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton'
import IconButton from '@/components/Common/IconButton'
import RunIcon from '@/components/svg/run'
export default {

View File

@@ -69,7 +69,7 @@
</template>
<script>
import Pager from './Pager.vue'
import Pager from '@/components/Common/Pager.vue'
export default {
name: 'SqlTable',

View File

@@ -37,7 +37,6 @@
:disabled="!enableTeleport"
>
<run-result
:tab="tab"
:result="tab.result"
:isGettingResults="tab.isGettingResults"
:error="tab.error"
@@ -64,10 +63,10 @@
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import SqlEditor from './SqlEditor'
import DataView from './DataView'
import RunResult from './RunResult'
import Splitpanes from '@/components/Common/Splitpanes'
import SqlEditor from '@/components/SqlEditor'
import DataView from '@/components/DataView'
import RunResult from '@/components/RunResult'
import { nextTick, computed } from 'vue'
import events from '@/lib/utils/events'

View File

@@ -1,48 +1,56 @@
<template>
<div class="value-viewer">
<div class="value-viewer-toolbar">
<button
v-for="format in formats"
:key="format.value"
type="button"
:aria-selected="currentFormat === format.value"
:class="format.value"
@click="currentFormat = format.value"
>
{{ format.text }}
</button>
<template v-if="!empty">
<div class="value-viewer-toolbar">
<button
v-for="format in formats"
:key="format.value"
type="button"
:aria-selected="currentFormat === format.value"
:class="format.value"
@click="currentFormat = format.value"
>
{{ format.text }}
</button>
<button type="button" class="copy" @click="copyToClipboard">Copy</button>
<button
type="button"
class="line-wrap"
:aria-selected="lineWrapping === true"
@click="lineWrapping = !lineWrapping"
>
Line wrap
</button>
</div>
<div class="value-body">
<codemirror
v-if="currentFormat === 'json' && formattedJson"
:value="formattedJson"
:options="cmOptions"
class="json-value original-style"
/>
<pre
v-if="currentFormat === 'text'"
:class="[
'text-value',
{ 'meta-value': isNull || isBlob },
{ 'line-wrap': lineWrapping }
]"
>{{ cellText }}</pre
>
<logs
v-if="messages && messages.length > 0"
:messages="messages"
class="messages"
/>
<button type="button" class="copy" @click="copyToClipboard">
Copy
</button>
<button
type="button"
class="line-wrap"
:aria-selected="lineWrapping === true"
@click="lineWrapping = !lineWrapping"
>
Line wrap
</button>
</div>
<div class="value-body">
<codemirror
v-if="currentFormat === 'json' && formattedJson"
:value="formattedJson"
:options="cmOptions"
class="json-value original-style"
/>
<pre
v-if="currentFormat === 'text'"
:class="[
'text-value',
{ 'meta-value': isNull || isBlob },
{ 'line-wrap': lineWrapping }
]"
>{{ cellText }}</pre
>
<logs
v-if="messages && messages.length > 0"
:messages="messages"
class="messages"
/>
</div>
</template>
<div v-show="empty" class="empty-message">
{{ emptyMessage }}
</div>
</div>
</template>
@@ -57,15 +65,22 @@ import 'codemirror/addon/fold/foldgutter.css'
import 'codemirror/addon/fold/brace-fold.js'
import 'codemirror/theme/neo.css'
import cIo from '@/lib/utils/clipboardIo'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
export default {
name: 'ValueViewer',
components: {
Codemirror,
Logs
},
props: {
cellValue: [String, Number, Uint8Array]
value: [String, Number, Uint8Array],
empty: Boolean,
emptyMessage: String,
defaultFormat: {
type: String,
default: 'text'
}
},
data() {
return {
@@ -73,7 +88,7 @@ export default {
{ text: 'Text', value: 'text' },
{ text: 'JSON', value: 'json' }
],
currentFormat: 'text',
currentFormat: this.defaultFormat,
lineWrapping: false,
formattedJson: '',
messages: []
@@ -94,13 +109,13 @@ export default {
}
},
isBlob() {
return this.cellValue && ArrayBuffer.isView(this.cellValue)
return this.value && ArrayBuffer.isView(this.value)
},
isNull() {
return this.cellValue === null
return this.value === null
},
cellText() {
const value = this.cellValue
const value = this.value
if (this.isNull) {
return 'NULL'
}
@@ -111,17 +126,23 @@ export default {
}
},
watch: {
currentFormat() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
currentFormat: {
immediate: true,
handler() {
this.messages = []
this.formattedJson = ''
if (this.currentFormat === 'json') {
this.formatJson(this.value)
}
}
},
cellValue() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.cellValue)
value: {
immediate: true,
handler() {
this.messages = []
if (this.currentFormat === 'json') {
this.formatJson(this.value)
}
}
}
},
@@ -141,7 +162,7 @@ export default {
},
copyToClipboard() {
cIo.copyText(
this.currentFormat === 'json' ? this.formattedJson : this.cellValue,
this.currentFormat === 'json' ? this.formattedJson : this.value,
'The value is copied to clipboard.'
)
}
@@ -153,8 +174,10 @@ export default {
.value-viewer {
background-color: var(--color-white);
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
}
.value-viewer-toolbar {
display: flex;
@@ -219,4 +242,14 @@ export default {
width: 1px;
background: var(--color-text-base);
}
.empty-message {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
}
</style>

View File

@@ -47,62 +47,61 @@
16.5336 10.0345 16.57C9.99349 16.6065 9.94049 16.6266 9.88559
16.6266V17.9725ZM8.11441
17.9725H9.88559V16.6266H8.11441V17.9725ZM6.55522
16.5879C6.60065 16.9695 6.78445 17.3212 7.0718 17.5764C7.35916
17.8316 7.73011 17.9725 8.11441 17.9725V16.6266C8.05951 16.6266
8.00651 16.6065 7.96546 16.57C7.9244 16.5336 7.89815 16.4833
7.89167 16.4288L6.55522 16.5879ZM6.44172 15.6342L6.55522 1
6.5879L7.89167 16.4288L7.77817 15.475L6.44172 15.6342ZM4.00097
13.2967L3.12066 13.6742L3.65104 14.9112L4.53136 14.5337L4.00097
13.2967ZM3.12066 13.6742C3.07021 13.6959 3.01346 13.6982 2.96138
13.6809C2.90929 13.6636 2.86536 13.6278 2.83791 13.5802L1.67238
14.2532C1.86453 14.586 2.17205 14.8368 2.53672 14.958C2.90138
15.0793 3.29785 15.0626 3.65104 14.9112L3.12066 13.6742ZM2.83791
13.5802L1.95233 12.0464L0.786798 12.7193L1.67238 14.2532L2.83791
13.5802ZM1.95233 12.0464C1.92487 11.9988 1.91582 11.9429 1.92688
11.8891C1.93794 11.8353 1.96834 11.7874 2.01233 11.7546L1.20626
10.6767C0.898499 10.9069 0.685823 11.2419 0.608517 11.6183C0.531211
11.9948 0.594643 12.3865 0.786798 12.7193L1.95233 12.0464ZM2.01233
11.7546L2.70007 11.2403L1.89389 10.1625L1.20626 10.6767L2.01233
11.7546ZM1.20626 7.32326L1.894 7.83761L2.70007 6.75979L2.01233
6.24544L1.20626 7.32326ZM0.786798 5.28067C0.594643 5.61348
0.531211 6.00521 0.608517 6.38165C0.685823 6.75809 0.898499
7.09311 1.20626 7.32326L2.01233 6.24544C1.96834 6.21257
1.93794 6.16471 1.92688 6.11093C1.91582 6.05714 1.92487
6.00117 1.95233 5.95361L0.786798 5.28067ZM1.67238
3.74682L0.786798 5.28067L1.95233 5.95361L2.83791
4.41976L1.67238 3.74682ZM3.65104 3.0888C3.29785
2.93735 2.90138 2.92067 2.53672 3.04195C2.17205 3.16322 1.86453
3.41401 1.67238 3.74682L2.83791 4.41976C2.86536 4.37223 2.90929
4.33641 2.96138 4.31909C3.01346 4.30176 3.07021 4.30414 3.12066
4.32577L3.65104 3.0888ZM4.53136 3.46632L3.65104 3.0888L3.12066
4.32577L4.00097 4.70329L4.53136 3.46632ZM6.55522 1.41207L6.44172
2.36584L7.77817 2.52499L7.89167 1.57122L6.55522 1.41207ZM6.3585
4.5022C7.04018 4.11795 7.6696 3.4366 7.77817 2.52499L6.44172
2.36584C6.39887 2.72575 6.13116 3.08544 5.69756 3.32994L6.3585
4.5022ZM4.00097 4.70329C4.81792 5.05367 5.68948 4.87938 6.3585
4.5022L5.69756 3.32994C5.28213 3.56412 4.85852 3.60663 4.53136
3.46632L4.00097 4.70329ZM3.72866 9C3.72866 8.20369 3.44288 7.3153
2.70007 6.75979L1.894 7.83761C2.19884 8.06562 2.38278 8.48834 2.38278
9H3.72866ZM6.3585 13.4978C5.68948 13.1206 4.81792 12.9463 4.00097
13.2967L4.53136 14.5337C4.85852 14.3934 5.28213 14.436 5.69756
14.6703L6.3585 13.4978ZM2.70007 11.2403C3.44299 10.6848 3.72866
9.79631 3.72866 9H2.38278C2.38278 9.51177 2.19873 9.9346 1.89389
10.1625L2.70007 11.2403ZM11.5243 13.4358C10.9034 13.8057 10.3448
14.4432 10.245 15.2803L11.5814 15.4394C11.6183 15.1293 11.8415
14.8134 12.2132 14.5919L11.5243 13.4358ZM13.8125 13.2167C13.0169
12.8756 12.1673 13.0527 11.5243 13.4358L12.2132 14.5919C12.5925
14.3659 12.9839 14.3258 13.282 14.4536L13.8125 13.2167ZM7.77817
15.475C7.6696 14.5635 7.04018 13.8822 6.3585 13.4978L5.69756
14.6703C6.13116 14.9147 6.39887 15.2742 6.44172 15.6342L7.77817
15.475ZM14.047 9C14.047 9.71656 14.316 10.5045 14.9802
11.0011L15.7861 9.92327C15.547 9.74449 15.3929 9.41139 15.3929
9H14.047ZM14.9801 6.99891C14.3159 7.49553 14.047 8.28332 14.047
9H15.3929C15.3929 8.58861 15.547 8.2555 15.7861 8.07673L14.9801
6.99891ZM11.5243 4.56422C12.1673 4.94734 13.0168 5.12444 13.8123
4.78326L13.282 3.5464C12.9839 3.67426 12.5925 3.63399 12.2132
3.408L11.5243 4.56422ZM10.2449 2.7197C10.3446 3.55683 10.9034
4.19433 11.5243 4.56422L12.2132 3.408C11.8415 3.1866 11.6184
2.87077 11.5815 2.56055L10.2449 2.7197Z"
16.5879C6.60065 16.9695 6.78445 17.3212 7.0718
17.5764C7.35916 17.8316 7.73011 17.9725 8.11441
17.9725V16.6266C8.05951 16.6266 8.00651 16.6065
7.96546 16.57C7.9244 16.5336 7.89815 16.4833 7.89167
16.4288L6.55522 16.5879ZM6.44172 15.6342L6.55522 16.5879L7.89167
16.4288L7.77817 15.475L6.44172 15.6342ZM4.00097 13.2967L3.12066
13.6742L3.65104 14.9112L4.53136 14.5337L4.00097 13.2967ZM3.12066
13.6742C3.07021 13.6959 3.01346 13.6982 2.96138 13.6809C2.90929
13.6636 2.86536 13.6278 2.83791 13.5802L1.67238 14.2532C1.86453
14.586 2.17205 14.8368 2.53672 14.958C2.90138 15.0793 3.29785
15.0626 3.65104 14.9112L3.12066 13.6742ZM2.83791 13.5802L1.95233
12.0464L0.786798 12.7193L1.67238 14.2532L2.83791 13.5802ZM1.95233
12.0464C1.92487 11.9988 1.91582 11.9429 1.92688 11.8891C1.93794
11.8353 1.96834 11.7874 2.01233 11.7546L1.20626 10.6767C0.898499
10.9069 0.685823 11.2419 0.608517 11.6183C0.531211 11.9948 0.594643
12.3865 0.786798 12.7193L1.95233 12.0464ZM2.01233 11.7546L2.70007
11.2403L1.89389 10.1625L1.20626 10.6767L2.01233 11.7546ZM1.20626
7.32326L1.894 7.83761L2.70007 6.75979L2.01233 6.24544L1.20626
7.32326ZM0.786798 5.28067C0.594643 5.61348 0.531211 6.00521
0.608517 6.38165C0.685823 6.75809 0.898499 7.09311 1.20626
7.32326L2.01233 6.24544C1.96834 6.21257 1.93794 6.16471 1.92688
6.11093C1.91582 6.05714 1.92487 6.00117 1.95233 5.95361L0.786798
5.28067ZM1.67238 3.74682L0.786798 5.28067L1.95233 5.95361L2.83791
4.41976L1.67238 3.74682ZM3.65104 3.0888C3.29785 2.93735 2.90138
2.92067 2.53672 3.04195C2.17205 3.16322 1.86453 3.41401 1.67238
3.74682L2.83791 4.41976C2.86536 4.37223 2.90929 4.33641 2.96138
4.31909C3.01346 4.30176 3.07021 4.30414 3.12066 4.32577L3.65104
3.0888ZM4.53136 3.46632L3.65104 3.0888L3.12066 4.32577L4.00097
4.70329L4.53136 3.46632ZM6.55522 1.41207L6.44172 2.36584L7.77817
2.52499L7.89167 1.57122L6.55522 1.41207ZM6.3585 4.5022C7.04018
4.11795 7.6696 3.4366 7.77817 2.52499L6.44172 2.36584C6.39887 2.72575
6.13116 3.08544 5.69756 3.32994L6.3585 4.5022ZM4.00097 4.70329C4.81792
5.05367 5.68948 4.87938 6.3585 4.5022L5.69756 3.32994C5.28213 3.56412
4.85852 3.60663 4.53136 3.46632L4.00097 4.70329ZM3.72866 9C3.72866
8.20369 3.44288 7.3153 2.70007 6.75979L1.894 7.83761C2.19884 8.06562
2.38278 8.48834 2.38278 9H3.72866ZM6.3585 13.4978C5.68948 13.1206
4.81792 12.9463 4.00097 13.2967L4.53136 14.5337C4.85852 14.3934
5.28213 14.436 5.69756 14.6703L6.3585 13.4978ZM2.70007
11.2403C3.44299 10.6848 3.72866 9.79631 3.72866 9H2.38278C2.38278
9.51177 2.19873 9.9346 1.89389 10.1625L2.70007 11.2403ZM11.5243
13.4358C10.9034 13.8057 10.3448 14.4432 10.245 15.2803L11.5814
15.4394C11.6183 15.1293 11.8415 14.8134 12.2132 14.5919L11.5243
13.4358ZM13.8125 13.2167C13.0169 12.8756 12.1673 13.0527 11.5243
13.4358L12.2132 14.5919C12.5925 14.3659 12.9839 14.3258 13.282
14.4536L13.8125 13.2167ZM7.77817 15.475C7.6696 14.5635 7.04018
13.8822 6.3585 13.4978L5.69756 14.6703C6.13116 14.9147 6.39887
15.2742 6.44172 15.6342L7.77817 15.475ZM14.047 9C14.047 9.71656
14.316 10.5045 14.9802 11.0011L15.7861 9.92327C15.547 9.74449 15.3929
9.41139 15.3929 9H14.047ZM14.9801 6.99891C14.3159 7.49553 14.047
8.28332 14.047 9H15.3929C15.3929 8.58861 15.547 8.2555 15.7861
8.07673L14.9801 6.99891ZM11.5243 4.56422C12.1673 4.94734 13.0168
5.12444 13.8123 4.78326L13.282 3.5464C12.9839 3.67426 12.5925
3.63399 12.2132 3.408L11.5243 4.56422ZM10.2449 2.7197C10.3446
3.55683 10.9034 4.19433 11.5243 4.56422L12.2132 3.408C11.8415
3.1866 11.6184 2.87077 11.5815 2.56055L10.2449 2.7197Z"
fill="#A2B1C6"
/>
<path

View File

@@ -127,6 +127,119 @@ export function updateEdges(graph, attributeUpdates) {
})
}
export function reduceNodes(nodeId, nodeData, interactionState, settings) {
const {
selectedNodeId,
hoveredNodeId,
selectedEdgeId,
hoveredEdgeId,
neighborsOfSelectedNode,
neighborsOfHoveredNode,
selectedEdgeExtremities,
hoveredEdgeExtremities
} = interactionState
const res = { ...nodeData }
if (selectedNodeId || hoveredNodeId || hoveredEdgeId || selectedEdgeId) {
res.zIndex = 2
res.highlighted = nodeId === selectedNodeId || nodeId === hoveredNodeId
const isInHoveredFamily =
nodeId === hoveredNodeId ||
neighborsOfHoveredNode?.has(nodeId) ||
hoveredEdgeExtremities.includes(nodeId)
const isInSelectedFamily =
nodeId === selectedNodeId ||
neighborsOfSelectedNode?.has(nodeId) ||
selectedEdgeExtremities.includes(nodeId)
if (isInSelectedFamily || isInHoveredFamily) {
res.forceLabel = true
} else {
res.color = getDiminishedColor(
nodeData.color,
settings.style.backgroundColor
)
res.label = ''
res.zIndex = 1
}
}
return res
}
export function reduceEdges(
edgeId,
edgeData,
interactionState,
settings,
graph
) {
const {
selectedNodeId,
hoveredNodeId,
selectedEdgeId,
hoveredEdgeId,
neighborsOfSelectedNode,
neighborsOfHoveredNode
} = interactionState
const res = { ...edgeData }
if (hoveredEdgeId || selectedEdgeId || selectedNodeId || hoveredNodeId) {
const extremities = graph.extremities(edgeId)
res.zIndex = 2
const isHighlighted = hoveredEdgeId === edgeId || selectedEdgeId === edgeId
let isVisible
if (settings.style.highlightMode === 'node_alone') {
isVisible = isHighlighted
} else if (settings.style.highlightMode === 'node_and_neighbors') {
isVisible =
isHighlighted ||
(selectedNodeId && extremities.includes(selectedNodeId)) ||
(hoveredNodeId && extremities.includes(hoveredNodeId))
} else {
isVisible =
isHighlighted ||
(selectedNodeId &&
extremities.every(
n => n === selectedNodeId || neighborsOfSelectedNode.has(n)
)) ||
(hoveredNodeId &&
extremities.every(
n => n === hoveredNodeId || neighborsOfHoveredNode.has(n)
))
}
if (isHighlighted) {
res.size = res.size * 2
res.forceLabel = true
} else if (!isVisible) {
res.color = getDiminishedColor(
edgeData.color,
settings.style.backgroundColor
)
res.zIndex = 1
res.label = ''
}
}
return res
}
export function getDiminishedColor(color, bgColor) {
const colorObj = tinycolor(color)
const colorOpacity = colorObj.getAlpha()
colorObj.setAlpha(0.25 * colorOpacity)
const fg = colorObj.toRgb()
const bg = tinycolor(bgColor).toRgb()
const r = Math.round(fg.r * fg.a + bg.r * (1 - fg.a))
const g = Math.round(fg.g * fg.a + bg.g * (1 - fg.a))
const b = Math.round(fg.b * fg.a + bg.b * (1 - fg.a))
return tinycolor({ r, g, b, a: 1 }).toHexString()
}
function getUpdateLabelMethod(labelSettings) {
const { source, color } = labelSettings
return attributes => {
@@ -161,9 +274,14 @@ function getUpdateSizeMethod(graph, sizeSettings) {
}
}
function getDirectVariableColorUpdateMethod(source) {
return attributes =>
(attributes.color = tinycolor(attributes.data[source]).toHexString())
function getDirectVariableColorUpdateMethod(source, opacity = 100) {
return attributes => {
const color = tinycolor(attributes.data[source])
const colorOpacity = color.getAlpha()
attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String()
}
}
function getUpdateNodeColorMethod(graph, colorSettings) {
@@ -175,10 +293,16 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
colorscale,
colorscaleDirection,
mode,
method
method,
opacity
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
const color = tinycolor(value)
const colorOpacity = color.getAlpha()
return attributes =>
(attributes.color = color
.setAlpha((opacity / 100) * colorOpacity)
.toHex8String())
} else if (type === 'variable') {
return sourceUsage === 'map_to'
? getColorMethod(
@@ -187,9 +311,10 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
(nodeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getNodeValueScale
getNodeValueScale,
opacity
)
: getDirectVariableColorUpdateMethod(source)
: getDirectVariableColorUpdateMethod(source, opacity)
} else {
return getColorMethod(
graph,
@@ -197,7 +322,8 @@ function getUpdateNodeColorMethod(graph, colorSettings) {
nodeId => graph[method](nodeId),
colorscale,
colorscaleDirection,
getNodeValueScale
getNodeValueScale,
opacity
)
}
}
@@ -244,8 +370,10 @@ function getColorMethod(
sourceGetter,
selectedColorscale,
colorscaleDirection,
valueScaleGetter
valueScaleGetter,
opacity = 100
) {
const opacityFactor = opacity / 100
const valueScale = valueScaleGetter(graph, sourceGetter)
let colorscale = selectedColorscale || DEFAULT_SCALE
if (colorscaleDirection === 'reversed') {
@@ -261,7 +389,9 @@ function getColorMethod(
)
return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category]
attributes.color = tinycolor(colorMap[category])
.setAlpha(opacityFactor)
.toHex8String()
}
} else {
const min = valueScale[0]
@@ -274,14 +404,18 @@ function getColorMethod(
const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) {
attributes.color = '#000000'
attributes.color = tinycolor('#000000')
.setAlpha(opacityFactor)
.toHex8String()
return
}
const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue
)
if (exactMatch) {
attributes.color = tinycolor(exactMatch[1]).toHexString()
attributes.color = tinycolor(exactMatch[1])
.setAlpha(opacityFactor)
.toHex8String()
return
}
@@ -305,7 +439,9 @@ function getColorMethod(
r: r0 + interpolationFactor * (r1 - r0),
g: g0 + interpolationFactor * (g1 - g0),
b: b0 + interpolationFactor * (b1 - b0)
}).toHexString()
})
.setAlpha(opacityFactor)
.toHex8String()
}
}
}

View File

@@ -1,12 +1,22 @@
export default {
_migrate(installedVersion, inquiries) {
if (installedVersion === 1) {
inquiries.forEach(inquire => {
inquire.viewType = 'chart'
inquire.viewOptions = inquire.chart
delete inquire.chart
if (installedVersion < 2) {
inquiries.forEach(inquiry => {
inquiry.viewType = 'chart'
inquiry.viewOptions = inquiry.chart
delete inquiry.chart
})
return inquiries
}
if (installedVersion < 3) {
inquiries.forEach(inquiry => {
if (inquiry.viewType === 'graph') {
inquiry.viewOptions.style.nodes.color.opacity = 100
inquiry.viewOptions.style.highlightMode = 'node_and_neighbors'
}
})
}
return inquiries
}
}

View File

@@ -7,7 +7,7 @@ const migrate = migration._migrate
const myInquiriesKey = 'myInquiries'
export default {
version: 2,
version: 3,
myInquiriesKey,
getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
@@ -21,7 +21,13 @@ export default {
return []
}
return (myInquiries && myInquiries.inquiries) || []
if (myInquiries.version === 2) {
myInquiries = migrate(2, myInquiries.inquiries)
this.updateStorage(myInquiries)
return myInquiries
}
return myInquiries.inquiries || []
},
duplicateInquiry(baseInquiry) {
@@ -63,6 +69,8 @@ export default {
// Turn data into array if they are not
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
inquiryList = migrate(1, inquiryList)
} else if (inquiries.version === 2) {
inquiryList = migrate(2, inquiries.inquiries)
} else {
inquiryList = inquiries.inquiries || []
}
@@ -82,11 +90,11 @@ export default {
importInquiries() {
return fu.importFile().then(str => {
const inquires = this.deserialiseInquiries(str)
const inquiries = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
events.send('inquiry.import', inquiries.length)
return inquires
return inquiries
})
},
export(inquiryList, fileName) {
@@ -102,6 +110,8 @@ export default {
if (!data.version) {
return data.length > 0 ? migrate(1, data) : []
} else if (data.version === 2) {
return migrate(2, data.inquiries)
} else {
return data.inquiries
}

View File

@@ -1,6 +1,6 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Workspace from '@/views/MainView/Workspace'
import Inquiries from '@/views/MainView/Inquiries'
import Workspace from '@/views/Workspace'
import Inquiries from '@/views/Inquiries'
import Welcome from '@/views/Welcome'
import MainView from '@/views/MainView'
import LoadView from '@/views/LoadView'

View File

@@ -184,14 +184,14 @@
</template>
<script>
import RenameIcon from './svg/rename'
import CopyIcon from './svg/copy'
import RenameIcon from '@/components/svg/rename'
import CopyIcon from '@/components/svg/copy'
import ExportIcon from '@/components/svg/export'
import DeleteIcon from './svg/delete'
import DeleteIcon from '@/components/svg/delete'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator'
import TextField from '@/components/Common/TextField'
import CheckBox from '@/components/Common/CheckBox'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries'
import eventBus from '@/lib/eventBus'

View File

@@ -15,7 +15,7 @@
<script>
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
import events from '@/lib/utils/events'
export default {

View File

@@ -10,7 +10,7 @@
</template>
<script>
import MainMenu from './MainMenu'
import MainMenu from '@/components/MainMenu'
import '@/assets/styles/scrollbars.css'
export default {

View File

@@ -17,9 +17,9 @@
</template>
<script>
import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema'
import Tabs from './Tabs'
import Splitpanes from '@/components/Common/Splitpanes'
import Schema from '@/components/Schema'
import Tabs from '@/components/Tabs'
import events from '@/lib/utils/events'
export default {

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, flushPromises } from '@vue/test-utils'
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart/index.vue'
import Chart from '@/components/Chart.vue'
import chartHelper from '@/lib/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo'
@@ -128,6 +128,8 @@ describe('Chart.vue', () => {
expect(plot.scrollWidth).not.to.equal(initialPlotWidth)
expect(plot.scrollHeight).not.to.equal(initialPlotHeight)
container.style.width = 'unset'
container.style.height = 'unset'
wrapper.unmount()
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import CheckBox from '@/components/CheckBox'
import CheckBox from '@/components/Common/CheckBox'
describe('CheckBox', () => {
it('unchecked by default', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import LoadingIndicator from '@/components/LoadingIndicator'
import LoadingIndicator from '@/components/Common/LoadingIndicator'
describe('LoadingIndicator.vue', () => {
it('Calculates animation class', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Logs from '@/components/Logs'
import Logs from '@/components/Common/Logs'
import { nextTick } from 'vue'
let place

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount } from '@vue/test-utils'
import Pager from '@/components/SqlTable/Pager'
import Pager from '@/components/Common/Pager'
describe('Pager.vue', () => {
afterEach(() => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import Splitpanes from '@/components/Splitpanes'
import Splitpanes from '@/components/Common/Splitpanes'
import { nextTick } from 'vue'
describe('Splitpanes.vue', () => {
@@ -49,6 +49,41 @@ describe('Splitpanes.vue', () => {
).to.equal('40%')
})
it('renders correctly with hidden panels', async () => {
// mount the component
const wrapper = shallowMount(Splitpanes, {
attachTo: document.body,
slots: {
leftPane: '<div />',
rightPane: '<div />'
},
props: {
before: { size: 60, max: 100, hidden: true },
after: { size: 40, max: 100 },
horizontal: true
}
})
expect(wrapper.findAll('.splitpanes-pane')[0].isVisible()).to.equal(false)
expect(wrapper.find('.splitpanes-splitter').isVisible()).to.equal(false)
expect(
wrapper.findAll('.splitpanes-pane')[1].element.style.height
).to.equal('100%')
await wrapper.setProps({
before: { size: 60, max: 100 },
after: { size: 40, max: 100, hidden: true }
})
expect(wrapper.findAll('.splitpanes-pane')[1].isVisible()).to.equal(false)
expect(wrapper.find('.splitpanes-splitter').isVisible()).to.equal(false)
expect(
wrapper.findAll('.splitpanes-pane')[0].element.style.height
).to.equal('100%')
wrapper.unmount()
})
it('toggles correctly - no maximized initially', async () => {
// mount the component
const wrapper = shallowMount(Splitpanes, {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import sinon from 'sinon'
import splitter from '@/components/Splitpanes/splitter'
import splitter from '@/components/Common/Splitpanes/splitter'
describe('splitter.js', () => {
afterEach(() => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView/index.vue'
import DataView from '@/components/DataView.vue'
import sinon from 'sinon'
import { nextTick } from 'vue'
import cIo from '@/lib/utils/clipboardIo'
@@ -418,4 +418,52 @@ describe('DataView.vue', () => {
wrapper.findComponent({ name: 'graph' }).props('initOptions')
).to.eql({ test_options: 'latest_graph_options' })
})
it('switches visibility of node or edge in graph mode', async () => {
const wrapper = mount(DataView, {
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
// viewNodeOrEdgeBtn is not disaplyed in chart mode
expect(
wrapper.findComponent({ ref: 'viewNodeOrEdgeBtn' }).exists()
).to.equal(false)
// Switch to pivot
const pivotBtn = wrapper.findComponent({ ref: 'pivotBtn' })
await pivotBtn.trigger('click')
// viewNodeOrEdgeBtn is not disaplyed in pivot mode
expect(
wrapper.findComponent({ ref: 'viewNodeOrEdgeBtn' }).exists()
).to.equal(false)
// Switch to graph
const graphBtn = wrapper.findComponent({ ref: 'graphBtn' })
await graphBtn.trigger('click')
// viewNodeOrEdgeBtn is disaplyed in graph mode
const viewNodeOrEdgeBtn = wrapper.findComponent({
ref: 'viewNodeOrEdgeBtn'
})
expect(viewNodeOrEdgeBtn.exists()).to.equal(true)
// by default node viewer is hidden
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
false
)
// Click to show node viewer
await viewNodeOrEdgeBtn.trigger('click')
expect(wrapper.findComponent({ name: 'value-viewer' }).exists()).to.equal(
true
)
wrapper.unmount()
})
})

View File

@@ -1,7 +1,8 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount, flushPromises } from '@vue/test-utils'
import Graph from '@/views/MainView/Workspace/Tabs/Tab/DataView/Graph/index.vue'
import Graph from '@/components/Graph/index.vue'
import { nextTick } from 'vue'
function getPixels(canvas) {
const context = canvas.getContext('webgl2')
@@ -163,6 +164,326 @@ describe('Graph.vue', () => {
wrapper.unmount()
})
it('the graph resizes when node viewer visibillity togglles', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: false
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialCanvasWidth = canvas.scrollWidth
await wrapper.setProps({ showValueViewer: true })
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialCanvasWidth)
await wrapper.setProps({ showValueViewer: false })
await flushPromises()
expect(canvas.scrollWidth).to.equal(initialCanvasWidth)
wrapper.unmount()
})
it('the graph resizes when the split pane resizes', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: true
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
const container =
wrapper.find('.graph-container').wrapperElement.parentElement
const canvas = wrapper.find('canvas.sigma-nodes').wrapperElement
const initialContainerWidth = container.scrollWidth
const initialCanvasWidth = canvas.scrollWidth
await wrapper.find('.splitpanes-splitter').trigger('mousedown')
document.dispatchEvent(
new MouseEvent('mousemove', {
clientX: initialContainerWidth / 2,
clientY: 80
})
)
document.dispatchEvent(new MouseEvent('mouseup'))
await nextTick()
await nextTick()
await flushPromises()
expect(canvas.scrollWidth).not.to.equal(initialCanvasWidth)
wrapper.unmount()
})
it('opens and closes node viewer', async () => {
const wrapper = mount(Graph, {
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: false
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
expect(wrapper.text()).not.contain('No node or edge selected to view')
await wrapper.setProps({ showValueViewer: true })
expect(wrapper.text()).contains('No node or edge selected to view')
})
it('passes selected item to node viewer', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,
props: {
dataSources: {
doc: [
'{"object_type":0,"node_id":"Gryffindor"}',
'{"object_type":0,"node_id":"Hufflepuff"}'
]
},
initOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: {
type: 'constant',
value: 10
},
color: {
type: 'constant',
value: '#1F77B4'
},
label: {
source: null,
color: '#444444'
}
},
edges: {
showDirection: true,
size: {
type: 'constant',
value: 2
},
color: {
type: 'constant',
value: '#a2b1c6'
},
label: {
source: null,
color: '#a2b1c6'
}
}
},
layout: {
type: 'circular',
options: null
}
},
showValueViewer: true
},
global: {
mocks: { $store },
provide: {
tabLayout: { dataView: 'above' }
}
}
})
expect(wrapper.find('.value-viewer .value-body').exists()).to.equal(false)
await wrapper
.findComponent({ ref: 'graphEditor' })
.vm.$emit('selectItem', { object_type: 0, node_id: 'Gryffindor' })
expect(wrapper.find('.value-viewer .value-body').text()).contains(
'"object_type": 0,'
)
expect(wrapper.find('.value-viewer .value-body').text()).contains(
'"node_id": "Gryffindor"'
)
await wrapper
.findComponent({ ref: 'graphEditor' })
.vm.$emit('clearSelection')
expect(wrapper.find('.value-viewer .value-body').exists()).to.equal(false)
wrapper.unmount()
})
it('nodes and edges are rendered', async () => {
const wrapper = mount(Graph, {
attachTo: document.body,

View File

@@ -24,7 +24,8 @@ const defaultInitOptions = {
},
color: {
type: 'constant',
value: '#1F77B4'
value: '#1F77B4',
opacity: 100
},
label: {
source: null,
@@ -404,7 +405,7 @@ describe('GraphEditor', () => {
doc: [
'{"type": 0, "node_id": 1, "color": "#ff0000", "points": 5}',
'{"type": 0, "node_id": 2, "color": "#abcdff", "points": 15}',
'{"type": 0, "node_id": 3, "color": "#123456", "points": 10}',
'{"type": 0, "node_id": 3, "color": "#12345680", "points": 10}',
'{"type": 1, "source": 2, "target": 3}'
]
},
@@ -440,11 +441,11 @@ describe('GraphEditor', () => {
// Set constant color
await wrapper
.findAllComponents({ name: 'ColorPicker' })[1]
.vm.$emit('colorChange', '#ff00ff')
.vm.$emit('colorChange', '#ff00ff80')
expect(graph.export().nodes[0].attributes.color).to.equal('#ff00ff')
expect(graph.export().nodes[1].attributes.color).to.equal('#ff00ff')
expect(graph.export().nodes[2].attributes.color).to.equal('#ff00ff')
expect(graph.export().nodes[0].attributes.color).to.equal('#ff00ff80')
expect(graph.export().nodes[1].attributes.color).to.equal('#ff00ff80')
expect(graph.export().nodes[2].attributes.color).to.equal('#ff00ff80')
// Switch to Variable
const variable = wrapper.findAll('.test_node_color .radio-block__option')[1]
@@ -459,27 +460,27 @@ describe('GraphEditor', () => {
await wrapper.findAll('.Select__menu .Select__option')[2].trigger('click')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6e')
expect(graph.export().nodes[1].attributes.color).to.equal('#bdea75')
expect(graph.export().nodes[2].attributes.color).to.equal('#86d780')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6eff')
expect(graph.export().nodes[1].attributes.color).to.equal('#bdea75ff')
expect(graph.export().nodes[2].attributes.color).to.equal('#86d780ff')
// Select Direct mapping
await wrapper
.find('.test_node_color_mapping_mode .radio-block__option')
.trigger('click')
expect(graph.export().nodes[0].attributes.color).to.equal('#ff0000')
expect(graph.export().nodes[1].attributes.color).to.equal('#abcdff')
expect(graph.export().nodes[2].attributes.color).to.equal('#123456')
expect(graph.export().nodes[0].attributes.color).to.equal('#ff0000ff')
expect(graph.export().nodes[1].attributes.color).to.equal('#abcdffff')
expect(graph.export().nodes[2].attributes.color).to.equal('#12345680')
// Switch to Calculated
const calculated = wrapper.findAll(
'.test_node_color .radio-block__option'
)[2]
await calculated.trigger('click')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6e')
expect(graph.export().nodes[1].attributes.color).to.equal('#2a4858')
expect(graph.export().nodes[2].attributes.color).to.equal('#2a4858')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6eff')
expect(graph.export().nodes[1].attributes.color).to.equal('#2a4858ff')
expect(graph.export().nodes[2].attributes.color).to.equal('#2a4858ff')
await nextTick()
// Choose in-degree
@@ -491,45 +492,68 @@ describe('GraphEditor', () => {
await wrapper.findAll('.Select__menu .Select__option')[1].trigger('click')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6e')
expect(graph.export().nodes[1].attributes.color).to.equal('#fafa6e')
expect(graph.export().nodes[2].attributes.color).to.equal('#2a4858')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6eff')
expect(graph.export().nodes[1].attributes.color).to.equal('#fafa6eff')
expect(graph.export().nodes[2].attributes.color).to.equal('#2a4858ff')
await nextTick()
// Set another opacity for calculated color
let opacityInput = wrapper.find(
'.test_node_opacity input.numeric-input__number'
)
await opacityInput.setValue(50)
opacityInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6e80')
expect(graph.export().nodes[1].attributes.color).to.equal('#fafa6e80')
expect(graph.export().nodes[2].attributes.color).to.equal('#2a485880')
await nextTick()
// Set Color as to Categorical
await wrapper
.findAll('.test_node_color_as .radio-block__option')[1]
.trigger('click')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6e')
expect(graph.export().nodes[1].attributes.color).to.equal('#fafa6e')
expect(graph.export().nodes[2].attributes.color).to.equal('#bdea75')
expect(graph.export().nodes[0].attributes.color).to.equal('#fafa6e80')
expect(graph.export().nodes[1].attributes.color).to.equal('#fafa6e80')
expect(graph.export().nodes[2].attributes.color).to.equal('#bdea7580')
await nextTick()
// Change colorscale direction
await wrapper
.findAll('.test_node_color_colorscale_direction .radio-block__option')[1]
.trigger('click')
expect(graph.export().nodes[0].attributes.color).to.equal('#2a4858')
expect(graph.export().nodes[1].attributes.color).to.equal('#2a4858')
expect(graph.export().nodes[2].attributes.color).to.equal('#1f5f70')
expect(graph.export().nodes[0].attributes.color).to.equal('#2a485880')
expect(graph.export().nodes[1].attributes.color).to.equal('#2a485880')
expect(graph.export().nodes[2].attributes.color).to.equal('#1f5f7080')
await nextTick()
// Switch to Variable
await variable.trigger('click')
// The latest settings from variable mode are applied
expect(graph.export().nodes[0].attributes.color).to.equal('#ff0000')
expect(graph.export().nodes[1].attributes.color).to.equal('#abcdff')
expect(graph.export().nodes[2].attributes.color).to.equal('#123456')
expect(graph.export().nodes[0].attributes.color).to.equal('#ff0000ff')
expect(graph.export().nodes[1].attributes.color).to.equal('#abcdffff')
expect(graph.export().nodes[2].attributes.color).to.equal('#12345680')
// Switch to Constant
const constant = wrapper.findAll('.test_node_color .radio-block__option')[0]
await constant.trigger('click')
// The latest settings from constant mode are applied
expect(graph.export().nodes[0].attributes.color).to.equal('#ff00ff')
expect(graph.export().nodes[1].attributes.color).to.equal('#ff00ff')
expect(graph.export().nodes[2].attributes.color).to.equal('#ff00ff')
expect(graph.export().nodes[0].attributes.color).to.equal('#ff00ff80')
expect(graph.export().nodes[1].attributes.color).to.equal('#ff00ff80')
expect(graph.export().nodes[2].attributes.color).to.equal('#ff00ff80')
// Set another opacity for constant color
await opacityInput.setValue(50)
opacityInput.wrapperElement.dispatchEvent(
new Event('blur', { bubbles: true })
)
expect(graph.export().nodes[0].attributes.color).to.equal('#ff00ff40')
expect(graph.export().nodes[1].attributes.color).to.equal('#ff00ff40')
expect(graph.export().nodes[2].attributes.color).to.equal('#ff00ff40')
await nextTick()
wrapper.unmount()
})
@@ -840,26 +864,26 @@ describe('GraphEditor', () => {
)
await wrapper.findAll('.Select__menu .Select__option')[5].trigger('click')
expect(graph.export().edges[0].attributes.color).to.equal('#fafa6e')
expect(graph.export().edges[1].attributes.color).to.equal('#bdea75')
expect(graph.export().edges[2].attributes.color).to.equal('#86d780')
expect(graph.export().edges[0].attributes.color).to.equal('#fafa6eff')
expect(graph.export().edges[1].attributes.color).to.equal('#bdea75ff')
expect(graph.export().edges[2].attributes.color).to.equal('#86d780ff')
// Set Color as to Continious
await wrapper
.findAll('.test_edge_color_as .radio-block__option')[0]
.trigger('click')
expect(graph.export().edges[0].attributes.color).to.equal('#fafa6e')
expect(graph.export().edges[1].attributes.color).to.equal('#39b48d')
expect(graph.export().edges[2].attributes.color).to.equal('#2a4858')
expect(graph.export().edges[0].attributes.color).to.equal('#fafa6eff')
expect(graph.export().edges[1].attributes.color).to.equal('#39b48dff')
expect(graph.export().edges[2].attributes.color).to.equal('#2a4858ff')
await nextTick()
// Change colorscale direction
await wrapper
.findAll('.test_edge_color_colorscale_direction .radio-block__option')[1]
.trigger('click')
expect(graph.export().edges[0].attributes.color).to.equal('#2a4858')
expect(graph.export().edges[1].attributes.color).to.equal('#139f8e')
expect(graph.export().edges[2].attributes.color).to.equal('#fafa6e')
expect(graph.export().edges[0].attributes.color).to.equal('#2a4858ff')
expect(graph.export().edges[1].attributes.color).to.equal('#139f8eff')
expect(graph.export().edges[2].attributes.color).to.equal('#fafa6eff')
await nextTick()
// Clear color source
@@ -883,9 +907,9 @@ describe('GraphEditor', () => {
.find('.test_edge_color_mapping_mode .radio-block__option')
.trigger('click')
expect(graph.export().edges[0].attributes.color).to.equal('#ff0000')
expect(graph.export().edges[1].attributes.color).to.equal('#abcdff')
expect(graph.export().edges[2].attributes.color).to.equal('#123456')
expect(graph.export().edges[0].attributes.color).to.equal('#ff0000ff')
expect(graph.export().edges[1].attributes.color).to.equal('#abcdffff')
expect(graph.export().edges[2].attributes.color).to.equal('#123456ff')
// Switch to Constant
const constant = wrapper.findAll('.test_edge_color .radio-block__option')[0]

View File

@@ -2,7 +2,7 @@ import { expect } from 'chai'
import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils'
import { createStore } from 'vuex'
import MainMenu from '@/views/MainView/MainMenu'
import MainMenu from '@/components/MainMenu'
import storedInquiries from '@/lib/storedInquiries'
import { nextTick } from 'vue'
import eventBus from '@/lib/eventBus'

View File

@@ -1,11 +1,11 @@
import { expect } from 'chai'
import { mount, flushPromises } from '@vue/test-utils'
import Pivot from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/index.vue'
import Pivot from '@/components/Pivot/index.vue'
import chartHelper from '@/lib/chartHelper'
import fIo from '@/lib/utils/fileIo'
import $ from 'jquery'
import sinon from 'sinon'
import pivotHelper from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
import pivotHelper from '@/components/Pivot/pivotHelper'
describe('Pivot.vue', () => {
let container

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import PivotSortBtn from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi/PivotSortBtn'
import PivotSortBtn from '@/components/Pivot/PivotUi/PivotSortBtn'
describe('PivotSortBtn.vue', () => {
it('switches order', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import PivotUi from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/PivotUi'
import PivotUi from '@/components/Pivot/PivotUi'
describe('PivotUi.vue', () => {
it('returns value when settings changed', async () => {

View File

@@ -3,7 +3,7 @@ import {
_getDataSources,
getPivotCanvas,
getPivotHtml
} from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
} from '@/components/Pivot/pivotHelper'
describe('pivotHelper.js', () => {
it('_getDataSources returns data sources', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import Record from '@/views/MainView/Workspace/Tabs/Tab/RunResult/Record'
import Record from '@/components/RunResult/Record'
describe('Record.vue', () => {
it('shows record with selected cell', async () => {

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import RunResult from '@/views/MainView/Workspace/Tabs/Tab/RunResult'
import RunResult from '@/components/RunResult'
import csv from '@/lib/csv'
import sinon from 'sinon'
import { nextTick } from 'vue'
@@ -18,7 +18,6 @@ describe('RunResult.vue', () => {
sinon.spy(window, 'alert')
const wrapper = mount(RunResult, {
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {
@@ -54,7 +53,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, {
attachTo: document.body,
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {
@@ -114,7 +112,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, {
attachTo: document.body,
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {
@@ -154,7 +151,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, {
attachTo: document.body,
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {
@@ -196,7 +192,6 @@ describe('RunResult.vue', () => {
it('shows value of selected cell - result set', async () => {
const wrapper = mount(RunResult, {
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {
@@ -250,9 +245,9 @@ describe('RunResult.vue', () => {
// Click on 'bar' cell again
await rows[1].findAll('td')[1].trigger('click')
expect(
wrapper.find('.value-viewer-container .table-preview').text()
).to.equals('No cell selected to view')
expect(wrapper.find('.value-viewer').text()).to.equals(
'No cell selected to view'
)
wrapper.unmount()
})
@@ -260,7 +255,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, {
attachTo: document.body,
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {
@@ -323,9 +317,9 @@ describe('RunResult.vue', () => {
// Click on 'foo' cell again
await rows[1].find('td').trigger('click')
expect(
wrapper.find('.value-viewer-container .table-preview').text()
).to.equals('No cell selected to view')
expect(wrapper.find('.value-viewer').text()).to.equals(
'No cell selected to view'
)
wrapper.unmount()
})
@@ -333,7 +327,6 @@ describe('RunResult.vue', () => {
const wrapper = mount(RunResult, {
attachTo: document.body,
props: {
tab: { id: 1 },
result: {
columns: ['id', 'name'],
values: {

View File

@@ -4,8 +4,8 @@ import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import actions from '@/store/actions'
import mutations from '@/store/mutations'
import Schema from '@/views/MainView/Workspace/Schema'
import TableDescription from '@/views/MainView/Workspace/Schema/TableDescription'
import Schema from '@/components/Schema'
import TableDescription from '@/components/Schema/TableDescription'
import database from '@/lib/database'
import fIo from '@/lib/utils/fileIo'
import csv from '@/lib/csv'

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import TableDescription from '@/views/MainView/Workspace/Schema/TableDescription'
import TableDescription from '@/components/Schema/TableDescription'
describe('TableDescription.vue', () => {
it('Initially the columns are hidden and table name is rendered', () => {

View File

@@ -1,7 +1,7 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import SqlEditor from '@/views/MainView/Workspace/Tabs/Tab/SqlEditor'
import SqlEditor from '@/components/SqlEditor'
import { nextTick } from 'vue'
describe('SqlEditor.vue', () => {

View File

@@ -1,9 +1,7 @@
import { expect } from 'chai'
import sinon from 'sinon'
import state from '@/store/state'
import showHint, {
getHints
} from '@/views/MainView/Workspace/Tabs/Tab/SqlEditor/hint'
import showHint, { getHints } from '@/components/SqlEditor/hint'
import CM from 'codemirror'
describe('hint.js', () => {

View File

@@ -3,7 +3,7 @@ import sinon from 'sinon'
import { mount } from '@vue/test-utils'
import mutations from '@/store/mutations'
import { createStore } from 'vuex'
import Tab from '@/views/MainView/Workspace/Tabs/Tab'
import Tab from '@/components/Tab'
import { nextTick } from 'vue'
let place

View File

@@ -3,7 +3,7 @@ import sinon from 'sinon'
import { shallowMount, mount } from '@vue/test-utils'
import mutations from '@/store/mutations'
import { createStore } from 'vuex'
import Tabs from '@/views/MainView/Workspace/Tabs'
import Tabs from '@/components/Tabs'
import eventBus from '@/lib/eventBus'
import { nextTick } from 'vue'
import cIo from '@/lib/utils/clipboardIo'

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import ValueViewer from '@/views/MainView/Workspace/Tabs/Tab/RunResult/ValueViewer.vue'
import ValueViewer from '@/components/ValueViewer.vue'
import sinon from 'sinon'
describe('ValueViewer.vue', () => {
@@ -11,28 +11,45 @@ describe('ValueViewer.vue', () => {
it('shows value in text mode', () => {
const wrapper = mount(ValueViewer, {
props: {
cellValue: 'foo'
value: 'foo'
}
})
expect(wrapper.find('.value-body').text()).to.equals('foo')
expect(wrapper.find('button.text').attributes('aria-selected')).to.equal(
'true'
)
})
it('shows meta values', async () => {
const wrapper = mount(ValueViewer, {
props: {
value: new Uint8Array()
}
})
expect(wrapper.find('.value-body').text()).to.equals('BLOB')
await wrapper.setProps({ value: null })
expect(wrapper.find('.value-body').text()).to.equals('NULL')
})
it('shows error in json mode if the value is not json', async () => {
const wrapper = mount(ValueViewer, {
props: {
cellValue: 'foo'
value: 'foo',
defaultFormat: 'json'
}
})
await wrapper.find('button.json').trigger('click')
expect(wrapper.find('.value-body').text()).to.equals("Can't parse JSON.")
expect(wrapper.find('button[aria-selected="true"]').text()).contains('JSON')
})
it('copy to clipboard', async () => {
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
const wrapper = mount(ValueViewer, {
props: {
cellValue: 'foo'
value: 'foo'
}
})
@@ -41,13 +58,20 @@ describe('ValueViewer.vue', () => {
expect(window.navigator.clipboard.writeText.calledOnceWith('foo')).to.equal(
true
)
await wrapper.setProps({ value: '{"foo": "bar"}' })
await wrapper.find('button.json').trigger('click')
await wrapper.find('button.copy').trigger('click')
expect(window.navigator.clipboard.writeText.args[1][0]).to.equal(
'{\n "foo": "bar"\n}'
)
})
it('wraps lines', async () => {
const wrapper = mount(ValueViewer, {
attachTo: document.body,
props: {
cellValue: 'foo'
value: 'foo'
}
})
@@ -55,7 +79,7 @@ describe('ValueViewer.vue', () => {
const valueBody = wrapper.find('.value-body').wrapperElement
expect(valueBody.scrollWidth).to.equal(valueBody.clientWidth)
await wrapper.setProps({ cellValue: 'foo'.repeat(100) })
await wrapper.setProps({ value: 'foo'.repeat(100) })
expect(valueBody.scrollWidth).not.to.equal(valueBody.clientWidth)
await wrapper.find('button.line-wrap').trigger('click')
@@ -67,7 +91,7 @@ describe('ValueViewer.vue', () => {
const wrapper = mount(ValueViewer, {
attachTo: document.body,
props: {
cellValue: '{"foo": "foofoofoofoofoofoofoofoofoofoo"}'
value: '{"foo": "foofoofoofoofoofoofoofoofoofoo"}'
}
})
@@ -83,4 +107,15 @@ describe('ValueViewer.vue', () => {
expect(codeMirrorScroll.scrollWidth).to.equal(codeMirrorScroll.clientWidth)
wrapper.unmount()
})
it('shows empty message if empty is true', () => {
const wrapper = mount(ValueViewer, {
props: {
empty: true,
emptyMessage: 'I am empty'
}
})
expect(wrapper.find('.value-viewer').text()).to.equals('I am empty')
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -39,4 +39,101 @@ describe('_migrations.js', () => {
}
])
})
it('migrates from version 2 to the current', () => {
const oldInquiries = [
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' },
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed'
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
createdAt: '2021-05-07T11:05:50.877Z'
}
]
expect(migrations._migrate(2, oldInquiries)).to.eql([
{
id: '123',
name: 'foo',
query: 'SELECT * FROM foo',
viewType: 'chart',
viewOptions: { here_are: 'foo chart settings' },
createdAt: '2021-05-06T11:05:50.877Z'
},
{
id: '456',
name: 'bar',
query: 'SELECT * FROM bar',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
createdAt: '2021-05-07T11:05:50.877Z'
}
])
})
})

View File

@@ -18,7 +18,7 @@ describe('storedInquiries.js', () => {
expect(inquiries).to.eql([])
})
it('getStoredInquiries migrate and returns inquiries of v1', () => {
it('getStoredInquiries migrates and returns inquiries of v1', () => {
localStorage.setItem(
'myQueries',
JSON.stringify([
@@ -55,6 +55,114 @@ describe('storedInquiries.js', () => {
])
})
it('getStoredInquiries migrates and returns inquiries of v2', () => {
localStorage.setItem(
'myInquiries',
JSON.stringify({
version: 2,
inquiries: [
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed'
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
},
{
id: 'Yh1Hc9v7P3mRPZVM59QiD',
query: 'SELECT * from test',
viewType: 'chart',
viewOptions: 'some chart view options',
name: 'student chart',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
}
]
})
)
const inquiries = storedInquiries.getStoredInquiries()
expect(inquiries).to.eql([
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
},
{
id: 'Yh1Hc9v7P3mRPZVM59QiD',
query: 'SELECT * from test',
viewType: 'chart',
viewOptions: 'some chart view options',
name: 'student chart',
updatedAt: '2026-01-19T21:49:40.708Z',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('updateStorage and getStoredInquiries', () => {
const data = [{ id: 1 }, { id: 2 }]
storedInquiries.updateStorage(data)
@@ -136,7 +244,7 @@ describe('storedInquiries.js', () => {
const str = storedInquiries.serialiseInquiries(inquiryList)
const parsedJson = JSON.parse(str)
expect(parsedJson.version).to.equal(2)
expect(parsedJson.version).to.equal(3)
expect(parsedJson.inquiries).to.have.lengthOf(2)
expect(parsedJson.inquiries[1]).to.eql(inquiryList[1])
expect(parsedJson.inquiries[0]).to.eql({
@@ -149,7 +257,7 @@ describe('storedInquiries.js', () => {
})
})
it('deserialiseInquiries migrates inquiries', () => {
it('deserialiseInquiries migrates inquiries of v1', () => {
const str = `[
{
"id": 1,
@@ -188,7 +296,110 @@ describe('storedInquiries.js', () => {
])
})
it('deserialiseInquiries return array for one inquiry of v1', () => {
it('deserialiseInquiries migrates inquiries of v2', () => {
const str = `{
"version": 2,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": "Xh1Hc9v7P3mRPZVM59QiC",
"query": "SELECT * from doc",
"viewType": "graph",
"viewOptions": {
"structure": {
"nodeId": "node_id",
"objectType": "object_type",
"edgeSource": "source",
"edgeTarget": "target"
},
"style": {
"backgroundColor": "white",
"nodes": {
"size": { "type": "constant", "value": 10 },
"color": {
"type": "calculated",
"method": "degree",
"colorscale": null,
"mode": "continious",
"colorscaleDirection": "reversed"
},
"label": { "source": "label", "color": "#444444" }
},
"edges": {
"showDirection": true,
"size": { "type": "constant", "value": 2 },
"color": { "type": "constant", "value": "#a2b1c6" },
"label": { "source": null, "color": "#a2b1c6" }
}
},
"layout": { "type": "circular", "options": null }
},
"name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z"
}
]
}
`
const inquiry = storedInquiries.deserialiseInquiries(str)
expect(inquiry).to.eql([
{
id: 1,
name: 'foo',
query: 'select * from foo',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('deserialiseInquiries returns array for one inquiry of v1', () => {
const str = `
{
"id": 1,
@@ -215,7 +426,7 @@ describe('storedInquiries.js', () => {
it('deserialiseInquiries generates new id to avoid duplication', () => {
storedInquiries.updateStorage([{ id: 1 }])
const str = `{
"version": 2,
"version": 3,
"inquiries": [
{
"id": 1,
@@ -275,7 +486,7 @@ describe('storedInquiries.js', () => {
it('importInquiries', async () => {
const str = `{
"version": 2,
"version": 3,
"inquiries": [{
"id": 1,
"name": "foo",
@@ -300,7 +511,7 @@ describe('storedInquiries.js', () => {
])
})
it('readPredefinedInquiries old', async () => {
it('readPredefinedInquiries v1', async () => {
const str = `[
{
"id": 1,
@@ -325,18 +536,55 @@ describe('storedInquiries.js', () => {
])
})
it('readPredefinedInquiries', async () => {
it('readPredefinedInquiries v2', async () => {
const str = `{
"version": 2,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
}]
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": "Xh1Hc9v7P3mRPZVM59QiC",
"query": "SELECT * from doc",
"viewType": "graph",
"viewOptions": {
"structure": {
"nodeId": "node_id",
"objectType": "object_type",
"edgeSource": "source",
"edgeTarget": "target"
},
"style": {
"backgroundColor": "white",
"nodes": {
"size": { "type": "constant", "value": 10 },
"color": {
"type": "calculated",
"method": "degree",
"colorscale": null,
"mode": "continious",
"colorscaleDirection": "reversed"
},
"label": { "source": "label", "color": "#444444" }
},
"edges": {
"showDirection": true,
"size": { "type": "constant", "value": 2 },
"color": { "type": "constant", "value": "#a2b1c6" },
"label": { "source": null, "color": "#a2b1c6" }
}
},
"layout": { "type": "circular", "options": null }
},
"name": "student graph",
"createdAt": "2026-01-19T21:46:13.899Z"
}
]
}
`
sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
@@ -350,6 +598,90 @@ describe('storedInquiries.js', () => {
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 'Xh1Hc9v7P3mRPZVM59QiC',
query: 'SELECT * from doc',
viewType: 'graph',
viewOptions: {
structure: {
nodeId: 'node_id',
objectType: 'object_type',
edgeSource: 'source',
edgeTarget: 'target'
},
style: {
backgroundColor: 'white',
highlightMode: 'node_and_neighbors',
nodes: {
size: { type: 'constant', value: 10 },
color: {
type: 'calculated',
method: 'degree',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'reversed',
opacity: 100
},
label: { source: 'label', color: '#444444' }
},
edges: {
showDirection: true,
size: { type: 'constant', value: 2 },
color: { type: 'constant', value: '#a2b1c6' },
label: { source: null, color: '#a2b1c6' }
}
},
layout: { type: 'circular', options: null }
},
name: 'student graph',
createdAt: '2026-01-19T21:46:13.899Z'
}
])
})
it('readPredefinedInquiries', async () => {
const str = `{
"version": 3,
"inquiries": [
{
"id": 1,
"name": "foo",
"query": "select * from foo",
"viewType": "chart",
"viewOptions": [],
"createdAt": "2020-11-03T14:17:49.524Z"
},
{
"id": 2,
"name": "boo",
"query": "select * from boo",
"viewType": "graph",
"viewOptions": {},
"createdAt": "2020-11-03T14:17:49.524Z"
}
]
}
`
sinon.stub(fu, 'readFile').returns(Promise.resolve(new Response(str)))
const inquiries = await storedInquiries.readPredefinedInquiries()
expect(fu.readFile.calledOnceWith('./inquiries.json')).to.equal(true)
expect(inquiries).to.eql([
{
id: 1,
name: 'foo',
query: 'select * from foo',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T14:17:49.524Z'
},
{
id: 2,
name: 'boo',
query: 'select * from boo',
viewType: 'graph',
viewOptions: {},
createdAt: '2020-11-03T14:17:49.524Z'
}
])
})

View File

@@ -2,7 +2,7 @@ import { expect } from 'chai'
import sinon from 'sinon'
import { mount, shallowMount } from '@vue/test-utils'
import { createStore } from 'vuex'
import Inquiries from '@/views/MainView/Inquiries'
import Inquiries from '@/views/Inquiries'
import storedInquiries from '@/lib/storedInquiries'
import mutations from '@/store/mutations'
import actions from '@/store/actions'

View File

@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'
import actions from '@/store/actions'
import mutations from '@/store/mutations'
import { createStore } from 'vuex'
import Workspace from '@/views/MainView/Workspace'
import Workspace from '@/views/Workspace'
describe('Workspace.vue', () => {
it('Creates a tab with example if schema is empty', () => {