Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/components/TableColumnProfiler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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: '<div><slot /></div>' },
'v-btn': {
props: ['icon'],
emits: ['click'],
template: '<button :data-icon="icon" @click="$emit(\'click\')"><slot /></button>',
},
'v-btn-toggle': { template: '<div><slot /></div>' },
'v-card': { template: '<div><slot /></div>' },
'v-card-text': { template: '<div><slot /></div>' },
'v-card-title': { template: '<div><slot /></div>' },
'v-chip': { template: '<span><slot /></span>' },
'v-dialog': { template: '<div><slot /></div>' },
'v-divider': { template: '<div />' },
'v-icon': { template: '<span><slot /></span>' },
'v-progress-circular': { template: '<span />' },
'v-select': { template: '<div />' },
'v-spacer': { template: '<span />' },
'v-table': { template: '<div><slot /></div>' },
'v-tooltip': { template: '<div><slot /></div>' },
},
},
});

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();

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');
});
});
31 changes: 26 additions & 5 deletions src/components/TableColumnProfiler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@

<div class="flex-grow-1 ml-2" style="min-width: 0">
<div class="font-mono font-weight-medium">{{ row.name }}</div>
<div v-if="row.doc" class="field-doc text-caption text-medium-emphasis">
{{ row.doc }}
</div>
</div>
<v-btn
v-if="row.profilable && hasChart(row.name)"
Expand Down Expand Up @@ -375,7 +378,7 @@ import { useUserStore } from '../stores/user';
import { useVisualStore } from '../stores/visual';
import { useLoQEStore } from '../stores/loqe';
import EntityTagsChips from './EntityTagsChips.vue';
import type { TableMetadata } from '../gen/iceberg/types.gen';
import type { StructField, TableMetadata } from '../gen/iceberg/types.gen';
import type { TargetTag } from '../gen/management/types.gen';

const props = defineProps<{
Expand Down Expand Up @@ -628,6 +631,7 @@ function shortType(t: any): string {
interface SchemaNode {
key: string;
name: string;
doc?: string;
type: string;
depth: number;
children: SchemaNode[];
Expand All @@ -641,29 +645,41 @@ interface SchemaNode {
function childrenOf(t: any, parentKey: string, depth: number): SchemaNode[] {
if (!t || typeof t !== 'object') return [];
if (t.type === 'struct') {
return (t.fields ?? []).map((f: any) => 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,
Expand All @@ -680,6 +696,7 @@ const schemaTree = computed<SchemaNode[]>(() => {
return {
key: f.name,
name: f.name,
doc: f.doc,
type: shortType(f.type),
depth: 0,
children,
Expand Down Expand Up @@ -951,6 +968,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;
}
Expand Down