@antelopejs/dms-database 0.0.14 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/db/models/query_history.model.d.ts +1 -0
- package/dist/db/models/query_history.model.js +6 -0
- package/dist/db/models/query_history.model.js.map +1 -1
- package/dist/db/tables/query_history.table.d.ts +3 -0
- package/dist/db/tables/query_history.table.js +12 -0
- package/dist/db/tables/query_history.table.js.map +1 -1
- package/dist/db/tables/saved_queries.table.d.ts +1 -0
- package/dist/db/tables/saved_queries.table.js +4 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -1
- package/dist/i18n/messages.d.ts +39 -0
- package/dist/i18n/messages.js +64 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +11 -0
- package/dist/module.js +55 -0
- package/dist/module.js.map +1 -0
- package/dist/pages/data.d.ts +1 -1
- package/dist/pages/data.js +9 -6
- package/dist/pages/data.js.map +1 -1
- package/dist/pages/diagram.d.ts +9 -0
- package/dist/pages/diagram.js +45 -0
- package/dist/pages/diagram.js.map +1 -0
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/overview.d.ts +4 -1
- package/dist/pages/overview.js +113 -6
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/query.d.ts +1 -1
- package/dist/pages/query.js +22 -6
- package/dist/pages/query.js.map +1 -1
- package/dist/pages/schemas.d.ts +2 -1
- package/dist/pages/schemas.js +143 -6
- package/dist/pages/schemas.js.map +1 -1
- package/dist/routes/data.d.ts +8 -0
- package/dist/routes/data.js +84 -21
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/overview.d.ts +12 -0
- package/dist/routes/overview.js +55 -0
- package/dist/routes/overview.js.map +1 -0
- package/dist/routes/query.d.ts +5 -1
- package/dist/routes/query.js +162 -27
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/tables.d.ts +15 -12
- package/dist/routes/tables.js +95 -55
- package/dist/routes/tables.js.map +1 -1
- package/dist/service/catalog.d.ts +3 -0
- package/dist/service/catalog.js +26 -0
- package/dist/service/catalog.js.map +1 -0
- package/dist/service/health.d.ts +3 -3
- package/dist/service/health.js +17 -14
- package/dist/service/health.js.map +1 -1
- package/dist/service/introspect.d.ts +3 -1
- package/dist/service/introspect.js +131 -43
- package/dist/service/introspect.js.map +1 -1
- package/dist/service/overview.d.ts +9 -0
- package/dist/service/overview.js +113 -0
- package/dist/service/overview.js.map +1 -0
- package/dist/service/queryInspection.d.ts +15 -0
- package/dist/service/queryInspection.js +107 -0
- package/dist/service/queryInspection.js.map +1 -0
- package/dist/service/references.d.ts +9 -0
- package/dist/service/references.js +52 -0
- package/dist/service/references.js.map +1 -0
- package/dist/service/schemaFilters.d.ts +26 -0
- package/dist/service/schemaFilters.js +42 -0
- package/dist/service/schemaFilters.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +2 -0
- package/dist/service/stagedSerialization.js +38 -10
- package/dist/service/stagedSerialization.js.map +1 -1
- package/dist/service/types.d.ts +11 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/constants.js +3 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/utils/query.d.ts +0 -2
- package/dist/utils/query.js +0 -20
- package/dist/utils/query.js.map +1 -1
- package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
- package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
- package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
- package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
- package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
- package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
- package/frontend-vue/app/build/data/cellValues.ts +132 -0
- package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
- package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
- package/frontend-vue/app/build/data/recentTables.ts +48 -0
- package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
- package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
- package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
- package/frontend-vue/app/build/diagram/autosave.ts +72 -0
- package/frontend-vue/app/build/diagram/graph.ts +585 -0
- package/frontend-vue/app/build/diagram/history.ts +178 -0
- package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
- package/frontend-vue/app/build/diagram/sync.ts +137 -0
- package/frontend-vue/app/build/query/completion.ts +100 -0
- package/frontend-vue/app/build/query/csv.ts +32 -0
- package/frontend-vue/app/build/query/examples.ts +38 -0
- package/frontend-vue/app/build/query/runs.ts +168 -0
- package/frontend-vue/app/build/query/tableColumns.ts +35 -0
- package/frontend-vue/app/build/query/target.ts +143 -0
- package/frontend-vue/app/build/schemas/filterState.ts +128 -0
- package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
- package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
- package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
- package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
- package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
- package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
- package/frontend-vue/app/components/DataBrowser.vue +126 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
- package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
- package/frontend-vue/app/components/DataFilterBar.vue +163 -0
- package/frontend-vue/app/components/DataGrid.vue +737 -499
- package/frontend-vue/app/components/DataGridCell.vue +125 -77
- package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
- package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
- package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
- package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
- package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
- package/frontend-vue/app/components/InstancePicker.vue +195 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
- package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
- package/frontend-vue/app/components/QueryConsole.vue +726 -0
- package/frontend-vue/app/components/QueryLibrary.vue +314 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
- package/frontend-vue/app/components/RowDrawer.vue +520 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
- package/frontend-vue/app/components/SchemaPicker.vue +86 -0
- package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
- package/frontend-vue/app/components/TableInspector.vue +741 -0
- package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
- package/frontend-vue/dms.frontend.ts +5 -14
- package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
- package/frontend-vue/package.json +2 -1
- package/frontend-vue/pnpm-lock.yaml +2601 -858
- package/frontend-vue/pnpm-workspace.yaml +1 -1
- package/frontend-vue/tests/dataBrowser.test.ts +206 -0
- package/frontend-vue/tests/diagram.test.ts +516 -0
- package/frontend-vue/tests/helpers.test.ts +205 -0
- package/frontend-vue/tests/instanceOptions.test.ts +53 -0
- package/frontend-vue/tests/locales.test.ts +56 -0
- package/frontend-vue/tests/persistence.test.ts +106 -5
- package/frontend-vue/tests/pickerMenus.test.ts +254 -0
- package/frontend-vue/tests/queryConsole.test.ts +282 -0
- package/frontend-vue/tests/queryRuntime.test.ts +2 -2
- package/frontend-vue/tests/registration.test.ts +36 -25
- package/frontend-vue/tests/schemasFilters.test.ts +115 -0
- package/package.json +7 -7
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
- package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
- package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
- package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
- package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
- package/frontend-vue/app/composables/useQueryStore.ts +0 -117
- package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
- package/frontend-vue/app/custom-pages/database/data.vue +0 -41
- package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
- package/frontend-vue/app/custom-pages/database/query.vue +0 -436
- package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
- package/frontend-vue/app/utils/dmsTableView.ts +0 -83
- package/frontend-vue/app/utils/relativeTime.ts +0 -18
|
@@ -1,204 +1,634 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import { useClipboard } from '@vueuse/core'
|
|
3
|
+
import { downloadText, resultColumns, toCsv } from '../build/query/csv'
|
|
4
|
+
import {
|
|
5
|
+
filledColumns,
|
|
6
|
+
numericColumns as numericOnly,
|
|
7
|
+
} from '../build/query/tableColumns'
|
|
8
|
+
import { latestOrderField } from '../build/query/examples'
|
|
9
|
+
import {
|
|
10
|
+
closestName,
|
|
11
|
+
unknownName,
|
|
12
|
+
type UnknownKind,
|
|
13
|
+
type UnknownName,
|
|
14
|
+
} from '../build/query/runs'
|
|
15
|
+
import {
|
|
16
|
+
describeTarget,
|
|
17
|
+
rowInstance,
|
|
18
|
+
type QueryTarget,
|
|
19
|
+
} from '../build/query/target'
|
|
20
|
+
import type { SchemaSummary } from '../build/composables/useDatabaseSchemas'
|
|
21
|
+
import type { ExecuteResult } from '../build/composables/useQueryStore'
|
|
22
|
+
import {
|
|
23
|
+
DATABASE_PAGES,
|
|
24
|
+
tableAccess,
|
|
25
|
+
tableLink,
|
|
26
|
+
} from '../build/utils/databaseLinks'
|
|
3
27
|
|
|
4
|
-
|
|
28
|
+
// The result of the last run (D-13): what it read or changed and where, the
|
|
29
|
+
// rows as a table, JSON or a chart with picked axes, copied or exported, and
|
|
30
|
+
// whether every row is shown. A failed run says what failed and, for a
|
|
31
|
+
// mistyped table, offers the right one.
|
|
5
32
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
33
|
+
type ResultMode = 'table' | 'json' | 'chart'
|
|
34
|
+
|
|
35
|
+
export interface QueryFailure {
|
|
36
|
+
message: string
|
|
37
|
+
/** How long the server ran it; null when it never reached the server. */
|
|
38
|
+
durationMs: number | null
|
|
39
|
+
/** The unknown name the failure is about, when the console found it. */
|
|
40
|
+
unknown?: UnknownName | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A named instance holding rows of the table, with how many. */
|
|
44
|
+
export interface InstanceRows {
|
|
45
|
+
id: string
|
|
46
|
+
count: number
|
|
9
47
|
}
|
|
10
48
|
|
|
11
|
-
const props = defineProps<
|
|
49
|
+
const props = defineProps<{
|
|
50
|
+
result: ExecuteResult | null
|
|
51
|
+
failure: QueryFailure | null
|
|
52
|
+
target: QueryTarget
|
|
53
|
+
schemas: SchemaSummary[]
|
|
54
|
+
/** A run is on its way: what shows is the previous one's. */
|
|
55
|
+
running?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* Named instances with rows of a table the read found empty in the
|
|
58
|
+
* default instance.
|
|
59
|
+
*/
|
|
60
|
+
elsewhere?: InstanceRows[]
|
|
61
|
+
}>()
|
|
12
62
|
|
|
13
63
|
const emit = defineEmits<{
|
|
14
|
-
|
|
15
|
-
|
|
64
|
+
'use-source': [source: string]
|
|
65
|
+
'replace-name': [kind: UnknownKind, from: string, to: string]
|
|
66
|
+
'use-instance': [instance: string]
|
|
67
|
+
}>()
|
|
16
68
|
|
|
17
|
-
const
|
|
69
|
+
const MAX_TABLE_ROWS = 200
|
|
70
|
+
const EXAMPLE_ROWS = 10
|
|
71
|
+
const CHART_ROWS = 50
|
|
72
|
+
// Bars past this many are drawn sideways, a fixed height each, so their
|
|
73
|
+
// labels stay apart.
|
|
74
|
+
const UPRIGHT_BARS = 20
|
|
75
|
+
const BAR_HEIGHT = 22
|
|
76
|
+
const CHART_HEIGHT = 280
|
|
18
77
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
78
|
+
const { t, n } = useI18n()
|
|
79
|
+
const { copy } = useClipboard()
|
|
80
|
+
const toast = useToast()
|
|
23
81
|
|
|
82
|
+
const mode = ref<ResultMode>('table')
|
|
24
83
|
const modeItems = computed(() => [
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
84
|
+
{
|
|
85
|
+
label: t('dms_database.query.result.table'),
|
|
86
|
+
value: 'table',
|
|
87
|
+
icon: 'i-ph-table',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: t('dms_database.query.result.json'),
|
|
91
|
+
value: 'json',
|
|
92
|
+
icon: 'i-ph-brackets-curly',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: t('dms_database.query.result.chart'),
|
|
96
|
+
value: 'chart',
|
|
97
|
+
icon: 'i-ph-chart-bar',
|
|
98
|
+
},
|
|
99
|
+
])
|
|
29
100
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
101
|
+
const rows = computed(() => props.result?.rows ?? [])
|
|
102
|
+
const columns = computed(() => resultColumns(rows.value))
|
|
103
|
+
const tableColumns = computed(() => filledColumns(rows.value, columns.value))
|
|
104
|
+
const rightAligned = computed(
|
|
105
|
+
() => new Set(numericOnly(rows.value, tableColumns.value)),
|
|
106
|
+
)
|
|
107
|
+
const shownRows = computed(() => rows.value.slice(0, MAX_TABLE_ROWS))
|
|
108
|
+
const targetLabel = computed(() => describeTarget(props.target))
|
|
109
|
+
const tableCut = computed(
|
|
110
|
+
() => mode.value === 'table' && rows.value.length > MAX_TABLE_ROWS,
|
|
111
|
+
)
|
|
36
112
|
|
|
37
|
-
|
|
113
|
+
function renderCell(value: unknown): string {
|
|
114
|
+
if (value === null || value === undefined) return 'null'
|
|
115
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
116
|
+
return String(value)
|
|
117
|
+
}
|
|
38
118
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
119
|
+
// --- links to the data browser ---
|
|
120
|
+
// A row of a read across every instance opens in its own instance.
|
|
121
|
+
const browsable = computed(() =>
|
|
122
|
+
Boolean(props.target.schema && props.target.table),
|
|
123
|
+
)
|
|
42
124
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
if (typeof
|
|
46
|
-
|
|
125
|
+
function rowLink(row: Record<string, unknown>): string | null {
|
|
126
|
+
const id = row._id
|
|
127
|
+
if (!browsable.value || (typeof id !== 'string' && typeof id !== 'number'))
|
|
128
|
+
return null
|
|
129
|
+
const instance = rowInstance(props.target, row)
|
|
130
|
+
if (instance === null) return null
|
|
131
|
+
return tableLink('data', {
|
|
132
|
+
schema: props.target.schema ?? '',
|
|
133
|
+
table: props.target.table ?? '',
|
|
134
|
+
instance: instance || undefined,
|
|
135
|
+
match: { field: '_id', value: String(id) },
|
|
136
|
+
})
|
|
47
137
|
}
|
|
48
138
|
|
|
49
|
-
|
|
50
|
-
|
|
139
|
+
const tableBrowserLink = computed(() =>
|
|
140
|
+
browsable.value && props.target.instance !== '*'
|
|
141
|
+
? tableLink('data', {
|
|
142
|
+
schema: props.target.schema ?? '',
|
|
143
|
+
table: props.target.table ?? '',
|
|
144
|
+
instance: props.target.instance || undefined,
|
|
145
|
+
})
|
|
146
|
+
: null,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
// --- copy and export ---
|
|
150
|
+
async function copyJson() {
|
|
151
|
+
await copy(JSON.stringify(rows.value, null, 2))
|
|
152
|
+
toast.add({
|
|
153
|
+
title: t('dms_database.query.result.copied', rows.value.length),
|
|
154
|
+
color: 'success',
|
|
155
|
+
icon: 'i-ph-check',
|
|
156
|
+
})
|
|
51
157
|
}
|
|
52
158
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
159
|
+
function exportCsv() {
|
|
160
|
+
const name = props.target.table ?? 'query'
|
|
161
|
+
downloadText(`${name}.csv`, toCsv(rows.value), 'text/csv;charset=utf-8')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- chart with picked axes ---
|
|
165
|
+
const numericColumns = computed(() =>
|
|
166
|
+
columns.value.filter((column) =>
|
|
167
|
+
rows.value.some((row) => typeof row[column] === 'number'),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
const xColumn = ref<string | undefined>()
|
|
171
|
+
const yColumn = ref<string | undefined>()
|
|
172
|
+
|
|
173
|
+
watch(
|
|
174
|
+
columns,
|
|
175
|
+
() => {
|
|
176
|
+
yColumn.value = numericColumns.value.includes(yColumn.value ?? '')
|
|
177
|
+
? yColumn.value
|
|
178
|
+
: numericColumns.value[0]
|
|
179
|
+
xColumn.value = columns.value.includes(xColumn.value ?? '')
|
|
180
|
+
? xColumn.value
|
|
181
|
+
: columns.value.find((column) => column !== yColumn.value)
|
|
182
|
+
},
|
|
183
|
+
{ immediate: true },
|
|
184
|
+
)
|
|
73
185
|
|
|
186
|
+
const chartRows = computed(() => rows.value.slice(0, CHART_ROWS))
|
|
187
|
+
const sideways = computed(() => chartRows.value.length > UPRIGHT_BARS)
|
|
188
|
+
const chartHeight = computed(
|
|
189
|
+
() =>
|
|
190
|
+
`${sideways.value ? Math.max(CHART_HEIGHT, chartRows.value.length * BAR_HEIGHT) : CHART_HEIGHT}px`,
|
|
191
|
+
)
|
|
74
192
|
const chartDataset = computed(() => {
|
|
75
|
-
const
|
|
76
|
-
if (!
|
|
77
|
-
const label = labelColumn.value;
|
|
193
|
+
const y = yColumn.value
|
|
194
|
+
if (!y) return []
|
|
78
195
|
return [
|
|
79
196
|
{
|
|
80
|
-
name:
|
|
81
|
-
data:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
y: isNumeric(record[col]) ? (record[col] as number) : 0,
|
|
86
|
-
};
|
|
87
|
-
}),
|
|
197
|
+
name: y,
|
|
198
|
+
data: chartRows.value.map((row, index) => ({
|
|
199
|
+
x: xColumn.value ? renderCell(row[xColumn.value]) : String(index + 1),
|
|
200
|
+
y: typeof row[y] === 'number' ? (row[y] as number) : 0,
|
|
201
|
+
})),
|
|
88
202
|
},
|
|
89
|
-
]
|
|
90
|
-
})
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// --- first run and failures ---
|
|
207
|
+
const examples = computed(() => {
|
|
208
|
+
const schema = props.schemas.find((candidate) => candidate.tables.length > 0)
|
|
209
|
+
if (!schema) return []
|
|
210
|
+
const [first, second] = schema.tables
|
|
211
|
+
const firstTable = `${schema.id}.${first?.name}`
|
|
212
|
+
const access = tableAccess({ schema: schema.id, table: first?.name ?? '' })
|
|
213
|
+
// "Latest" rows only when a date orders them; else the first ones.
|
|
214
|
+
const dated = first ? latestOrderField(first) : null
|
|
215
|
+
const items = [
|
|
216
|
+
dated
|
|
217
|
+
? {
|
|
218
|
+
label: t('dms_database.query.result.example_latest', {
|
|
219
|
+
table: firstTable,
|
|
220
|
+
}),
|
|
221
|
+
source: `${access}.orderBy(${JSON.stringify(dated)}, "desc").slice(0, ${EXAMPLE_ROWS})`,
|
|
222
|
+
}
|
|
223
|
+
: {
|
|
224
|
+
label: t('dms_database.query.result.example_first', {
|
|
225
|
+
table: firstTable,
|
|
226
|
+
}),
|
|
227
|
+
source: `${access}.slice(0, ${EXAMPLE_ROWS})`,
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
const counted = second ?? first
|
|
231
|
+
if (counted) {
|
|
232
|
+
items.push({
|
|
233
|
+
label: t('dms_database.query.result.example_count', {
|
|
234
|
+
table: `${schema.id}.${counted.name}`,
|
|
235
|
+
}),
|
|
236
|
+
source: `${tableAccess({ schema: schema.id, table: counted.name })}.count()`,
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
return items
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const exampleActions = computed(() =>
|
|
243
|
+
examples.value.map((example) => ({
|
|
244
|
+
label: example.label,
|
|
245
|
+
color: 'neutral' as const,
|
|
246
|
+
variant: 'outline' as const,
|
|
247
|
+
class: 'font-mono',
|
|
248
|
+
onClick: () => emit('use-source', example.source),
|
|
249
|
+
})),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
// One button per instance holding rows: `eu · 160` reruns the read there.
|
|
253
|
+
const elsewhereActions = computed(() =>
|
|
254
|
+
(props.elsewhere ?? []).map((instance) => ({
|
|
255
|
+
label: `${instance.id} · ${n(instance.count)}`,
|
|
256
|
+
color: 'neutral' as const,
|
|
257
|
+
variant: 'outline' as const,
|
|
258
|
+
class: 'font-mono',
|
|
259
|
+
onClick: () => emit('use-instance', instance.id),
|
|
260
|
+
})),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// A mistyped schema, instance or table, and the closest name the workspace
|
|
264
|
+
// has: the console offers to use it, as for a table.
|
|
265
|
+
const mistyped = computed<UnknownName | null>(() =>
|
|
266
|
+
props.failure
|
|
267
|
+
? (props.failure.unknown ?? unknownName(props.failure.message))
|
|
268
|
+
: null,
|
|
269
|
+
)
|
|
270
|
+
const suggestion = computed(() => {
|
|
271
|
+
const unknown = mistyped.value
|
|
272
|
+
if (!unknown) return null
|
|
273
|
+
if (unknown.kind === 'schema')
|
|
274
|
+
return closestName(
|
|
275
|
+
unknown.name,
|
|
276
|
+
props.schemas.map((schema) => schema.id),
|
|
277
|
+
)
|
|
278
|
+
const schema = props.schemas.find(
|
|
279
|
+
(candidate) => candidate.id === props.target.schema,
|
|
280
|
+
)
|
|
281
|
+
if (!schema) return null
|
|
282
|
+
return closestName(
|
|
283
|
+
unknown.name,
|
|
284
|
+
unknown.kind === 'table'
|
|
285
|
+
? schema.tables.map((table) => table.name)
|
|
286
|
+
: (schema.instances ?? []),
|
|
287
|
+
)
|
|
288
|
+
})
|
|
91
289
|
</script>
|
|
92
290
|
|
|
93
291
|
<template>
|
|
94
|
-
<DmsCard>
|
|
95
|
-
|
|
96
|
-
|
|
292
|
+
<DmsCard :padded="false" class="overflow-hidden">
|
|
293
|
+
<div
|
|
294
|
+
class="border-default flex flex-wrap items-center gap-2.5 border-b px-4 py-2.5"
|
|
295
|
+
>
|
|
97
296
|
<UIcon
|
|
98
297
|
:name="
|
|
99
|
-
|
|
298
|
+
running
|
|
299
|
+
? 'i-ph-circle-notch'
|
|
300
|
+
: failure
|
|
301
|
+
? 'i-ph-x-circle'
|
|
302
|
+
: 'i-ph-check-circle'
|
|
303
|
+
"
|
|
304
|
+
:class="
|
|
305
|
+
running
|
|
306
|
+
? 'text-dimmed animate-spin'
|
|
307
|
+
: failure
|
|
308
|
+
? 'text-error'
|
|
309
|
+
: result
|
|
310
|
+
? 'text-success'
|
|
311
|
+
: 'text-dimmed'
|
|
100
312
|
"
|
|
101
|
-
:class="result?.error ? 'text-error' : 'text-success'"
|
|
102
313
|
class="size-4"
|
|
103
314
|
/>
|
|
104
|
-
<b class="text-highlighted text-
|
|
105
|
-
{{
|
|
315
|
+
<b class="text-highlighted text-sm">
|
|
316
|
+
{{ t('dms_database.query.result.title') }}
|
|
106
317
|
</b>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
318
|
+
<span v-if="running" class="text-muted text-xs" role="status">
|
|
319
|
+
{{ t('dms_database.query.result.running') }}
|
|
320
|
+
</span>
|
|
321
|
+
<template v-else-if="result">
|
|
322
|
+
<UBadge
|
|
323
|
+
:color="result.mutation ? 'info' : 'success'"
|
|
324
|
+
variant="soft"
|
|
325
|
+
size="sm"
|
|
326
|
+
class="font-mono"
|
|
327
|
+
>
|
|
328
|
+
{{
|
|
329
|
+
result.mutation
|
|
330
|
+
? t(
|
|
331
|
+
'dms_database.query.result.changed',
|
|
332
|
+
{ count: n(result.rowCount) },
|
|
333
|
+
result.rowCount,
|
|
334
|
+
)
|
|
335
|
+
: t(
|
|
336
|
+
'dms_database.query.result.rows',
|
|
337
|
+
{ count: n(result.rowCount) },
|
|
338
|
+
result.rowCount,
|
|
339
|
+
)
|
|
340
|
+
}}
|
|
110
341
|
</UBadge>
|
|
111
342
|
<span class="text-dimmed font-mono text-xs">
|
|
112
|
-
{{
|
|
343
|
+
{{ n(result.durationMs) }} ms ·
|
|
344
|
+
{{
|
|
345
|
+
result.mutation
|
|
346
|
+
? t('dms_database.query.result.wrote')
|
|
347
|
+
: t('dms_database.query.result.read_only')
|
|
348
|
+
}}
|
|
349
|
+
<template v-if="targetLabel">· {{ targetLabel }}</template>
|
|
113
350
|
</span>
|
|
114
351
|
</template>
|
|
115
|
-
<DmsSegmented
|
|
116
|
-
v-model="internalMode"
|
|
117
|
-
:items="modeItems"
|
|
118
|
-
:aria-label="$t('dms_database.query.result')"
|
|
119
|
-
class="ml-auto"
|
|
120
|
-
/>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<!-- body -->
|
|
124
|
-
<div class="min-h-64">
|
|
125
352
|
<div
|
|
126
|
-
v-if="!
|
|
127
|
-
class="flex flex-
|
|
353
|
+
v-if="result && rows.length > 0 && !running"
|
|
354
|
+
class="ml-auto flex flex-wrap items-center gap-1.5"
|
|
128
355
|
>
|
|
129
|
-
<
|
|
130
|
-
|
|
356
|
+
<DmsSegmented
|
|
357
|
+
v-model="mode"
|
|
358
|
+
:items="modeItems"
|
|
359
|
+
size="sm"
|
|
360
|
+
:aria-label="t('dms_database.query.result.title')"
|
|
361
|
+
/>
|
|
362
|
+
<UTooltip :text="t('dms_database.query.result.copy')">
|
|
363
|
+
<UButton
|
|
364
|
+
icon="i-ph-copy"
|
|
365
|
+
color="neutral"
|
|
366
|
+
variant="ghost"
|
|
367
|
+
size="sm"
|
|
368
|
+
:aria-label="t('dms_database.query.result.copy')"
|
|
369
|
+
@click="copyJson"
|
|
370
|
+
/>
|
|
371
|
+
</UTooltip>
|
|
372
|
+
<UButton
|
|
373
|
+
icon="i-ph-download-simple"
|
|
374
|
+
color="neutral"
|
|
375
|
+
variant="outline"
|
|
376
|
+
size="sm"
|
|
377
|
+
:label="t('dms_database.query.result.export')"
|
|
378
|
+
@click="exportCsv"
|
|
379
|
+
/>
|
|
131
380
|
</div>
|
|
381
|
+
</div>
|
|
132
382
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
383
|
+
<div
|
|
384
|
+
class="min-h-56 transition-opacity"
|
|
385
|
+
:class="running ? 'pointer-events-none opacity-50' : ''"
|
|
386
|
+
:aria-busy="running"
|
|
387
|
+
>
|
|
388
|
+
<div v-if="failure" class="grid gap-3 p-4">
|
|
389
|
+
<div class="border-error/40 bg-error/10 rounded-lg border px-4 py-3">
|
|
390
|
+
<p class="text-error text-sm font-semibold">
|
|
391
|
+
{{ t('dms_database.query.result.failed_title') }}
|
|
392
|
+
</p>
|
|
393
|
+
<p class="text-muted text-xs">
|
|
394
|
+
{{
|
|
395
|
+
failure.durationMs === null
|
|
396
|
+
? t('dms_database.query.result.failed_local')
|
|
397
|
+
: t('dms_database.query.result.failed_detail', {
|
|
398
|
+
ms: n(failure.durationMs),
|
|
399
|
+
})
|
|
400
|
+
}}
|
|
401
|
+
</p>
|
|
402
|
+
<p
|
|
403
|
+
class="text-toned mt-2 whitespace-pre-wrap font-mono text-[12.5px]"
|
|
404
|
+
>
|
|
405
|
+
{{ failure.message }}
|
|
406
|
+
</p>
|
|
407
|
+
</div>
|
|
408
|
+
<div v-if="mistyped" class="flex flex-wrap gap-2">
|
|
409
|
+
<UButton
|
|
410
|
+
v-if="suggestion"
|
|
411
|
+
size="sm"
|
|
412
|
+
icon="i-ph-arrow-right"
|
|
413
|
+
:label="
|
|
414
|
+
t('dms_database.query.result.replace', { table: suggestion })
|
|
415
|
+
"
|
|
416
|
+
@click="
|
|
417
|
+
emit('replace-name', mistyped.kind, mistyped.name, suggestion)
|
|
418
|
+
"
|
|
419
|
+
/>
|
|
420
|
+
<UButton
|
|
421
|
+
v-if="target.schema && mistyped.kind === 'table'"
|
|
422
|
+
size="sm"
|
|
423
|
+
color="neutral"
|
|
424
|
+
variant="ghost"
|
|
425
|
+
:label="
|
|
426
|
+
t('dms_database.query.result.see_tables', {
|
|
427
|
+
schema: target.schema,
|
|
428
|
+
})
|
|
429
|
+
"
|
|
430
|
+
:to="`${DATABASE_PAGES.schemas}?tab=schema-${target.schema}`"
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div v-else-if="!result" class="grid place-items-center p-8">
|
|
436
|
+
<DmsEmptyState
|
|
437
|
+
icon="i-ph-play"
|
|
438
|
+
:title="t('dms_database.query.result.first_title')"
|
|
439
|
+
:description="
|
|
440
|
+
examples.length
|
|
441
|
+
? t('dms_database.query.result.first_description')
|
|
442
|
+
: undefined
|
|
443
|
+
"
|
|
444
|
+
:actions="exampleActions"
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div v-else-if="rows.length === 0" class="grid place-items-center p-8">
|
|
449
|
+
<DmsEmptyState
|
|
450
|
+
v-if="!result.mutation && elsewhereActions.length"
|
|
451
|
+
size="sm"
|
|
452
|
+
icon="i-ph-stack"
|
|
453
|
+
:title="t('dms_database.query.result.elsewhere_title')"
|
|
454
|
+
:description="t('dms_database.query.result.elsewhere_description')"
|
|
455
|
+
:actions="elsewhereActions"
|
|
456
|
+
/>
|
|
457
|
+
<DmsEmptyState
|
|
458
|
+
v-else
|
|
459
|
+
size="sm"
|
|
460
|
+
:title="
|
|
461
|
+
result.mutation
|
|
462
|
+
? t('dms_database.query.result.done')
|
|
463
|
+
: t('dms_database.query.result.no_rows')
|
|
464
|
+
"
|
|
465
|
+
/>
|
|
141
466
|
</div>
|
|
142
467
|
|
|
143
|
-
|
|
144
|
-
<div v-else-if="internalMode === 'table'" class="overflow-x-auto -mx-1">
|
|
468
|
+
<div v-else-if="mode === 'table'" class="max-h-[28rem] overflow-auto">
|
|
145
469
|
<table class="w-full text-sm">
|
|
146
|
-
<thead>
|
|
147
|
-
<tr class="border-
|
|
470
|
+
<thead class="bg-elevated sticky top-0">
|
|
471
|
+
<tr class="border-default border-b text-left">
|
|
148
472
|
<th
|
|
149
|
-
v-for="column in
|
|
473
|
+
v-for="column in tableColumns"
|
|
150
474
|
:key="column"
|
|
151
|
-
class="px-3 py-2 font-mono text-xs font-semibold
|
|
475
|
+
class="text-dimmed whitespace-nowrap px-3 py-2 font-mono text-xs font-semibold"
|
|
476
|
+
:class="rightAligned.has(column) ? 'text-right' : ''"
|
|
152
477
|
>
|
|
153
478
|
{{ column }}
|
|
154
479
|
</th>
|
|
480
|
+
<th v-if="browsable" class="w-8" />
|
|
155
481
|
</tr>
|
|
156
482
|
</thead>
|
|
157
483
|
<tbody>
|
|
158
484
|
<tr
|
|
159
|
-
v-for="(row, rowIndex) in
|
|
485
|
+
v-for="(row, rowIndex) in shownRows"
|
|
160
486
|
:key="rowIndex"
|
|
161
|
-
class="
|
|
487
|
+
class="group/result border-default/60 hover:bg-elevated/40 border-b last:border-0"
|
|
162
488
|
>
|
|
163
489
|
<td
|
|
164
|
-
v-for="
|
|
490
|
+
v-for="column in tableColumns"
|
|
165
491
|
:key="column"
|
|
166
|
-
class="px-3 py-
|
|
167
|
-
:class="
|
|
492
|
+
class="max-w-80 truncate whitespace-nowrap px-3 py-1.5 font-mono text-[12.5px]"
|
|
493
|
+
:class="[
|
|
494
|
+
row[column] === null || row[column] === undefined
|
|
495
|
+
? 'text-dimmed'
|
|
496
|
+
: 'text-toned',
|
|
497
|
+
typeof row[column] === 'number'
|
|
498
|
+
? 'text-right tabular-nums'
|
|
499
|
+
: '',
|
|
500
|
+
]"
|
|
168
501
|
>
|
|
169
|
-
{{ renderCell(
|
|
502
|
+
{{ renderCell(row[column]) }}
|
|
503
|
+
</td>
|
|
504
|
+
<td v-if="browsable" class="px-1">
|
|
505
|
+
<UButton
|
|
506
|
+
v-if="rowLink(row)"
|
|
507
|
+
:to="rowLink(row) ?? undefined"
|
|
508
|
+
icon="i-ph-arrow-square-out"
|
|
509
|
+
color="neutral"
|
|
510
|
+
variant="ghost"
|
|
511
|
+
size="xs"
|
|
512
|
+
class="opacity-0 focus-visible:opacity-100 group-hover/result:opacity-100"
|
|
513
|
+
:aria-label="t('dms_database.query.result.open_row')"
|
|
514
|
+
:title="t('dms_database.query.result.open_row')"
|
|
515
|
+
/>
|
|
170
516
|
</td>
|
|
171
517
|
</tr>
|
|
172
518
|
</tbody>
|
|
173
519
|
</table>
|
|
174
520
|
</div>
|
|
175
521
|
|
|
176
|
-
<!-- json -->
|
|
177
522
|
<pre
|
|
178
|
-
v-else-if="
|
|
179
|
-
class="text-
|
|
180
|
-
>{{
|
|
523
|
+
v-else-if="mode === 'json'"
|
|
524
|
+
class="text-toned max-h-[28rem] overflow-auto whitespace-pre p-4 font-mono text-xs"
|
|
525
|
+
>{{ JSON.stringify(rows, null, 2) }}</pre
|
|
181
526
|
>
|
|
182
527
|
|
|
183
|
-
|
|
184
|
-
|
|
528
|
+
<div v-else class="grid gap-3 p-4">
|
|
529
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
530
|
+
<DmsEyebrow :label="t('dms_database.query.result.bars')" />
|
|
531
|
+
<USelect
|
|
532
|
+
v-model="xColumn"
|
|
533
|
+
:items="columns"
|
|
534
|
+
size="xs"
|
|
535
|
+
class="min-w-36 font-mono"
|
|
536
|
+
:aria-label="t('dms_database.query.result.x_axis')"
|
|
537
|
+
>
|
|
538
|
+
<template #leading>
|
|
539
|
+
<span class="text-dimmed text-[11px]">x</span>
|
|
540
|
+
</template>
|
|
541
|
+
</USelect>
|
|
542
|
+
<USelect
|
|
543
|
+
v-model="yColumn"
|
|
544
|
+
:items="numericColumns"
|
|
545
|
+
size="xs"
|
|
546
|
+
class="min-w-36 font-mono"
|
|
547
|
+
:aria-label="t('dms_database.query.result.y_axis')"
|
|
548
|
+
>
|
|
549
|
+
<template #leading>
|
|
550
|
+
<span class="text-dimmed text-[11px]">y</span>
|
|
551
|
+
</template>
|
|
552
|
+
</USelect>
|
|
553
|
+
<span v-if="rows.length > CHART_ROWS" class="text-dimmed text-xs">
|
|
554
|
+
{{
|
|
555
|
+
t('dms_database.query.result.chart_first', {
|
|
556
|
+
count: CHART_ROWS,
|
|
557
|
+
total: n(rows.length),
|
|
558
|
+
})
|
|
559
|
+
}}
|
|
560
|
+
</span>
|
|
561
|
+
</div>
|
|
185
562
|
<DmsChart
|
|
186
|
-
v-if="
|
|
187
|
-
|
|
563
|
+
v-if="yColumn"
|
|
564
|
+
:key="sideways ? 'sideways' : 'upright'"
|
|
565
|
+
page-id="query"
|
|
566
|
+
component-id="query-result-chart"
|
|
567
|
+
type="bar"
|
|
188
568
|
:static-dataset="chartDataset"
|
|
189
569
|
color="primary"
|
|
190
|
-
height="
|
|
570
|
+
:height="chartHeight"
|
|
571
|
+
:orientation="sideways ? 'horizontal' : 'vertical'"
|
|
191
572
|
:show-legend="false"
|
|
192
573
|
xaxis-type="category"
|
|
193
574
|
/>
|
|
194
|
-
<
|
|
575
|
+
<DmsEmptyState
|
|
195
576
|
v-else
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
</div>
|
|
577
|
+
size="sm"
|
|
578
|
+
icon="i-ph-chart-bar"
|
|
579
|
+
:title="t('dms_database.query.result.chart_unavailable')"
|
|
580
|
+
/>
|
|
201
581
|
</div>
|
|
202
582
|
</div>
|
|
583
|
+
|
|
584
|
+
<div
|
|
585
|
+
v-if="result && rows.length > 0"
|
|
586
|
+
class="border-default text-muted flex flex-wrap items-center gap-2 border-t px-4 py-2 text-xs"
|
|
587
|
+
>
|
|
588
|
+
<UIcon
|
|
589
|
+
:name="result.truncated ? 'i-ph-warning' : 'i-ph-info'"
|
|
590
|
+
:class="result.truncated ? 'text-warning' : ''"
|
|
591
|
+
class="size-3.5"
|
|
592
|
+
/>
|
|
593
|
+
<!-- The server cut the result and the table shows fewer still: both
|
|
594
|
+
limits are said. -->
|
|
595
|
+
<span v-if="result.truncated">
|
|
596
|
+
{{
|
|
597
|
+
t('dms_database.query.result.truncated', {
|
|
598
|
+
shown: n(rows.length),
|
|
599
|
+
total: n(result.rowCount),
|
|
600
|
+
})
|
|
601
|
+
}}
|
|
602
|
+
</span>
|
|
603
|
+
<span v-if="tableCut">
|
|
604
|
+
{{
|
|
605
|
+
t('dms_database.query.result.table_first', {
|
|
606
|
+
shown: n(MAX_TABLE_ROWS),
|
|
607
|
+
total: n(rows.length),
|
|
608
|
+
})
|
|
609
|
+
}}
|
|
610
|
+
</span>
|
|
611
|
+
<span v-if="!result.truncated && !tableCut">
|
|
612
|
+
{{
|
|
613
|
+
t(
|
|
614
|
+
'dms_database.query.result.all_shown',
|
|
615
|
+
{ count: n(rows.length) },
|
|
616
|
+
rows.length,
|
|
617
|
+
)
|
|
618
|
+
}}
|
|
619
|
+
</span>
|
|
620
|
+
<UButton
|
|
621
|
+
v-if="tableBrowserLink"
|
|
622
|
+
class="ml-auto"
|
|
623
|
+
size="xs"
|
|
624
|
+
color="neutral"
|
|
625
|
+
variant="ghost"
|
|
626
|
+
icon="i-ph-rows"
|
|
627
|
+
:to="tableBrowserLink"
|
|
628
|
+
:label="
|
|
629
|
+
t('dms_database.query.result.open_table', { target: targetLabel })
|
|
630
|
+
"
|
|
631
|
+
/>
|
|
632
|
+
</div>
|
|
203
633
|
</DmsCard>
|
|
204
634
|
</template>
|