import React from 'react'; import { Body, Cell, Checkbox, Row, Table, TableHeader, css, cx, spacing, Label, palette, Tooltip, Icon, IconButton, Select, Option, useDarkMode, } from '@mongodb-js/compass-components'; import { createDebug } from '../utils/logger'; import type { CSVParsableFieldType, CSVField } from '../csv/csv-types'; import { CSVFieldTypeLabels } from '../csv/csv-types'; import { findBrokenCSVTypeExample } from '../csv/csv-utils'; const debug = createDebug('import-preview'); // the max length of a value in the preview const MAX_VALUE_LENGTH = 80; // the max length in a cell in the preview table const MAX_PREVIEW_LENGTH = 1000; const columnHeaderStyles = css({ display: 'flex', gap: spacing[1], minWidth: spacing[6] * 2, flexDirection: 'column', alignItems: 'flex-start', }); const warningCellStylesLight = css({ backgroundColor: palette.yellow.light3, }); const warningCellStylesDark = css({ backgroundColor: palette.yellow.dark3, }); const columnNameStyles = css({ display: 'flex', flexDirection: 'row', }); const fieldPathHeaderStyles = css({ maxWidth: spacing[7] * 3, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', }); const cellContainerStyles = css({ padding: `${spacing[1]}px ${spacing[2]}px`, }); const cellStyles = css({ maxWidth: spacing[7] * 3, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', }); const cellUncheckedStyles = css({ opacity: 0.4, }); const rowIndexStyles = css({ minWidth: 0, color: palette.gray.base, }); const fieldTypeContainerStyles = css({ display: 'flex', alignItems: 'center', gap: spacing[1], }); const infoIconCommonStyles = css({ // Hack: Align the icon relative to the SelectBox. marginBottom: `-${spacing[1]}px`, marginTop: `-${spacing[1]}px`, }); const infoIconLightStyles = css({ color: palette.gray.dark2, }); const infoIconDarkStyles = css({ color: palette.gray.light2, }); const warningIconCommonStyles = css(infoIconCommonStyles, { marginLeft: spacing[1], marginRight: spacing[1], paddingTop: spacing[1], }); const warningIconLightStyles = css({ color: palette.red.base, }); const warningIconDarkStyles = css({ color: palette.red.light1, }); const typesListStyles = css({ margin: `${spacing[3]}px 0`, }); const selectStyles = css({ minWidth: spacing[3] * 9, }); const arrayTextStyles = css({ fontWeight: 'normal', whiteSpace: 'nowrap', }); function fieldTypeName(type: CSVParsableFieldType | 'undefined') { if (type === 'undefined') { return 'Blank'; } return CSVFieldTypeLabels[type]; } function needsMixedWarning(field: Field) { // Only show the warning for mixed and number types and once the user manually // changed the type, make the warning go away return ( field.result && ['mixed', 'number'].includes(field.result.detected) && field.result.detected === field.type ); } function needsTypeWarning(field: Field) { return !!( field.result && findBrokenCSVTypeExample(field.result.types, field.type) ); } function needsWarning(field: Field) { return needsMixedWarning(field) || needsTypeWarning(field); } function SelectFieldType({ fieldPath, selectedType, onChange, }: { fieldPath: string; selectedType: CSVParsableFieldType; onChange: (type: string) => void; }) { return ( ); } type Field = { isArray?: boolean; path: string; type: CSVParsableFieldType; checked: boolean; result?: CSVField; }; function InfoIcon() { const darkMode = useDarkMode(); return ( ); } function WarningIcon() { const darkMode = useDarkMode(); return (
); } function MixedWarning({ result, selectedType, children: triggerChildren, }: { result: CSVField; selectedType: CSVParsableFieldType; children: React.ReactElement; }) { return ( (
{tooltipChildren} {triggerChildren}
)} > <> This field has{' '} {selectedType === 'number' ? 'mixed numeric types' : 'mixed data types'} : To standardize your data, select a different type.
); } function TypeWarning({ result, selectedType, children: triggerChildren, }: { result: CSVField; selectedType: CSVParsableFieldType; children: React.ReactElement; }) { const example = findBrokenCSVTypeExample(result.types, selectedType); if (!example) { return null; } const value = example.firstValue.length < MAX_VALUE_LENGTH ? example.firstValue : `${example.firstValue.slice(0, MAX_VALUE_LENGTH)}…`; return ( (
{tooltipChildren} {triggerChildren}
)} > <> This field has these detected types: Row {example.firstRowIndex + 1} contains the value{' '} "{value}". This will cause an error for type{' '} {CSVFieldTypeLabels[selectedType]}.
); } function capStringLength(value: any): string { if (typeof value !== 'string') { return ''; } if (value.length > MAX_PREVIEW_LENGTH) { return value.substring(0, MAX_PREVIEW_LENGTH) + '…'; } return value; } function FieldTypeHeading({ field, onFieldCheckedChanged, setFieldType, }: { field: Field; onFieldCheckedChanged: (fieldPath: string, checked: boolean) => void; setFieldType: (fieldPath: string, fieldType: string) => void; }) { const children = (
) => onFieldCheckedChanged(field.path, !!e.target.checked) } />
{field.isArray && Array of} setFieldType(field.path, newType)} /> {field.result && needsMixedWarning(field) && } {field.result && needsTypeWarning(field) && }
); if (field.result) { if (needsMixedWarning(field)) { return ( {children} ); } else if (needsTypeWarning(field)) { return ( {children} ); } } return children; } function ImportPreview({ fields, values, onFieldCheckedChanged, setFieldType, loaded, }: { fields: Field[]; values: string[][]; onFieldCheckedChanged: (fieldPath: string, checked: boolean) => void; setFieldType: (fieldPath: string, fieldType: string) => void; loaded: boolean; }) { const darkMode = useDarkMode(); if (!loaded) { debug('Preview unavailable: not loaded yet'); return null; } if (!Array.isArray(fields) || !Array.isArray(values)) { debug('Preview unavailable: Fields or values is not an array', { fields, values, }); return null; } const gapOrFields: (string | Field)[] = ['', ...fields]; const warningCellStyles = darkMode ? warningCellStylesDark : warningCellStylesLight; return ( { if (typeof field !== 'string' && 'path' in field) { return ( } /> ); } else { return ( ); } })} > {({ datum: values, index: rowIndex }) => ( {rowIndex + 1} {fields.map(({ path }, fieldIndex) => (
{values[fieldIndex] === '' ? ( empty string ) : ( capStringLength(values[fieldIndex]) )}
))}
)}
); } export { ImportPreview };