@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.
Files changed (41) hide show
  1. package/README.md +73 -49
  2. package/dist/db/models/diagram_layouts.model.d.ts +1 -0
  3. package/dist/db/models/diagram_notes.model.d.ts +1 -0
  4. package/dist/db/models/query_history.model.d.ts +1 -0
  5. package/dist/db/models/saved_queries.model.d.ts +1 -0
  6. package/dist/routes/data.d.ts +1 -3
  7. package/dist/routes/data.js +6 -1
  8. package/dist/routes/data.js.map +1 -1
  9. package/frontend-vue/app/build/composables/useDataBrowserGrid.ts +4 -38
  10. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +7 -4
  11. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +1 -2
  12. package/frontend-vue/app/build/composables/useDiagramPersistence.ts +49 -16
  13. package/frontend-vue/app/build/composables/useQueryStore.ts +59 -27
  14. package/frontend-vue/app/build/composables/useTableCounts.ts +50 -0
  15. package/frontend-vue/app/build/data/stagedWrites.ts +46 -6
  16. package/frontend-vue/app/build/data/useStagedSave.ts +42 -12
  17. package/frontend-vue/app/build/query/runs.ts +7 -27
  18. package/frontend-vue/app/build/schemas/useSchemasListing.ts +10 -33
  19. package/frontend-vue/app/components/ConnectionStatus.vue +14 -11
  20. package/frontend-vue/app/components/DataBrowserSidebar.vue +20 -42
  21. package/frontend-vue/app/components/DataBrowserTabBar.vue +1 -1
  22. package/frontend-vue/app/components/DataGrid.vue +73 -91
  23. package/frontend-vue/app/components/InstancePicker.vue +63 -55
  24. package/frontend-vue/app/components/QueryConsole.vue +47 -37
  25. package/frontend-vue/app/components/QueryLibrary.vue +20 -17
  26. package/frontend-vue/app/components/RowDrawer.vue +15 -18
  27. package/frontend-vue/app/components/SchemaDiagram.vue +7 -7
  28. package/frontend-vue/app/components/TableInspector.vue +116 -84
  29. package/frontend-vue/package.json +4 -3
  30. package/frontend-vue/pnpm-lock.yaml +1388 -2803
  31. package/frontend-vue/tests/dataBrowser.test.ts +59 -1
  32. package/frontend-vue/tests/fixtures/inertia.ts +8 -0
  33. package/frontend-vue/tests/fixtures/serverRender.ts +219 -0
  34. package/frontend-vue/tests/fixtures/vueI18n.ts +4 -0
  35. package/frontend-vue/tests/helpers.test.ts +1 -0
  36. package/frontend-vue/tests/locales.test.ts +24 -0
  37. package/frontend-vue/tests/onDemandReads.test.ts +106 -0
  38. package/frontend-vue/tests/serverRender.test.ts +235 -0
  39. package/frontend-vue/vitest.config.ts +24 -0
  40. package/package.json +31 -14
  41. package/skills/dms-database/SKILL.md +35 -0
