diff --git a/app/gui/src/project-view/components/shared/AgGridTableView.vue b/app/gui/src/project-view/components/shared/AgGridTableView.vue index c33df0fe687a..786c74ff9f5d 100644 --- a/app/gui/src/project-view/components/shared/AgGridTableView.vue +++ b/app/gui/src/project-view/components/shared/AgGridTableView.vue @@ -84,8 +84,6 @@ const _props = defineProps<{ suppressMoveWhenColumnDragging?: boolean textFormatOption?: TextFormatOptions processDataFromClipboard?: (params: ProcessDataFromClipboardParams) => string[][] | null - pinnedTopRowData?: TData[] - pinnedRowHeightMultiplier?: number }>() const emit = defineEmits<{ cellEditingStarted: [event: CellEditingStartedEvent] @@ -108,10 +106,6 @@ function onGridReady(event: GridReadyEvent) { } function getRowHeight(params: RowHeightParams): number { - if (params.node.rowPinned === 'top') { - return DEFAULT_ROW_HEIGHT * (_props.pinnedRowHeightMultiplier ?? 2) - } - if (_props.textFormatOption === 'off') { return DEFAULT_ROW_HEIGHT } @@ -274,7 +268,6 @@ const { AgGridVue } = await import('ag-grid-vue3') :suppressDragLeaveHidesColumns="suppressDragLeaveHidesColumns" :suppressMoveWhenColumnDragging="suppressMoveWhenColumnDragging" :processDataFromClipboard="processDataFromClipboard" - :pinnedTopRowData="pinnedTopRowData" @gridReady="onGridReady" @firstDataRendered="updateColumnWidths" @rowDataUpdated="updateColumnWidths($event), emit('rowDataUpdated', $event)" diff --git a/app/gui/src/project-view/components/visualizations/TableVisualisationTooltip.ts b/app/gui/src/project-view/components/visualizations/TableVisualisationTooltip.ts new file mode 100644 index 000000000000..b9e5e8ca9cbb --- /dev/null +++ b/app/gui/src/project-view/components/visualizations/TableVisualisationTooltip.ts @@ -0,0 +1,68 @@ +import { ITooltipComp, ITooltipParams } from '@ag-grid-community/core' + +/** + * Custom tooltip for table visualization. + */ +export class TableVisualisationTooltip implements ITooltipComp { + eGui!: HTMLElement + + /** + * Initializes the tooltip with the provided parameters. + * @param params The tooltip parameters: the data quality metrics, total row count, + * and a flag whether to show/hide the data quality indicators. + */ + init( + params: ITooltipParams & { + numberOfNothing: number + numberOfWhitespace: number + total: number + showDataQuality: boolean + }, + ) { + this.eGui = document.createElement('div') + + Object.assign(this.eGui.style, { + backgroundColor: '#f5f5f5', + border: '1px solid #c0c0c0', + padding: '10px', + boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.15)', + borderRadius: '4px', + fontFamily: 'Arial, sans-serif', + color: '#333', + }) + + const getPercentage = (value: number) => ((value / params.total) * 100).toFixed(2) + const getDisplay = (value: number) => (value > 0 ? 'block' : 'none') + const createIndicator = (value: number) => { + const color = + value < 33 ? 'green' + : value < 66 ? 'orange' + : 'red' + return `
` + } + + const dataQualityTemplate = ` +
+ Nulls/Nothing: ${getPercentage(params.numberOfNothing)}% ${createIndicator(+getPercentage(params.numberOfWhitespace))} +
+
+ Trailing/Leading Whitespace: ${getPercentage(params.numberOfWhitespace)}% ${createIndicator(+getPercentage(params.numberOfWhitespace))} +
+ ` + + this.eGui.innerHTML = ` +
Column value type: ${params.value}
+
+ Data Quality Indicators + ${dataQualityTemplate} +
+ ` + } + + /** + * Returns the tooltip DOM element. + */ + getGui() { + return this.eGui + } +} diff --git a/app/gui/src/project-view/components/visualizations/TableVisualization.vue b/app/gui/src/project-view/components/visualizations/TableVisualization.vue index 5cb7888105e6..5ee4d7ab6e49 100644 --- a/app/gui/src/project-view/components/visualizations/TableVisualization.vue +++ b/app/gui/src/project-view/components/visualizations/TableVisualization.vue @@ -14,6 +14,7 @@ import type { SortChangedEvent, } from 'ag-grid-enterprise' import { computed, onMounted, ref, shallowRef, watchEffect, type Ref } from 'vue' +import { TableVisualisationTooltip } from './TableVisualisationTooltip' export const name = 'Table' export const icon = 'table' @@ -125,9 +126,7 @@ const defaultColDef: Ref = ref({ filter: true, resizable: true, minWidth: 25, - cellRenderer: (params: ICellRendererParams) => { - return params.node.rowPinned === 'top' ? customCellRenderer(params) : cellRenderer(params) - }, + cellRenderer: cellRenderer, cellClass: cellClass, contextMenuItems: [commonContextMenuActions.copy, 'copyWithHeaders', 'separator', 'export'], } satisfies ColDef) @@ -151,47 +150,6 @@ const selectableRowLimits = computed(() => { return defaults }) -const pinnedTopRowData = computed(() => { - if (typeof props.data === 'object' && 'data_quality_pairs' in props.data) { - const data_ = props.data - const headers = data_.header - const numberOfNothing = data_.data_quality_pairs!.number_of_nothing - const numberOfWhitespace = data_.data_quality_pairs!.number_of_whitespace - const total = data_.all_rows_count as number - if (headers?.length) { - const pairs: Record = headers.reduce( - (obj: any, key: string, index: number) => { - obj[key] = { - numberOfNothing: numberOfNothing[index], - numberOfWhitespace: numberOfWhitespace[index], - total, - } - return obj - }, - {}, - ) - return [{ [INDEX_FIELD_NAME]: 'Data Quality', ...pairs }] - } - return [ - { - [INDEX_FIELD_NAME]: 'Data Quality', - Value: { - numberOfNothing: numberOfNothing[0] ?? null, - numberOfWhitespace: numberOfWhitespace[0] ?? null, - total, - }, - }, - ] - } - return [] -}) - -const pinnedRowHeight = computed(() => { - const valueTypes = - typeof props.data === 'object' && 'value_type' in props.data ? props.data.value_type : [] - return valueTypes.some((t) => t.constructor === 'Char' || t.constructor === 'Mixed') ? 2 : 1 -}) - const newNodeSelectorValues = computed(() => { let tooltipValue let headerName @@ -334,39 +292,6 @@ function cellClass(params: CellClassParams) { return null } -const createVisual = (value: number) => { - let color - if (value < 33) { - color = 'green' - } else if (value < 66) { - color = 'orange' - } else { - color = 'red' - } - return ` -
- ` -} - -const customCellRenderer = (params: ICellRendererParams) => { - if (params.node.rowPinned === 'top') { - const nothingPerecent = (params.value.numberOfNothing / params.value.total) * 100 - const wsPerecent = (params.value.numberOfWhitespace / params.value.total) * 100 - const nothingVisibility = params.value.numberOfNothing === null ? 'hidden' : 'visible' - const whitespaceVisibility = params.value.numberOfWhitespace === null ? 'hidden' : 'visible' - - return `
-
- Nulls/Nothing: ${nothingPerecent.toFixed(2)}% ${createVisual(nothingPerecent)} -
-
- Trailing/Leading Whitespace: ${wsPerecent.toFixed(2)}% ${createVisual(wsPerecent)} -
-
` - } - return null -} - function cellRenderer(params: ICellRendererParams) { // Convert's the value into a display string. if (params.value === null) return 'Nothing' @@ -391,34 +316,67 @@ function addRowIndex(data: object[]): object[] { return data.map((row, i) => ({ [INDEX_FIELD_NAME]: i, ...row })) } -function toField(name: string, valueType?: ValueType | null | undefined): ColDef { - const valType = valueType ? valueType.constructor : null - const displayValue = valueType ? valueType.display_text : null - let icon - switch (valType) { +function getValueTypeIcon(valueType: string) { + switch (valueType) { case 'Char': - icon = 'text3' - break + return 'text3' case 'Boolean': - icon = 'check' - break + return 'check' case 'Integer': case 'Float': case 'Decimal': case 'Byte': - icon = 'math' - break + return 'math' case 'Date': case 'Date_Time': - icon = 'calendar' - break + return 'calendar' case 'Time': - icon = 'time' - break + return 'time' case 'Mixed': - icon = 'mixed' + return 'mixed' } - const svgTemplate = ` ` +} + +/** + * Generates the column definition for the table vizulization, including displaying the data value type and + * data quality indicators. + * @param name - The name which will be displayed in the table header and used to idenfiy the column. + * @param [options.index] - The index of column the corresponds to the data in the `dataQuality` arrays + * (`number_of_nothing` and `number_of_whitespace`). This identifies the correct indicators for each column + * to be displayed in the toolip. If absent the data quality metrics will not be shown. + * @param [options.valueType] - The data type of the column, displayed as an icon + * and in text within the tooltip. If absent the value type icon and text will not be shown. + */ +function toField( + name: string, + options: { index?: number; valueType?: ValueType | null | undefined } = {}, +): ColDef { + const { index, valueType } = options + + const displayValue = valueType ? valueType.display_text : null + const icon = valueType ? getValueTypeIcon(valueType.constructor) : null + + const dataQuality = + typeof props.data === 'object' && 'data_quality_pairs' in props.data ? + props.data.data_quality_pairs + // eslint-disable-next-line camelcase + : { number_of_nothing: [], number_of_whitespace: [] } + + const nothingIsNonZero = + index != null && dataQuality?.number_of_nothing ? + (dataQuality.number_of_nothing[index] ?? 0) > 0 + : false + + const whitespaceIsNonZero = + index != null && dataQuality?.number_of_nothing ? + (dataQuality.number_of_whitespace[index] ?? 0) > 0 + : false + + const showDataQuality = nothingIsNonZero || whitespaceIsNonZero + + const getSvgTemplate = (icon: string) => + ` ` + const svgTemplateWarning = showDataQuality ? getSvgTemplate('warning') : '' const menu = ` ` const sort = ` @@ -427,23 +385,33 @@ function toField(name: string, valueType?: ValueType | null | undefined): ColDef ` + + const styles = 'display:flex; flex-direction:row; justify-content:space-between; width:inherit;' const template = icon ? - ` ${name} ${menu} ${sort} ${svgTemplate}` - : `${name} ${menu} ${sort}` + ` ${name} ${menu} ${sort} ${getSvgTemplate(icon)} ${svgTemplateWarning}` + : `${name} ${menu} ${sort} ${svgTemplateWarning}` + return { field: name, headerComponentParams: { template, setAriaSort: () => {}, }, + tooltipComponent: TableVisualisationTooltip, headerTooltip: displayValue ? displayValue : '', + tooltipComponentParams: { + numberOfNothing: index != null ? dataQuality.number_of_nothing[index] : null, + numberOfWhitespace: index != null ? dataQuality.number_of_whitespace[index] : null, + total: typeof props.data === 'object' ? props.data.all_rows_count : 0, + showDataQuality, + }, } } -function toRowField(name: string, valueType?: ValueType | null | undefined) { +function toRowField(name: string, index: number, valueType?: ValueType | null | undefined) { return { - ...toField(name, valueType), + ...toField(name, { index, valueType }), cellDataType: false, } } @@ -546,7 +514,7 @@ watchEffect(() => { const keys = new Set() for (const val of data_.json) { if (val != null) { - Object.keys(val).forEach((k) => { + Object.keys(val).forEach((k, i) => { if (!keys.has(k)) { keys.add(k) columnDefs.value.push(toField(k)) @@ -583,9 +551,9 @@ watchEffect(() => { return toLinkField(v, data_.get_child_node_action, data_.link_value_type) } if (config.nodeType === ROW_NODE_TYPE) { - return toRowField(v, valueType) + return toRowField(v, i, valueType) } - return toField(v, valueType) + return toField(v, { index: i, valueType }) }) ?? [] columnDefs.value = @@ -726,8 +694,6 @@ config.setToolbar( :rowData="rowData" :defaultColDef="defaultColDef" :textFormatOption="textFormatterSelected" - :pinnedTopRowData="pinnedTopRowData" - :pinnedRowHeightMultiplier="pinnedRowHeight" @sortOrFilterUpdated="(e) => checkSortAndFilter(e)" /> @@ -775,4 +741,11 @@ config.setToolbar( display: flex; flex-direction: row; } + +.ag-header-cell .myclass { + display: flex; + flex-direction: row; + justify-content: space-between; + width: inherit; +}