@antelopejs/dms-database 0.1.2 → 0.2.1
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 +73 -49
- package/dist/db/models/diagram_layouts.model.d.ts +1 -0
- package/dist/db/models/diagram_notes.model.d.ts +1 -0
- package/dist/db/models/query_history.model.d.ts +1 -0
- package/dist/db/models/saved_queries.model.d.ts +1 -0
- package/dist/routes/data.d.ts +1 -3
- package/dist/routes/data.js +6 -1
- package/dist/routes/data.js.map +1 -1
- package/frontend-vue/app/build/composables/useDataBrowserGrid.ts +4 -38
- package/frontend-vue/app/build/composables/useDatabaseHealth.ts +7 -4
- package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +1 -2
- package/frontend-vue/app/build/composables/useDiagramPersistence.ts +49 -16
- package/frontend-vue/app/build/composables/useQueryStore.ts +59 -27
- package/frontend-vue/app/build/composables/useTableCounts.ts +50 -0
- package/frontend-vue/app/build/data/stagedWrites.ts +46 -6
- package/frontend-vue/app/build/data/useStagedSave.ts +42 -12
- package/frontend-vue/app/build/query/runs.ts +7 -27
- package/frontend-vue/app/build/schemas/useSchemasListing.ts +10 -33
- package/frontend-vue/app/components/ConnectionStatus.vue +14 -11
- package/frontend-vue/app/components/DataBrowserSidebar.vue +20 -42
- package/frontend-vue/app/components/DataBrowserTabBar.vue +1 -1
- package/frontend-vue/app/components/DataGrid.vue +73 -91
- package/frontend-vue/app/components/InstancePicker.vue +63 -55
- package/frontend-vue/app/components/QueryConsole.vue +47 -37
- package/frontend-vue/app/components/QueryLibrary.vue +20 -17
- package/frontend-vue/app/components/RowDrawer.vue +15 -18
- package/frontend-vue/app/components/SchemaDiagram.vue +7 -7
- package/frontend-vue/app/components/TableInspector.vue +116 -84
- package/frontend-vue/package.json +4 -3
- package/frontend-vue/pnpm-lock.yaml +1388 -2803
- package/frontend-vue/tests/dataBrowser.test.ts +59 -1
- package/frontend-vue/tests/fixtures/inertia.ts +8 -0
- package/frontend-vue/tests/fixtures/serverRender.ts +219 -0
- package/frontend-vue/tests/fixtures/vueI18n.ts +4 -0
- package/frontend-vue/tests/helpers.test.ts +1 -0
- package/frontend-vue/tests/locales.test.ts +24 -0
- package/frontend-vue/tests/onDemandReads.test.ts +106 -0
- package/frontend-vue/tests/serverRender.test.ts +235 -0
- package/frontend-vue/vitest.config.ts +24 -0
- package/package.json +31 -14
- package/skills/dms-database/SKILL.md +35 -0
|
@@ -8,14 +8,14 @@ import { useDatabaseSchemas } from '../build/composables/useDatabaseSchemas'
|
|
|
8
8
|
import {
|
|
9
9
|
GRID_PAGE_SIZES,
|
|
10
10
|
browseListQuery,
|
|
11
|
-
browseParamsKey,
|
|
12
11
|
browseSelectionQuery,
|
|
13
12
|
useDataBrowserGrid,
|
|
14
13
|
} from '../build/composables/useDataBrowserGrid'
|
|
15
14
|
import { onKeyStroke, useEventListener } from '@vueuse/core'
|
|
16
15
|
import { parseDraft } from '../build/data/cellValues'
|
|
17
16
|
import { useStagedEdits } from '../build/data/stagedEdits'
|
|
18
|
-
import {
|
|
17
|
+
import { mayMoveRows, withWrittenRow } from '../build/data/stagedWrites'
|
|
18
|
+
import { type SavedChanges, useStagedSave } from '../build/data/useStagedSave'
|
|
19
19
|
import type { TableSummary } from '../build/composables/useDatabaseSchemas'
|
|
20
20
|
import type { BrowserTab } from '../build/composables/useDataBrowserTabs'
|
|
21
21
|
import type {
|
|
@@ -34,6 +34,8 @@ import RowDrawer from './RowDrawer.vue'
|
|
|
34
34
|
// The grid of one open table. Rows are read a page at a time; a committed
|
|
35
35
|
// cell is staged, not written (D-02): the save bar offers a review of every
|
|
36
36
|
// change before it is written, then the save and its undo for a few seconds.
|
|
37
|
+
// A row written shows as the server answered it, in place: the page is read
|
|
38
|
+
// again only when the write may move rows in or out of it.
|
|
37
39
|
|
|
38
40
|
const BROWSE_ENDPOINT = '/api/database/browse'
|
|
39
41
|
// Browse-only columns: the source instance of a cross-instance read.
|
|
@@ -63,7 +65,6 @@ const props = defineProps<{
|
|
|
63
65
|
}>()
|
|
64
66
|
|
|
65
67
|
const { t, n } = useI18n()
|
|
66
|
-
const { $authFetch } = useAuthFetch()
|
|
67
68
|
const router = useDmsRouter()
|
|
68
69
|
const grid = useDataBrowserGrid()
|
|
69
70
|
const staged = useStagedEdits()
|
|
@@ -75,12 +76,23 @@ const state = grid.getState(props.tab.id)
|
|
|
75
76
|
const readOnly = computed(() => props.tab.instance === CROSS_INSTANCE_VALUE)
|
|
76
77
|
|
|
77
78
|
// --- data loading ---
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const selection = computed(() => browseSelectionQuery(props.tab))
|
|
80
|
+
// The columns sampled from the table: the table alone decides them. A
|
|
81
|
+
// failure leaves the grid on the introspected fields.
|
|
82
|
+
const columnsRead = useApiData<ColumnsResponse>(`${BROWSE_ENDPOINT}/columns`, {
|
|
83
|
+
query: selection,
|
|
84
|
+
})
|
|
85
|
+
// A page of rows, read again as the selection, the page, the sort, the search
|
|
86
|
+
// or the filters change; the previous page stays on screen meanwhile.
|
|
87
|
+
const listRead = useApiData<ListResponse>(`${BROWSE_ENDPOINT}/list`, {
|
|
88
|
+
query: () => ({ ...selection.value, ...browseListQuery(state) }),
|
|
89
|
+
})
|
|
90
|
+
const loading = computed(() => listRead.pending.value)
|
|
91
|
+
const loadError = computed(() => {
|
|
92
|
+
const error = listRead.error.value
|
|
93
|
+
if (!error) return null
|
|
94
|
+
return error instanceof Error ? error.message : String(error)
|
|
95
|
+
})
|
|
84
96
|
|
|
85
97
|
function columnsOf(
|
|
86
98
|
meta: ColumnsResponse | null,
|
|
@@ -96,90 +108,36 @@ function columnsOf(
|
|
|
96
108
|
}))
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
: Promise.resolve(columnsMeta.value),
|
|
110
|
-
$authFetch<ListResponse>(`${BROWSE_ENDPOINT}/list`, {
|
|
111
|
-
query: { ...selection, ...browseListQuery(state) },
|
|
112
|
-
}),
|
|
113
|
-
])
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function load(force = false) {
|
|
117
|
-
const key = browseParamsKey(props.tab, state)
|
|
118
|
-
const cached = grid.getCache(props.tab.id)
|
|
119
|
-
if (!force && cached && cached.paramsKey === key) {
|
|
120
|
-
// Retire a request in flight: its late answer would overwrite the cache
|
|
121
|
-
// with rows for parameters the user has left.
|
|
122
|
-
requestSeq += 1
|
|
123
|
-
data.value = cached
|
|
124
|
-
loadError.value = null
|
|
125
|
-
loading.value = false
|
|
126
|
-
return
|
|
127
|
-
}
|
|
128
|
-
const seq = ++requestSeq
|
|
129
|
-
loading.value = true
|
|
130
|
-
loadError.value = null
|
|
131
|
-
try {
|
|
132
|
-
const [columnsRes, listRes] = await fetchPage(force)
|
|
133
|
-
if (seq !== requestSeq) return
|
|
134
|
-
if (columnsRes) columnsMeta.value = columnsRes
|
|
135
|
-
const primaryKey =
|
|
136
|
-
listRes.primaryKey ?? columnsMeta.value?.primaryKey ?? null
|
|
137
|
-
const next: GridData = {
|
|
138
|
-
columns: columnsOf(columnsMeta.value, primaryKey),
|
|
139
|
-
rows: listRes.results ?? [],
|
|
140
|
-
total: listRes.total ?? 0,
|
|
141
|
-
primaryKey,
|
|
142
|
-
paramsKey: key,
|
|
143
|
-
}
|
|
144
|
-
grid.setCache(props.tab.id, next)
|
|
145
|
-
data.value = next
|
|
146
|
-
if (
|
|
147
|
-
editing.value &&
|
|
148
|
-
rowIdOf(next.rows[editing.value.r] ?? {}) !== editing.value.rowId
|
|
149
|
-
) {
|
|
150
|
-
cancelEdit()
|
|
151
|
-
}
|
|
152
|
-
// Rows deleted elsewhere can leave the page past the end.
|
|
153
|
-
if (next.total > 0 && state.page * state.pageSize >= next.total) {
|
|
154
|
-
state.page = Math.max(0, Math.ceil(next.total / state.pageSize) - 1)
|
|
155
|
-
}
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (seq !== requestSeq) return
|
|
158
|
-
loadError.value = error instanceof Error ? error.message : String(error)
|
|
159
|
-
} finally {
|
|
160
|
-
if (seq === requestSeq) loading.value = false
|
|
111
|
+
const data = computed<GridData | null>(() => {
|
|
112
|
+
const list = listRead.data.value
|
|
113
|
+
if (!list) return null
|
|
114
|
+
const meta = columnsRead.data.value
|
|
115
|
+
const primaryKey = list.primaryKey ?? meta?.primaryKey ?? null
|
|
116
|
+
return {
|
|
117
|
+
columns: columnsOf(meta, primaryKey),
|
|
118
|
+
rows: list.results ?? [],
|
|
119
|
+
total: list.total ?? 0,
|
|
120
|
+
primaryKey,
|
|
161
121
|
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
watch(
|
|
165
|
-
() => browseParamsKey(props.tab, state),
|
|
166
|
-
() => load(),
|
|
167
|
-
{ immediate: true },
|
|
168
|
-
)
|
|
122
|
+
})
|
|
169
123
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
124
|
+
watch(data, (next) => {
|
|
125
|
+
if (!next) return
|
|
126
|
+
if (
|
|
127
|
+
editing.value &&
|
|
128
|
+
rowIdOf(next.rows[editing.value.r] ?? {}) !== editing.value.rowId
|
|
129
|
+
) {
|
|
130
|
+
cancelEdit()
|
|
131
|
+
}
|
|
132
|
+
// Rows deleted elsewhere can leave the page past the end.
|
|
133
|
+
if (next.total > 0 && state.page * state.pageSize >= next.total) {
|
|
134
|
+
state.page = Math.max(0, Math.ceil(next.total / state.pageSize) - 1)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
179
137
|
|
|
180
138
|
function reload() {
|
|
181
|
-
|
|
182
|
-
|
|
139
|
+
void columnsRead.refresh()
|
|
140
|
+
void listRead.refresh()
|
|
183
141
|
}
|
|
184
142
|
|
|
185
143
|
const columns = computed(() => data.value?.columns ?? [])
|
|
@@ -237,7 +195,6 @@ watch(searchDraft, (value) => {
|
|
|
237
195
|
})
|
|
238
196
|
onBeforeUnmount(() => {
|
|
239
197
|
clearTimeout(searchTimer)
|
|
240
|
-
requestSeq += 1
|
|
241
198
|
const value = searchDraft.value.trim()
|
|
242
199
|
if (value !== state.search) {
|
|
243
200
|
state.search = value
|
|
@@ -492,6 +449,30 @@ const pendingCount = computed(() => staged.count(props.tab.id))
|
|
|
492
449
|
const changedFields = computed(() => [
|
|
493
450
|
...new Set(staged.list(props.tab.id).map((change) => change.field)),
|
|
494
451
|
])
|
|
452
|
+
// The page on screen, with a row written in place of the one read: the read's
|
|
453
|
+
// data is replaced (it is shallow), so the cell shows the stored value at once.
|
|
454
|
+
function showWritten(tab: BrowserTab, row: Record<string, unknown>) {
|
|
455
|
+
const list = listRead.data.value
|
|
456
|
+
if (tab.id !== props.tab.id || !list) return
|
|
457
|
+
listRead.data.value = {
|
|
458
|
+
...list,
|
|
459
|
+
results: withWrittenRow(list.results ?? [], row, rowIdOf),
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// A save may change which rows the page holds, or their order, only through
|
|
464
|
+
// the fields it sorts, filters or searches by; a row gone meanwhile leaves.
|
|
465
|
+
function afterSave({ fields, gone }: SavedChanges) {
|
|
466
|
+
const filtered = Object.entries(state.filters)
|
|
467
|
+
.filter(([, filter]) => filter.value)
|
|
468
|
+
.map(([field]) => field)
|
|
469
|
+
if (
|
|
470
|
+
gone > 0 ||
|
|
471
|
+
mayMoveRows(fields, { sortKey: state.sortKey, search: state.search, filtered })
|
|
472
|
+
)
|
|
473
|
+
reload()
|
|
474
|
+
}
|
|
475
|
+
|
|
495
476
|
const {
|
|
496
477
|
saving,
|
|
497
478
|
review,
|
|
@@ -499,7 +480,8 @@ const {
|
|
|
499
480
|
undo,
|
|
500
481
|
} = useStagedSave({
|
|
501
482
|
tab: () => props.tab,
|
|
502
|
-
|
|
483
|
+
onWritten: showWritten,
|
|
484
|
+
onSaved: afterSave,
|
|
503
485
|
})
|
|
504
486
|
|
|
505
487
|
onKeyStroke('s', (event) => {
|
|
@@ -40,76 +40,84 @@ const emit = defineEmits<{ 'update:modelValue': [choice: InstanceChoice] }>()
|
|
|
40
40
|
const INSTANCES_ENDPOINT = '/api/database/tables/instances'
|
|
41
41
|
const SEARCH_DEBOUNCE_MS = 250
|
|
42
42
|
|
|
43
|
+
/** One read of a schema's named instances, with what it searched. */
|
|
44
|
+
interface InstancesPage {
|
|
45
|
+
schema: string
|
|
46
|
+
term: string
|
|
47
|
+
instances: string[]
|
|
48
|
+
total: number
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
const { t, n } = useI18n()
|
|
44
|
-
const { $authFetch } = useAuthFetch()
|
|
45
52
|
|
|
46
53
|
const search = ref('')
|
|
54
|
+
// What the server searches: the term once typing settles.
|
|
55
|
+
const term = ref('')
|
|
47
56
|
const open = ref(false)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const res = await $authFetch<{ instances: string[]; total: number }>(
|
|
57
|
+
|
|
58
|
+
watchDebounced(search, (value) => (term.value = value.trim()), {
|
|
59
|
+
debounce: SEARCH_DEBOUNCE_MS,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Read when the menu opens, in the browser only, never on the page's
|
|
63
|
+
// refreshes: a closed menu shows nothing of it.
|
|
64
|
+
const instances = useApiData<InstancesPage>(
|
|
65
|
+
() =>
|
|
66
|
+
props.schema
|
|
67
|
+
? `dms-database:instances:${JSON.stringify([props.schema, term.value])}`
|
|
68
|
+
: null,
|
|
69
|
+
async ({ $authFetch, signal }) => {
|
|
70
|
+
const schema = props.schema as string
|
|
71
|
+
const searched = term.value
|
|
72
|
+
const page = await $authFetch<{ instances: string[]; total: number }>(
|
|
66
73
|
INSTANCES_ENDPOINT,
|
|
67
74
|
{
|
|
68
|
-
query: {
|
|
69
|
-
|
|
70
|
-
search: term || undefined,
|
|
71
|
-
limit: PICKER_LIMIT,
|
|
72
|
-
},
|
|
75
|
+
query: { schema, search: searched || undefined, limit: PICKER_LIMIT },
|
|
76
|
+
signal,
|
|
73
77
|
},
|
|
74
78
|
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (current === request) status.value = 'error'
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Another schema, other instances: they load when the menu opens.
|
|
87
|
-
watch(
|
|
88
|
-
() => props.schema,
|
|
89
|
-
() => {
|
|
90
|
-
request++
|
|
91
|
-
loadedKey = null
|
|
92
|
-
names.value = []
|
|
93
|
-
matched.value = 0
|
|
94
|
-
total.value = null
|
|
95
|
-
status.value = 'idle'
|
|
96
|
-
if (open.value) void load()
|
|
79
|
+
return { schema, term: searched, ...page }
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
immediate: false,
|
|
83
|
+
server: false,
|
|
84
|
+
watch: false,
|
|
85
|
+
refreshOnPageRefresh: false,
|
|
97
86
|
},
|
|
98
87
|
)
|
|
99
88
|
|
|
89
|
+
// Another schema shows none of the previous one's instances while its own
|
|
90
|
+
// load; another search keeps the previous matches on screen meanwhile.
|
|
91
|
+
const page = computed(() => {
|
|
92
|
+
const value = instances.data.value
|
|
93
|
+
return value?.schema === props.schema ? value : null
|
|
94
|
+
})
|
|
95
|
+
const names = computed(() => page.value?.instances ?? [])
|
|
96
|
+
const matched = computed(() => page.value?.total ?? 0)
|
|
97
|
+
const status = computed(() => instances.status.value as InstancesStatus)
|
|
98
|
+
|
|
99
|
+
// Named instances of the schema, before any search; null until known.
|
|
100
|
+
const unsearched = shallowRef<InstancesPage | null>(null)
|
|
101
|
+
watch(page, (value) => {
|
|
102
|
+
if (value && !value.term) unsearched.value = value
|
|
103
|
+
})
|
|
104
|
+
const total = computed(() =>
|
|
105
|
+
unsearched.value?.schema === props.schema ? unsearched.value.total : null,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
function load() {
|
|
109
|
+
if (!open.value || !props.schema) return
|
|
110
|
+
if (instances.pending.value || instances.status.value === 'success') return
|
|
111
|
+
void instances.execute()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
watch([() => props.schema, term], load)
|
|
115
|
+
|
|
100
116
|
function onOpen(value: boolean) {
|
|
101
117
|
open.value = value
|
|
102
|
-
|
|
118
|
+
load()
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
watchDebounced(
|
|
106
|
-
search,
|
|
107
|
-
() => {
|
|
108
|
-
if (open.value) void load()
|
|
109
|
-
},
|
|
110
|
-
{ debounce: SEARCH_DEBOUNCE_MS },
|
|
111
|
-
)
|
|
112
|
-
|
|
113
121
|
const items = computed(() =>
|
|
114
122
|
instanceMenu({
|
|
115
123
|
search: search.value,
|
|
@@ -50,10 +50,9 @@ const OPERATION_ICONS: Record<string, string> = {
|
|
|
50
50
|
const { t, n } = useI18n()
|
|
51
51
|
const route = useDmsRoute()
|
|
52
52
|
const router = useDmsRouter()
|
|
53
|
-
const { $authFetch } = useAuthFetch()
|
|
54
53
|
const { confirm } = useConfirm()
|
|
55
54
|
const toast = useToast()
|
|
56
|
-
const { user } =
|
|
55
|
+
const { user } = useDmsUserSession()
|
|
57
56
|
const store = useQueryStore()
|
|
58
57
|
const { schemas } = useDatabaseSchemas()
|
|
59
58
|
|
|
@@ -64,11 +63,9 @@ const failure = ref<QueryFailure | null>(null)
|
|
|
64
63
|
const target = ref<QueryTarget>({})
|
|
65
64
|
const loadedSaved = ref<SavedQuery | null>(null)
|
|
66
65
|
const activeHistoryId = ref<string | null>(null)
|
|
67
|
-
const libraryLoading = ref(true)
|
|
68
66
|
// Named instances holding rows of a table the last read found empty in the
|
|
69
67
|
// default instance.
|
|
70
68
|
const elsewhere = ref<InstanceRows[]>([])
|
|
71
|
-
const team = ref<SavedQuery[]>([])
|
|
72
69
|
const editor = useTemplateRef<InstanceType<typeof QueryBuilderPanel>>('editor')
|
|
73
70
|
|
|
74
71
|
const edited = computed(
|
|
@@ -80,24 +77,10 @@ const ownId = computed(
|
|
|
80
77
|
)
|
|
81
78
|
|
|
82
79
|
// --- library ---
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
async function refreshSaved() {
|
|
88
|
-
await store.fetchSaved('me').catch(() => undefined)
|
|
89
|
-
try {
|
|
90
|
-
const res = await $authFetch<{ items: SavedQuery[] }>(
|
|
91
|
-
'/api/database/query/saved',
|
|
92
|
-
{
|
|
93
|
-
query: { scope: 'shared' },
|
|
94
|
-
},
|
|
95
|
-
)
|
|
96
|
-
team.value = res.items.filter((query) => query.userId !== ownId.value)
|
|
97
|
-
} catch {
|
|
98
|
-
team.value = []
|
|
99
|
-
}
|
|
100
|
-
}
|
|
80
|
+
// The team's queries: the shared ones the caller did not write.
|
|
81
|
+
const team = computed(() =>
|
|
82
|
+
store.sharedQueries.value.filter((query) => query.userId !== ownId.value),
|
|
83
|
+
)
|
|
101
84
|
|
|
102
85
|
function openSaved(query: SavedQuery) {
|
|
103
86
|
loadedSaved.value = query
|
|
@@ -151,7 +134,7 @@ async function deleteSaved(query: SavedQuery) {
|
|
|
151
134
|
if (!confirmed) return
|
|
152
135
|
await store.deleteSaved(query.id).catch(() => undefined)
|
|
153
136
|
if (loadedSaved.value?.id === query.id) loadedSaved.value = null
|
|
154
|
-
await refreshSaved()
|
|
137
|
+
await store.refreshSaved()
|
|
155
138
|
}
|
|
156
139
|
|
|
157
140
|
async function clearHistory() {
|
|
@@ -185,22 +168,25 @@ function fetchDuration(error: unknown): number | null {
|
|
|
185
168
|
return typeof duration === 'number' ? duration : null
|
|
186
169
|
}
|
|
187
170
|
|
|
171
|
+
type CountRows = (query: Record<string, string>) => Promise<{ total: number }>
|
|
172
|
+
|
|
188
173
|
/**
|
|
189
174
|
* The named instances of a table that hold rows, each with its count: asked
|
|
190
175
|
* only when a read found the table empty in the default instance (decision
|
|
191
176
|
* 11). An instance whose count fails is left out.
|
|
192
177
|
*/
|
|
193
|
-
async function instancesWithRows(
|
|
178
|
+
async function instancesWithRows(
|
|
179
|
+
target: QueryTarget,
|
|
180
|
+
count: CountRows,
|
|
181
|
+
): Promise<InstanceRows[]> {
|
|
194
182
|
const instances =
|
|
195
183
|
schemas.value.find((schema) => schema.id === target.schema)?.instances ?? []
|
|
196
184
|
const counts = await Promise.all(
|
|
197
185
|
instances.map((id) =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
filter_instance: `is:${id}`,
|
|
203
|
-
},
|
|
186
|
+
count({
|
|
187
|
+
filter_schema: `is:${target.schema}`,
|
|
188
|
+
filter_table: `is:${target.table}`,
|
|
189
|
+
filter_instance: `is:${id}`,
|
|
204
190
|
}).then(
|
|
205
191
|
(res) => ({ id, count: res.total }),
|
|
206
192
|
() => ({ id, count: 0 }),
|
|
@@ -210,6 +196,29 @@ async function instancesWithRows(target: QueryTarget): Promise<InstanceRows[]> {
|
|
|
210
196
|
return counts.filter((entry) => entry.count > 0)
|
|
211
197
|
}
|
|
212
198
|
|
|
199
|
+
// The table the last read found empty, whose instances are counted: in the
|
|
200
|
+
// browser, after a run.
|
|
201
|
+
const emptyTarget = shallowRef<QueryTarget | null>(null)
|
|
202
|
+
const elsewhereRead = useApiData<InstanceRows[]>(
|
|
203
|
+
() =>
|
|
204
|
+
emptyTarget.value
|
|
205
|
+
? `dms-database:elsewhere:${JSON.stringify([
|
|
206
|
+
emptyTarget.value.schema,
|
|
207
|
+
emptyTarget.value.table,
|
|
208
|
+
])}`
|
|
209
|
+
: null,
|
|
210
|
+
({ $authFetch, signal }) =>
|
|
211
|
+
instancesWithRows(emptyTarget.value as QueryTarget, (query) =>
|
|
212
|
+
$authFetch<{ total: number }>(BROWSE_COUNT, { query, signal }),
|
|
213
|
+
),
|
|
214
|
+
{
|
|
215
|
+
immediate: false,
|
|
216
|
+
server: false,
|
|
217
|
+
watch: false,
|
|
218
|
+
refreshOnPageRefresh: false,
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
213
222
|
// Each run drops the instances found for the previous one; a slow answer
|
|
214
223
|
// for an older run is ignored.
|
|
215
224
|
let runId = 0
|
|
@@ -228,8 +237,9 @@ async function findElsewhere(
|
|
|
228
237
|
!next.table
|
|
229
238
|
)
|
|
230
239
|
return
|
|
231
|
-
|
|
232
|
-
|
|
240
|
+
emptyTarget.value = next
|
|
241
|
+
await elsewhereRead.execute()
|
|
242
|
+
if (id === runId) elsewhere.value = elsewhereRead.data.value ?? []
|
|
233
243
|
}
|
|
234
244
|
|
|
235
245
|
// The result keeps naming the previous run's target until the new one lands.
|
|
@@ -251,7 +261,7 @@ async function execute(query: Record<string, unknown>, text: string) {
|
|
|
251
261
|
target.value = next
|
|
252
262
|
executing.value = false
|
|
253
263
|
activeHistoryId.value = null
|
|
254
|
-
await refreshHistory()
|
|
264
|
+
await store.refreshHistory()
|
|
255
265
|
}
|
|
256
266
|
}
|
|
257
267
|
|
|
@@ -516,7 +526,7 @@ async function confirmSave() {
|
|
|
516
526
|
color: 'success',
|
|
517
527
|
icon: 'i-ph-check',
|
|
518
528
|
})
|
|
519
|
-
await refreshSaved()
|
|
529
|
+
await store.refreshSaved()
|
|
520
530
|
} catch (error) {
|
|
521
531
|
saveError.value =
|
|
522
532
|
error instanceof Error && !('data' in error)
|
|
@@ -570,9 +580,9 @@ async function openFromRoute(): Promise<boolean> {
|
|
|
570
580
|
return opened
|
|
571
581
|
}
|
|
572
582
|
|
|
583
|
+
// The link and the draft name queries of the library: once it is read.
|
|
573
584
|
onMounted(async () => {
|
|
574
|
-
await
|
|
575
|
-
libraryLoading.value = false
|
|
585
|
+
await store.libraryRead
|
|
576
586
|
if (!(await openFromRoute())) restoreDraft()
|
|
577
587
|
})
|
|
578
588
|
</script>
|
|
@@ -590,7 +600,7 @@ onMounted(async () => {
|
|
|
590
600
|
:team="team"
|
|
591
601
|
:active-saved-id="loadedSaved?.id ?? null"
|
|
592
602
|
:active-history-id="activeHistoryId"
|
|
593
|
-
:loading="libraryLoading"
|
|
603
|
+
:loading="store.libraryLoading.value"
|
|
594
604
|
@open-history="openHistory"
|
|
595
605
|
@save-history="openSaveModal"
|
|
596
606
|
@open-saved="openSaved"
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
-
dayLabel,
|
|
4
3
|
groupByDay,
|
|
5
4
|
HISTORY_KEPT,
|
|
6
5
|
RUN_DOT_CLASSES,
|
|
@@ -38,7 +37,10 @@ const emit = defineEmits<{
|
|
|
38
37
|
'clear-history': []
|
|
39
38
|
}>()
|
|
40
39
|
|
|
41
|
-
const { t, n
|
|
40
|
+
const { t, n } = useI18n()
|
|
41
|
+
// Times and days in the user's time zone, which a server render writes in
|
|
42
|
+
// the browser's: the HTML shows what the browser shows.
|
|
43
|
+
const { dateTimeFormat } = useRegionalFormat()
|
|
42
44
|
|
|
43
45
|
const tab = ref<LibraryTab>('history')
|
|
44
46
|
const search = ref('')
|
|
@@ -76,15 +78,18 @@ const historyGroups = computed(() =>
|
|
|
76
78
|
matches(entry.source, entry.error, target),
|
|
77
79
|
),
|
|
78
80
|
({ entry }) => entry.executedAt,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
label: dayLabel(group.date, locale.value, {
|
|
82
|
-
today: t('dms_database.query.library.today'),
|
|
83
|
-
yesterday: t('dms_database.query.library.yesterday'),
|
|
84
|
-
}),
|
|
85
|
-
})),
|
|
81
|
+
regionalDayKey,
|
|
82
|
+
).map((group) => ({ ...group, label: dayLabel(group.date) })),
|
|
86
83
|
)
|
|
87
84
|
|
|
85
|
+
/** "Today", "Yesterday" or the date. */
|
|
86
|
+
function dayLabel(date: Date): string {
|
|
87
|
+
const daysAgo = regionalDayNumber(new Date()) - regionalDayNumber(date)
|
|
88
|
+
if (daysAgo === 0) return t('dms_database.query.library.today')
|
|
89
|
+
if (daysAgo === 1) return t('dms_database.query.library.yesterday')
|
|
90
|
+
return dateTimeFormat({ day: 'numeric', month: 'short' }).format(date)
|
|
91
|
+
}
|
|
92
|
+
|
|
88
93
|
const savedItems = computed(() =>
|
|
89
94
|
(tab.value === 'team' ? props.team : props.saved).filter((query) =>
|
|
90
95
|
matches(query.name, query.description, query.source, query.ownerName),
|
|
@@ -92,10 +97,9 @@ const savedItems = computed(() =>
|
|
|
92
97
|
)
|
|
93
98
|
|
|
94
99
|
function time(iso: string): string {
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}).format(new Date(iso))
|
|
100
|
+
return dateTimeFormat({ hour: '2-digit', minute: '2-digit' }).format(
|
|
101
|
+
new Date(iso),
|
|
102
|
+
)
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
function runMeta(entry: HistoryEntry): string {
|
|
@@ -116,10 +120,9 @@ function runMeta(entry: HistoryEntry): string {
|
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
function savedMeta(query: SavedQuery): string {
|
|
119
|
-
const date =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}).format(new Date(query.updatedAt ?? query.createdAt))
|
|
123
|
+
const date = dateTimeFormat({ day: 'numeric', month: 'short' }).format(
|
|
124
|
+
new Date(query.updatedAt ?? query.createdAt),
|
|
125
|
+
)
|
|
123
126
|
return query.ownerName ? `${query.ownerName} · ${date}` : date
|
|
124
127
|
}
|
|
125
128
|
|
|
@@ -58,7 +58,6 @@ const props = defineProps<{
|
|
|
58
58
|
const emit = defineEmits<{ success: [] }>()
|
|
59
59
|
|
|
60
60
|
const { t, n } = useI18n()
|
|
61
|
-
const { $authFetch } = useAuthFetch()
|
|
62
61
|
const staged = useStagedEdits()
|
|
63
62
|
|
|
64
63
|
const active = ref<DrawerTab>('fields')
|
|
@@ -169,23 +168,21 @@ function setBoolean(field: string, value: boolean) {
|
|
|
169
168
|
}
|
|
170
169
|
|
|
171
170
|
// --- referenced by ---
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
referencesFailed.value = true
|
|
188
|
-
}
|
|
171
|
+
// Read once the drawer opens, in the browser.
|
|
172
|
+
const referencesRead = useApiData<{ items: RowReference[] }>(
|
|
173
|
+
() =>
|
|
174
|
+
props.rowId && !props.readOnly ? '/api/database/browse/references' : null,
|
|
175
|
+
{
|
|
176
|
+
query: () => ({ ...browseSelectionQuery(props.tab), id: props.rowId }),
|
|
177
|
+
immediate: false,
|
|
178
|
+
server: false,
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
const references = computed(() => referencesRead.data.value?.items ?? null)
|
|
182
|
+
const referencesFailed = computed(() => referencesRead.status.value === 'error')
|
|
183
|
+
|
|
184
|
+
function loadReferences() {
|
|
185
|
+
void referencesRead.execute()
|
|
189
186
|
}
|
|
190
187
|
|
|
191
188
|
onMounted(loadReferences)
|