@@ -0,0 +1,50 @@
1
+ import type { Ref } from 'vue'
2
+ import { isFilterToken } from './useDataBrowserGrid'
3
+ import { DEFAULT_INSTANCE_VALUE } from './useDataBrowserTabs'
4
+
5
+ const TABLES_ENDPOINT = '/api/database/browse/tables'
6
+
7
+ interface TableEntry {
8
+ name: string
9
+ elementCount: number
10
+ }
11
+
12
+ /**
13
+ * The rows of each table of a schema in one instance, for the data browser's
14
+ * table list. A server render reads them and hands them over with the page;
15
+ * another schema or instance is read once picked. No count shows while they
16
+ * load, nor once they failed: `failed` says so.
17
+ */
18
+ export function useTableCounts(
19
+ schema: Ref<string | null>,
20
+ instance: Ref<string>,
21
+ ) {
22
+ const { data, status } = useApiData<{ items: TableEntry[] }>(
23
+ () => (schema.value ? TABLES_ENDPOINT : null),
24
+ {
25
+ query: () => {
26
+ const query: Record<string, string> = {
27
+ filter_schema: isFilterToken(schema.value ?? ''),
28
+ }
29
+ if (instance.value !== DEFAULT_INSTANCE_VALUE)
30
+ query.filter_instance = isFilterToken(instance.value)
31
+ return query
32
+ },
33
+ },
34
+ )
35
+
36
+ return {
37
+ /** Rows by table name. */
38
+ counts: computed<Record<string, number>>(() =>
39
+ status.value === 'success'
40
+ ? Object.fromEntries(
41
+ (data.value?.items ?? []).map((item) => [
42
+ item.name,
43
+ item.elementCount,
44
+ ]),
45
+ )
46
+ : {},
47
+ ),
48
+ failed: computed(() => status.value === 'error'),
49
+ }
50
+ }
@@ -20,11 +20,11 @@ function statusOf(error: unknown): number | undefined {
20
20
  return failure?.statusCode ?? failure?.status
21
21
  }
22
22
 
