/** * Copyright (c) 2025-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { observer } from 'mobx-react-lite'; import React, { useState, useRef } from 'react'; import { BlankPanelPlaceholder, PanelContent, PanelHeader, PlusIcon, TimesIcon, LockIcon, TrashIcon, UploadIcon, FileImportIcon, Dialog, clsx, CustomSelectorInput, } from '@finos/legend-art'; import { RelationElement, RelationRowTestData } from '@finos/legend-graph'; import { guaranteeNonNullable } from '@finos/legend-shared'; import type { RelationElementsDataState, RelationElementState, } from '../../../../stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js'; import { ActionAlertActionType, ActionAlertType, useApplicationNavigationContext, } from '@finos/legend-application'; import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../../../__lib__/LegendStudioApplicationNavigationContext.js'; import { useEditorStore } from '../../EditorStoreProvider.js'; const NewRelationElementModal = observer( (props: { dataState: RelationElementsDataState; isReadOnly: boolean }) => { const { isReadOnly, dataState } = props; const applicationStore = dataState.editorStore.applicationStore; const availableAccessorOptions = dataState.availableAccessorOptions; const hasAccessorOptions = dataState.accessorOptions !== undefined && availableAccessorOptions.length > 0; const [selectedAccessor, setSelectedAccessor] = useState< string | undefined >(availableAccessorOptions[0]?.value); enum PathType { SCHEMA_TABLE = 'Schema and Table', } interface PathTypeOption { value: PathType; label: string; } const pathTypeOptions = Object.values(PathType).map((type) => ({ label: type, value: type, })); const [pathType, setPathType] = useState( pathTypeOptions[0], ); const onPathTypeChange = (val: PathTypeOption): void => { setPathType(val); }; const [schemaName, setSchemaName] = useState(); const [tableName, setTableName] = useState(); const changeSchemaValue: React.ChangeEventHandler = ( event, ) => { setSchemaName(event.target.value); }; const changeTableValue: React.ChangeEventHandler = ( event, ) => { setTableName(event.target.value); }; const closeModal = (): void => dataState.setShowNewRelationElementModal(false); const handleSubmit = (): void => { if (hasAccessorOptions && selectedAccessor) { const option = availableAccessorOptions.find( (o) => o.value === selectedAccessor, ); if (option) { const relationElement = new RelationElement(); relationElement.paths = [option.value]; relationElement.columns = option.columns; const emptyRow = new RelationRowTestData(); emptyRow.values = option.columns.map(() => ''); relationElement.rows = [emptyRow]; dataState.addRelationElement(relationElement); } } else { const path: string[] = []; if (pathType && schemaName && tableName) { path.push(schemaName); path.push(tableName); } const relationElement = new RelationElement(); relationElement.columns = []; relationElement.rows = [new RelationRowTestData()]; relationElement.paths = path; dataState.addRelationElement(relationElement); } closeModal(); }; return (
{ event.preventDefault(); handleSubmit(); }} className="modal modal--dark search-modal" >
Add Relation Element
{hasAccessorOptions ? (
{dataState.accessorTypeLabel}
({ value: o.value, label: o.label, }))} onChange={( val: { value: string; label: string } | null, ): void => { setSelectedAccessor(val?.value); }} value={ selectedAccessor ? { value: selectedAccessor, label: selectedAccessor, } : null } placeholder={`Select ${dataState.accessorTypeLabel ?? 'option'}...`} isClearable={false} darkMode={ !applicationStore.layoutService .TEMPORARY__isLightColorThemeEnabled } />
) : ( <>
)}
); }, ); export const RelationElementEditor = observer( (props: { relationElementState: RelationElementState; isReadOnly: boolean; hideColumnDefinitions?: boolean; }) => { const { relationElementState, isReadOnly, hideColumnDefinitions } = props; const editorStore = useEditorStore(); const embeddedData = relationElementState.relationElement; const canEditColumns = !isReadOnly && relationElementState.supportsColumnEditing; const fileInputRef = useRef(null); const addColumn = (): void => { if (canEditColumns) { const columnName = `column_${embeddedData.columns.length + 1}`; relationElementState.addColumn(columnName); } }; const removeColumn = (index: number): void => { if (canEditColumns) { relationElementState.removeColumn(index); } }; const updateColumn = (index: number, value: string): void => { if (canEditColumns && index >= 0 && index < embeddedData.columns.length) { relationElementState.updateColumn(index, value); } }; const addRow = (): void => { if (!isReadOnly) { relationElementState.addRow(); } }; const removeRow = (index: number): void => { if (!isReadOnly) { relationElementState.removeRow(index); } }; const updateCellValue = ( rowIndex: number, columnIndex: number, value: string, ): void => { if (!isReadOnly) { relationElementState.updateRow(rowIndex, columnIndex, value); } }; const handleFileUpload = ( event: React.ChangeEvent, ): void => { const file = event.target.files?.[0]; if (file && file.type === 'text/csv') { const reader = new FileReader(); reader.onload = (e) => { const csvContent = e.target?.result as string; if (csvContent) { relationElementState.importCSV(csvContent); } }; reader.readAsText(file); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const exportCSV = (): void => { const content = relationElementState.exportCSV(); const blob = new Blob([content], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'test_data.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleClearData = (): void => { editorStore.applicationStore.alertService.setActionAlertInfo({ message: 'Are you sure you want to clear all test data?', type: ActionAlertType.CAUTION, actions: [ { label: 'Confirm', type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: (): void => { relationElementState.clearAllData(); }, }, { label: 'Cancel', type: ActionAlertActionType.PROCEED, default: true, }, ], }); }; return (
Test Data ({embeddedData.rows.length} rows)
Add Column
Add Row
Upload CSV
{!hideColumnDefinitions ? (
Column Definitions
{embeddedData.columns.map((column, index) => (
updateColumn(index, e.target.value)} placeholder="Column Name" disabled={!canEditColumns} />
))}
) : null} {embeddedData.rows.length === 0 && embeddedData.columns.length === 0 ? (
No test data rows. Click "+" below to start entering data.
) : (
{embeddedData.columns.map((column, columnIndex) => ( ))} {embeddedData.rows.map((_row, rowIndex) => ( {embeddedData.columns.map((column, columnIndex) => ( ))} ))}
updateColumn(columnIndex, e.target.value) } disabled={!canEditColumns} title={column} />
updateCellValue( rowIndex, columnIndex, e.target.value, ) } disabled={isReadOnly} className="relation-test-data-editor__data-input" />
)}
Export CSV
); }, ); export const RelationElementsDataEditor = observer( (props: { dataState: RelationElementsDataState; isReadOnly: boolean; isSharedData?: boolean | undefined; hideColumnDefinitions?: boolean; }) => { const { dataState, isReadOnly, isSharedData, hideColumnDefinitions } = props; useApplicationNavigationContext( LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.EMBEDDED_DATA_RELATIONAL_EDITOR, ); const addRelationElement = (): void => { const doAdd = (): void => { if ( dataState.accessorOptions !== undefined && dataState.availableAccessorOptions.length === 0 ) { dataState.editorStore.applicationStore.notificationService.notifyWarning( `All referenced ${dataState.accessorTypeLabel ?? 'item'}s' data is already present`, ); return; } dataState.setShowNewRelationElementModal(true); }; if (dataState.refreshAccessorOptions) { dataState.refreshAccessorOptions().then(doAdd).catch(doAdd); } else { doAdd(); } }; const changeRelationElement = ( newRelationElement: RelationElementState, ): void => { dataState.setActiveRelationElement(newRelationElement); }; return (
{isReadOnly && (
)}
{dataState.label()}
{dataState.relationElementStates.map((relationElementState) => (
changeRelationElement(relationElementState) } className={clsx('service-editor__tab', { 'service-editor__tab--active': relationElementState === dataState.activeRelationElement, })} > {relationElementState.relationElement.paths.length > 1 ? ( {relationElementState.relationElement.paths.join('.')} ) : ( {relationElementState.relationElement.paths[0]} )} {!isReadOnly && !isSharedData && ( )}
))}
{!isSharedData && ( )}
{dataState.relationElementStates.length === 0 || dataState.activeRelationElement === undefined ? ( ) : ( )}
{dataState.showNewRelationElementModal && ( )}
); }, );