From a31c365f11f1e790e438d906d06ee412bfc443c0 Mon Sep 17 00:00:00 2001 From: willemijn-zeno Date: Tue, 11 Aug 2026 11:49:02 +0200 Subject: [PATCH 1/2] feat(ui): display Iceberg field documentation --- src/components/TableColumnProfiler.test.ts | 106 +++++++++++++++++++++ src/components/TableColumnProfiler.vue | 31 +++++- 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 src/components/TableColumnProfiler.test.ts diff --git a/src/components/TableColumnProfiler.test.ts b/src/components/TableColumnProfiler.test.ts new file mode 100644 index 00000000..12258645 --- /dev/null +++ b/src/components/TableColumnProfiler.test.ts @@ -0,0 +1,106 @@ +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { describe, expect, it, vi } from 'vitest'; +import type { TableMetadata } from '../gen/iceberg/types.gen'; +import TableColumnProfiler from './TableColumnProfiler.vue'; + +vi.mock('../plugins/functions', () => ({ + useFunctions: () => ({ + copyToClipboard: vi.fn(), + listTableColumnTags: vi.fn(), + }), +})); + +vi.mock('../composables/useLoQE', () => ({ + useLoQE: () => ({}), +})); + +vi.mock('../stores/user', () => ({ + useUserStore: () => ({ user: { access_token: '' } }), +})); + +vi.mock('../stores/visual', () => ({ + useVisualStore: () => ({ themeLight: true, tagsRefresh: 0 }), +})); + +vi.mock('../stores/loqe', () => ({ + useLoQEStore: () => ({ + clearTableProfiles: vi.fn(), + getTableProfiles: () => ({}), + }), +})); + +describe('TableColumnProfiler', () => { + it('shows field documentation from the current schema for top-level and nested fields', async () => { + const metadata = { + 'current-schema-id': 2, + schemas: [ + { + 'schema-id': 1, + type: 'struct', + fields: [{ id: 1, name: 'legacy', type: 'string', required: false, doc: 'Legacy field' }], + }, + { + 'schema-id': 2, + type: 'struct', + fields: [ + { + id: 2, + name: 'customer', + required: true, + doc: 'Current customer record', + type: { + type: 'struct', + fields: [ + { + id: 3, + name: 'email', + type: 'string', + required: false, + doc: 'Primary contact address', + }, + ], + }, + }, + ], + }, + ], + } as unknown as TableMetadata; + + const wrapper = mount(TableColumnProfiler, { + props: { metadata }, + global: { + stubs: { + 'v-alert': { template: '
' }, + 'v-btn': { + props: ['icon'], + emits: ['click'], + template: '', + }, + 'v-btn-toggle': { template: '
' }, + 'v-card': { template: '
' }, + 'v-card-text': { template: '
' }, + 'v-card-title': { template: '
' }, + 'v-chip': { template: '' }, + 'v-dialog': { template: '
' }, + 'v-divider': { template: '
' }, + 'v-icon': { template: '' }, + 'v-progress-circular': { template: '' }, + 'v-select': { template: '
' }, + 'v-spacer': { template: '' }, + 'v-table': { template: '
' }, + 'v-tooltip': { template: '
' }, + }, + }, + }); + + expect(wrapper.text()).toContain('Current customer record'); + expect(wrapper.text()).not.toContain('Legacy field'); + expect(wrapper.text()).not.toContain('Primary contact address'); + + await wrapper.get('button[data-icon="mdi-chevron-right"]').trigger('click'); + await nextTick(); + + expect(wrapper.text()).toContain('Primary contact address'); + }); +}); diff --git a/src/components/TableColumnProfiler.vue b/src/components/TableColumnProfiler.vue index 9036343f..d93054d7 100644 --- a/src/components/TableColumnProfiler.vue +++ b/src/components/TableColumnProfiler.vue @@ -186,6 +186,9 @@
{{ row.name }}
+
+ {{ row.doc }} +
makeNode(f.name, f.type, parentKey, depth)); + return (t.fields ?? []).map((f: StructField) => + makeNode(f.name, f.type, f.doc, parentKey, depth), + ); } if (t.type === 'list') { const el = t.element; if (el && typeof el === 'object') { if (el.type === 'struct') return childrenOf(el, parentKey, depth); - return [makeNode('element', el, parentKey, depth)]; + return [makeNode('element', el, undefined, parentKey, depth)]; } return []; } if (t.type === 'map') { - return [makeNode('key', t.key, parentKey, depth), makeNode('value', t.value, parentKey, depth)]; + return [ + makeNode('key', t.key, undefined, parentKey, depth), + makeNode('value', t.value, undefined, parentKey, depth), + ]; } return []; } -function makeNode(name: string, type: any, parentKey: string, parentDepth: number): SchemaNode { +function makeNode( + name: string, + type: any, + doc: string | undefined, + parentKey: string, + parentDepth: number, +): SchemaNode { const depth = parentDepth + 1; const key = `${parentKey}.${name}`; const children = childrenOf(type, key, depth); return { key, name, + doc, type: shortType(type), depth, children, @@ -680,6 +696,7 @@ const schemaTree = computed(() => { return { key: f.name, name: f.name, + doc: f.doc, type: shortType(f.type), depth: 0, children, @@ -955,6 +972,10 @@ watch( min-width: 220px; white-space: normal; } +.field-doc { + overflow-wrap: anywhere; + white-space: normal; +} .profiler-table :deep(.nested-row) td { border-bottom: none; } From ea2de8d57cb293755e4427e48450abdaf4e80dcb Mon Sep 17 00:00:00 2001 From: willemijn-zeno Date: Tue, 11 Aug 2026 12:11:15 +0200 Subject: [PATCH 2/2] test(ui): scope field doc assertions --- src/components/TableColumnProfiler.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/TableColumnProfiler.test.ts b/src/components/TableColumnProfiler.test.ts index 12258645..f6053032 100644 --- a/src/components/TableColumnProfiler.test.ts +++ b/src/components/TableColumnProfiler.test.ts @@ -94,13 +94,17 @@ describe('TableColumnProfiler', () => { }, }); - expect(wrapper.text()).toContain('Current customer record'); - expect(wrapper.text()).not.toContain('Legacy field'); - expect(wrapper.text()).not.toContain('Primary contact address'); + const initialFieldDocs = wrapper.findAll('.field-doc').map((node) => node.text()); + expect(initialFieldDocs).toContain('Current customer record'); + expect(initialFieldDocs).not.toContain('Legacy field'); + expect(initialFieldDocs).not.toContain('Primary contact address'); await wrapper.get('button[data-icon="mdi-chevron-right"]').trigger('click'); await nextTick(); - expect(wrapper.text()).toContain('Primary contact address'); + const expandedFieldDocs = wrapper.findAll('.field-doc').map((node) => node.text()); + expect(expandedFieldDocs).toContain('Current customer record'); + expect(expandedFieldDocs).not.toContain('Legacy field'); + expect(expandedFieldDocs).toContain('Primary contact address'); }); });