23
- export async function writeRows(
23
+ export async function writeRows<T = unknown>(
24
24
  rows: Iterable<[string, Record<string, unknown>]>,
25
- write: (rowId: string, body: Record<string, unknown>) => Promise<unknown>,
26
- /** Called once a row is settled: written, or gone. */
27
- onSettled?: (rowId: string) => void,
25
+ write: (rowId: string, body: Record<string, unknown>) => Promise<T>,
26
+ /** Called once a row is settled: written, with the write's answer, or gone. */
27
+ onSettled?: (rowId: string, answer?: T) => void,
28
28
  ): Promise<RowWriteOutcome> {
29
29
  const outcome: RowWriteOutcome = {
30
30
  written: [],
@@ -37,8 +37,9 @@ export async function writeRows(
37
37
  outcome.left.push(rowId)
38
38
  continue
39
39
  }
40
+ let answer: T | undefined
40
41
  try {
41
- await write(rowId, body)
42
+ answer = await write(rowId, body)
42
43
  outcome.written.push(rowId)
43
44
  } catch (error) {
44
45
  if (statusOf(error) !== NOT_FOUND) {
@@ -49,7 +50,7 @@ export async function writeRows(
49
50
  }
50
51
  outcome.gone.push(rowId)
51
52
  }
52
- onSettled?.(rowId)
53
+ onSettled?.(rowId, answer)
53
54
  }
54
55
  return outcome
55
56
  }
@@ -62,3 +63,42 @@ export function changesOf<T extends { rowId: string }>(
62
63
  const rows = new Set(rowIds)
63
64
  return changes.filter((change) => rows.has(change.rowId))
64
65
  }
66
+
67
+ /** How the rows of a grid page are narrowed and ordered. */
68
+ export interface PageRefinement {
69
+ sortKey: string | null
70
+ search: string
71
+ /** The fields a column filter narrows, with a value. */
72
+ filtered: string[]
73
+ }
74
+
75
+ /**
76
+ * Whether writing `fields` may take rows in or out of the page, or move them
77
+ * in it: a field it is sorted or filtered by, or any field while a search
78
+ * runs (it matches every column). The page is then read again; otherwise
79
+ * the rows written stay where they are, as the server answered them.
80
+ */
81
+ export function mayMoveRows(
82
+ fields: Iterable<string>,
83
+ refinement: PageRefinement,
84
+ ): boolean {
85
+ if (refinement.search !== '') return true
86
+ const narrowing = new Set(refinement.filtered)
87
+ if (refinement.sortKey) narrowing.add(refinement.sortKey)
88
+ for (const field of fields) if (narrowing.has(field)) return true
89
+ return false
90
+ }
91
+
92
+ /**
93
+ * `rows` with the row the server answered a write with in place of the one of
94
+ * the same id: the fields it holds replace theirs, the browse-only ones stay.
95
+ */
96
+ export function withWrittenRow(
97
+ rows: Record<string, unknown>[],
98
+ written: Record<string, unknown>,
99
+ idOf: (row: Record<string, unknown>) => string | null,
100
+ ): Record<string, unknown>[] {
101
+ const id = idOf(written)
102
+ if (id === null) return rows
103
+ return rows.map((row) => (idOf(row) === id ? { ...row, ...written } : row))
104
+ }
@@ -12,9 +12,10 @@ import { changesOf, type RowWriteOutcome, writeRows } from './stagedWrites'
12
12
  // Writes the staged edits of a tab after the user reviewed them, then offers
13
13
  // to take them back for a few seconds, in the save bar of the current tab
14
14
  // (another tab ends the offer). Each row is written with one call of the
15
- // edit route; an undo writes the previous values back the same way. Only the
16
- // rows written are announced and undone: a row gone meanwhile drops its
17
- // changes, and a failure stops the save with the rows left still staged.
15
+ // edit route, which answers with the row as stored; an undo writes the
16
+ // previous values back the same way. Only the rows written are announced and
17
+ // undone: a row gone meanwhile drops its changes, and a failure stops the
18
+ // save with the rows left still staged.
18
19
 
19
20
  const EDIT_ENDPOINT = '/api/database/browse/edit'
20
21
  const UNDO_WINDOW_MS = 10_000
@@ -27,9 +28,19 @@ interface SaveNotice {
27
28
  scope: string
28
29
  }
29
30
 
31
+ /** What a save or an undo wrote, once it is settled. */
32
+ export interface SavedChanges {
33
+ /** The fields written. */
34
+ fields: string[]
35
+ /** Rows gone meanwhile, which the page may still show. */
36
+ gone: number
37
+ }
38
+
30
39
  interface StagedSaveOptions {
31
40
  tab: () => BrowserTab
32
- onSaved: () => void
41
+ /** A row written, as the server answered: shown at once in place of the one read. */
42
+ onWritten: (tab: BrowserTab, row: Record<string, unknown>) => void
43
+ onSaved: (saved: SavedChanges) => void
33
44
  }
34
45
 
35
46
  /** The server's reason when it gave one, else the error's own message. */
@@ -41,6 +52,10 @@ function messageOf(error: unknown): string {
41
52
  return error instanceof Error ? error.message : String(error)
42
53
  }
43
54
 
55
+ function fieldsOf(changes: StagedChange[]): string[] {
56
+ return [...new Set(changes.map((change) => change.field))]
57
+ }
58
+
44
59
  export function useStagedSave(options: StagedSaveOptions) {
45
60
  const { t, n } = useI18n()
46
61
  const { $authFetch } = useAuthFetch()
@@ -85,12 +100,13 @@ export function useStagedSave(options: StagedSaveOptions) {
85
100
  return `${tab.schema}.${tab.table} ${badge}`
86
101
  }
87
102
 
88
- async function writeRow(
103
+ /** Writes a row, and answers with it as the server now stores it. */
104
+ function writeRow(
89
105
  tab: BrowserTab,
90
106
  rowId: string,
91
107
  body: Record<string, unknown>,
92
- ) {
93
- await $authFetch(EDIT_ENDPOINT, {
108
+ ): Promise<Record<string, unknown>> {
109
+ return $authFetch<Record<string, unknown>>(EDIT_ENDPOINT, {
94
110
  method: 'PUT',
95
111
  query: { ...browseSelectionQuery(tab), id: rowId },
96
112
  body,
@@ -135,8 +151,13 @@ export function useStagedSave(options: StagedSaveOptions) {
135
151
  )
136
152
  }
137
153
 
138
- // Each row written (or gone) leaves the staged edits at once: a failure
139
- // further on keeps only the rows not written.
154
+ /** Shows a row written as the server answered it. */
155
+ function showWritten(tab: BrowserTab, row: Record<string, unknown> | undefined) {
156
+ if (row) options.onWritten(tab, row)
157
+ }
158
+
159
+ // Each row written (or gone) leaves the staged edits at once, its answer
160
+ // shown in its place: a failure further on keeps only the rows not written.
140
161
  function saveAll(tab: BrowserTab): Promise<RowWriteOutcome> {
141
162
  const rows = staged
142
163
  .rowIds(tab.id)
@@ -147,7 +168,10 @@ export function useStagedSave(options: StagedSaveOptions) {
147
168
  return writeRows(
148
169
  rows,
149
170
  (rowId, body) => writeRow(tab, rowId, body),
150
- (rowId) => staged.discard(tab.id, [rowId]),
171
+ (rowId, row) => {
172
+ showWritten(tab, row)
173
+ staged.discard(tab.id, [rowId])
174
+ },
151
175
  )
152
176
  }
153
177
 
@@ -202,6 +226,7 @@ export function useStagedSave(options: StagedSaveOptions) {
202
226
  const outcome = await writeRows(
203
227
  undoBodies(current.changes),
204
228
  (rowId, body) => writeRow(tab, rowId, body),
229
+ (_rowId, row) => showWritten(tab, row),
205
230
  )
206
231
  const restored = changesOf(current.changes, outcome.written)
207
232
  const gone = changesOf(current.changes, outcome.gone)
@@ -243,7 +268,10 @@ export function useStagedSave(options: StagedSaveOptions) {
243
268
  ),
244
269
  color: 'warning',
245
270
  })
246
- options.onSaved()
271
+ options.onSaved({
272
+ fields: fieldsOf(current.changes),
273
+ gone: outcome.gone.length,
274
+ })
247
275
  }
248
276
 
249
277
  function announce(tab: BrowserTab, saved: StagedChange[]) {
@@ -259,6 +287,7 @@ export function useStagedSave(options: StagedSaveOptions) {
259
287
  // the dialog closed, what was written is announced and undoable.
260
288
  const saved: StagedChange[] = []
261
289
  let settled = false
290
+ let gone = 0
262
291
  try {
263
292
  await confirm({
264
293
  title: t(
@@ -281,6 +310,7 @@ export function useStagedSave(options: StagedSaveOptions) {
281
310
  try {
282
311
  const outcome = await saveAll(tab)
283
312
  saved.push(...changesOf(changes, outcome.written))
313
+ gone += outcome.gone.length
284
314
  if (outcome.written.length + outcome.gone.length > 0) settled = true
285
315
  return partialOutcome(outcome, changes, saved.length)
286
316
  } finally {
@@ -289,7 +319,7 @@ export function useStagedSave(options: StagedSaveOptions) {
289
319
  },
290
320
  })
291
321
  } finally {
292
- if (settled) options.onSaved()
322
+ if (settled) options.onSaved({ fields: fieldsOf(saved), gone })
293
323
  if (saved.length > 0) announce(tab, saved)
294
324
  }
295
325
  }
@@ -33,19 +33,20 @@ export interface DayGroup<T> {
33
33
  items: T[]
34
34
  }
35
35
 
36
- function dayKey(date: Date): string {
37
- return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
38
- }
39
-
40
- /** Entries by local calendar day, newest day first, in their given order. */
36
+ /**
37
+ * Entries by calendar day, newest day first, in their given order. `dayOf`
38
+ * names a date's day: the reader's (`regionalDayKey`), so that a server
39
+ * render groups as the browser does.
40
+ */
41
41
  export function groupByDay<T>(
42
42
  entries: T[],
43
43
  dateOf: (entry: T) => string,
44
+ dayOf: (date: Date) => string,
44
45
  ): DayGroup<T>[] {
45
46
  const groups = new Map<string, DayGroup<T>>()
46
47
  for (const entry of entries) {
47
48
  const date = new Date(dateOf(entry))
48
- const key = dayKey(date)
49
+ const key = dayOf(date)
49
50
  const group = groups.get(key) ?? { key, date, items: [] }
50
51
  group.items.push(entry)
51
52
  groups.set(key, group)
@@ -53,27 +54,6 @@ export function groupByDay<T>(
53
54
  return [...groups.values()]
54
55
  }
55
56
 
56
- const DAY_MS = 86_400_000
57
-
58
- /** "Today", "Yesterday" or the date, in the interface language. */
59
- export function dayLabel(
60
- date: Date,
61
- locale: string,
62
- words: { today: string; yesterday: string },
63
- ): string {
64
- const today = new Date()
65
- today.setHours(0, 0, 0, 0)
66
- const day = new Date(date)
67
- day.setHours(0, 0, 0, 0)
68
- const days = Math.round((today.getTime() - day.getTime()) / DAY_MS)
69
- if (days === 0) return words.today
70
- if (days === 1) return words.yesterday
71
- return new Intl.DateTimeFormat(locale, {
72
- day: 'numeric',
73
- month: 'short',
74
- }).format(date)
75
- }
76
-
77
57
  export type UnknownKind = 'schema' | 'instance' | 'table'
78
58
 
79
59
  /** A name a query used that its schema does not know. */
@@ -1,4 +1,4 @@
1
- import { computed, ref, watch } from 'vue'
1
+ import { computed } from 'vue'
2
2
  import {
3
3
  readFilterState,
4
4
  sourceQuery,
@@ -16,47 +16,24 @@ interface SourceResponse {
16
16
  all?: number
17
17
  }
18
18
 
19
- type ListingStatus = 'idle' | 'pending' | 'success' | 'error'
20
-
21
19
  export function useSchemasListing() {
22
20
  const route = useDmsRoute()
23
- const { $authFetch } = useAuthFetch()
24
21
  const state = computed<SchemasFilterState>(() => readFilterState(route.query))
25
- const query = computed(() => sourceQuery(state.value))
26
-
27
- // The counts of the last answer stay while the next one loads.
28
- const total = ref(0)
29
- const all = ref<number | null>(null)
30
- const status = ref<ListingStatus>('idle')
31
- let latest = 0
32
22
 
33
- async function load() {
34
- const request = ++latest
35
- status.value = 'pending'
36
- try {
37
- const response = await $authFetch<SourceResponse>(TABLES_SOURCE, {
38
- query: query.value,
39
- })
40
- if (request !== latest) return
41
- total.value = response.total
42
- all.value = response.all ?? null
43
- status.value = 'success'
44
- } catch {
45
- if (request === latest) status.value = 'error'
46
- }
47
- }
48
-
49
- // In the browser only: the server renders no count.
50
- if (typeof window !== 'undefined') {
51
- watch(() => JSON.stringify(query.value), load, { immediate: true })
52
- }
23
+ // Rendered on the server and handed over, the counts only (not the rows
24
+ // the route lists); the counts of the last answer stay while the next one
25
+ // loads.
26
+ const { data, status } = useApiData<SourceResponse>(TABLES_SOURCE, {
27
+ query: () => sourceQuery(state.value),
28
+ pick: ['total', 'all'],
29
+ })
53
30
 
54
31
  return {
55
32
  state,
56
33
  /** Tables the bar's filters list. */
57
- total,
34
+ total: computed(() => data.value?.total ?? 0),
58
35
  /** Registered tables before any filter; null until known. */
59
- all,
36
+ all: computed(() => data.value?.all ?? null),
60
37
  status,
61
38
  }
62
39
  }
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useDatabaseHealth } from '../build/composables/useDatabaseHealth'
3
- import { useIntervalFn, useNow } from '@vueuse/core'
3
+ import { useNow } from '@vueuse/core'
4
4
 
5
5
  // The overview's health hero: whether the database answers, how fast, and
6
6
  // what it holds. The probe runs again every PROBE_INTERVAL_MS while the page
@@ -10,10 +10,16 @@ const SECOND_MS = 1000
10
10
  const MINUTE_S = 60
11
11
 
12
12
  const { t, n, locale } = useI18n()
13
- const { health, isLoading, error, refresh } = useDatabaseHealth()
14
- const now = useNow({ interval: SECOND_MS })
15
-
16
- useIntervalFn(() => refresh(), PROBE_INTERVAL_MS)
13
+ const { health, isLoading, error, refresh } = useDatabaseHealth({
14
+ refreshInterval: PROBE_INTERVAL_MS,
15
+ })
16
+ const clock = useNow({ interval: SECOND_MS })
17
+ // The probe's own time until the page is mounted: the server renders the age
18
+ // a few seconds before the browser hydrates it, and both must write the same.
19
+ const mounted = ref(false)
20
+ onMounted(() => {
21
+ mounted.value = true
22
+ })
17
23
 
18
24
  // A failed health request is a lost connection too: the route answers
19
25
  // whenever the backend is up.
@@ -24,12 +30,9 @@ const isDown = computed(
24
30
  const checkedAgo = computed(() => {
25
31
  const checkedAt = health.value.checkedAt
26
32
  if (!checkedAt) return ''
27
- const seconds = Math.max(
28
- 0,
29
- Math.round(
30
- (now.value.getTime() - new Date(checkedAt).getTime()) / SECOND_MS,
31
- ),
32
- )
33
+ const checked = new Date(checkedAt).getTime()
34
+ const now = mounted.value ? clock.value.getTime() : checked
35
+ const seconds = Math.max(0, Math.round((now - checked) / SECOND_MS))
33
36
  return seconds < MINUTE_S
34
37
  ? t('dms_database.overview.connection.checked_seconds', { count: seconds })
35
38
  : t('dms_database.overview.connection.checked_minutes', {
@@ -6,10 +6,11 @@ import {
6
6
  tabInstanceOf,
7
7
  useDataBrowserTabs,
8
8
  } from '../build/composables/useDataBrowserTabs'
9
- import { isFilterToken } from '../build/composables/useDataBrowserGrid'
9
+ import { useTableCounts } from '../build/composables/useTableCounts'
10
10
  import type { InstanceChoice } from '../build/data/instanceOptions'
11
11
  import { onKeyStroke } from '@vueuse/core'
12
12
  import type { SchemaSummary } from '../build/composables/useDatabaseSchemas'
13
+ import { instanceFromUrl } from '../build/utils/databaseLinks'
13
14
  import InstancePicker from './InstancePicker.vue'
14
15
  import SchemaPicker from './SchemaPicker.vue'
15
16
 
@@ -20,13 +21,6 @@ import SchemaPicker from './SchemaPicker.vue'
20
21
  // table open in a tab carries a hollow ring, the active one included: the
21
22
  // fill of the row says which is shown.
22
23
 
23
- const TABLES_ENDPOINT = '/api/database/browse/tables'
24
-
25
- interface TableEntry {
26
- name: string
27
- elementCount: number
28
- }
29
-
30
24
  const props = defineProps<{
31
25
  schemas: SchemaSummary[]
32
26
  /** The schema list is being read. */
@@ -38,7 +32,6 @@ const props = defineProps<{
38
32
  const emit = defineEmits<{ retry: [] }>()
39
33
 
40
34
  const { t, n } = useI18n()
41
- const { $authFetch } = useAuthFetch()
42
35
  const { tabs, activeId, activeTab, activations, openTab } = useDataBrowserTabs()
43
36
  const route = useDmsRoute()
44
37
 
@@ -49,13 +42,17 @@ const filterInput = useTemplateRef<{ inputRef?: HTMLInputElement }>(
49
42
  'filterInput',
50
43
  )
51
44
 
52
- // A schema-only link (?schema=X with no table) picks the schema; otherwise
53
- // the active tab leads, then the first schema. A schema or instance picked
54
- // here holds until the user goes to a tab (another one, or the same again).
45
+ // The schema a link names (?schema=X) leads, else the first one; once a tab
46
+ // is active, it leads. A table link (?schema=X&table=Y) opens its tab once
47
+ // mounted, in the instance it names: that scope is shown from the start, so
48
+ // the server renders the counts the browser keeps. A schema or instance
49
+ // picked here holds until the user goes to a tab (another one, or the same
50
+ // again).
55
51
  const linkedSchema =
56
- typeof route.query.schema === 'string' && !route.query.table
57
- ? route.query.schema
58
- : null
52
+ typeof route.query.schema === 'string' ? route.query.schema : null
53
+ const linkedInstance = route.query.table
54
+ ? tabInstanceOf(instanceFromUrl(route.query.instance))
55
+ : DEFAULT_INSTANCE_VALUE
59
56
 
60
57
  watch(
61
58
  [activeId, activations],
@@ -74,6 +71,7 @@ watch(
74
71
  if (schemaId.value || list.length === 0) return
75
72
  const linked = list.find((schema) => schema.id === linkedSchema)
76
73
  schemaId.value = linked?.id ?? list[0]?.id ?? null
74
+ if (linked) instance.value = linkedInstance
77
75
  },
78
76
  { immediate: true },
79
77
  )
@@ -111,32 +109,7 @@ const instanceName = computed(() => {
111
109
  })
112
110
 
113
111
  // --- tables and their counts in the picked instance ---
114
- const counts = ref<Record<string, number>>({})
115
- const countsFailed = ref(false)
116
-
117
- watch(
118
- [schemaId, instance],
119
- async ([id, picked]) => {
120
- counts.value = {}
121
- countsFailed.value = false
122
- if (!id) return
123
- const query: Record<string, string> = { filter_schema: isFilterToken(id) }
124
- if (picked !== DEFAULT_INSTANCE_VALUE)
125
- query.filter_instance = isFilterToken(picked)
126
- try {
127
- const res = await $authFetch<{ items: TableEntry[] }>(TABLES_ENDPOINT, {
128
- query,
129
- })
130
- if (schemaId.value !== id || instance.value !== picked) return
131
- counts.value = Object.fromEntries(
132
- res.items.map((item) => [item.name, item.elementCount]),
133
- )
134
- } catch {
135
- if (schemaId.value === id) countsFailed.value = true
136
- }
137
- },
138
- { immediate: true },
139
- )
112
+ const { counts, failed: countsFailed } = useTableCounts(schemaId, instance)
140
113
 
141
114
  const openIds = computed(() => new Set(tabs.value.map((tab) => tab.id)))
142
115
 
@@ -327,7 +300,12 @@ onKeyStroke('t', (event) => {
327
300
  <span v-if="countsFailed">
328
301
  {{ t('dms_database.data.scope.counts_failed') }}
329
302
  </span>
330
- <i18n-t v-else keypath="dms_database.data.scope.counts_for" tag="span">
303
+ <i18n-t
304
+ v-else
305
+ keypath="dms_database.data.scope.counts_for"
306
+ tag="span"
307
+ scope="global"
308
+ >
331
309
  <template #instance>
332
310
  <span class="text-primary font-mono">{{ instanceName }}</span>
333
311
  </template>
@@ -248,7 +248,7 @@ async function close(tab: BrowserTab) {
248
248
  v-if="hasPreview"
249
249
  class="text-dimmed hidden shrink-0 self-center whitespace-nowrap px-3 pb-1 pt-1.5 text-[11px] lg:inline"
250
250
  >
251
- <i18n-t keypath="dms_database.data.tabs.hint" tag="span">
251
+ <i18n-t keypath="dms_database.data.tabs.hint" tag="span" scope="global">
252
252
  <template #italic>
253
253
  <em>{{ t('dms_database.data.tabs.italic') }}</em>
254
254
  </template>