/** * Copyright (c) 2020-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 { BlankPanelPlaceholder, CaretDownIcon, clsx, ContextMenu, CustomSelectorInput, Dialog, ControlledDropdownMenu, InfoCircleIcon, MaskIcon, MenuContent, MenuContentItem, Modal, ModalBody, ModalFooter, ModalFooterButton, ModalHeader, PanelLoadingIndicator, PlusIcon, PURE_ConnectionIcon, RefreshIcon, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, ResizablePanelSplitterLine, TimesIcon, } from '@finos/legend-art'; import { type IdentifiedConnection, type ConnectionTestData, type DataElement, type ValueSpecification, getAllIdentifiedServiceConnections, DataElementReference, PackageableElementExplicitReference, Column, } from '@finos/legend-graph'; import { observer } from 'mobx-react-lite'; import { forwardRef, useState } from 'react'; import { createMockPrimitiveValueSpecificationFromRelationalDataType, type ConnectionTestDataState, type RowIdentifierState, type ServiceTestDataState, type TableRowIdentifierState, } from '../../../../../stores/editor/editor-state/element-editor-state/service/testable/ServiceTestDataState.js'; import type { EmbeddedDataTypeOption } from '../../../../../stores/editor/editor-state/element-editor-state/data/DataEditorState.js'; import { EmbeddedDataEditor } from '../../data-editor/EmbeddedDataEditor.js'; import { EmbeddedDataType } from '../../../../../stores/editor/editor-state/ExternalFormatState.js'; import { flowResult } from 'mobx'; import { ActionAlertActionType, ActionAlertType, useApplicationStore, } from '@finos/legend-application'; import { buildElementOption } from '@finos/legend-lego/graph-editor'; import { filterByType, guaranteeNonNullable, prettyCONSTName, } from '@finos/legend-shared'; import type { DSL_Data_LegendStudioApplicationPlugin_Extension } from '../../../../../stores/extensions/DSL_Data_LegendStudioApplicationPlugin_Extension.js'; import { useEditorStore } from '../../../EditorStoreProvider.js'; import { BasicValueSpecificationEditor, LambdaParameterValuesEditor, } from '@finos/legend-query-builder'; import { LEGEND_STUDIO_DOCUMENTATION_KEY } from '../../../../../__lib__/LegendStudioDocumentation.js'; import type { TableOption } from '../../data-editor/RelationalCSVDataEditor.js'; import { getPrimitiveTypeFromRelationalType } from '../../../../../stores/editor/utils/MockDataUtils.js'; export interface ColumnOption { value: Column; label: string; } export const RowIdentifierEditor = observer( (props: { tableRowIdentifierState: TableRowIdentifierState; rowIdentifierState: RowIdentifierState; }) => { const { tableRowIdentifierState, rowIdentifierState } = props; const applicationStore = useApplicationStore(); const columnOptions = tableRowIdentifierState.table.columns .filter(filterByType(Column)) .map((_c) => ({ label: _c.name, value: _c, })); const changeColumn = (val: ColumnOption): void => { if (rowIdentifierState.column.name !== val.value.name) { const valueSpec = createMockPrimitiveValueSpecificationFromRelationalDataType( guaranteeNonNullable(rowIdentifierState.column.type), tableRowIdentifierState.connectionTestDataState.editorStore .graphManagerState.graph, tableRowIdentifierState.connectionTestDataState.editorStore .changeDetectionState.observerContext, ); rowIdentifierState.updateRowIdentifierValue(valueSpec); rowIdentifierState.updateRowIdentifierColumn(val.value); } }; const resetNode = (): void => { const valueSpec = createMockPrimitiveValueSpecificationFromRelationalDataType( guaranteeNonNullable(rowIdentifierState.column.type), tableRowIdentifierState.connectionTestDataState.editorStore .graphManagerState.graph, tableRowIdentifierState.connectionTestDataState.editorStore .changeDetectionState.observerContext, ); rowIdentifierState.updateRowIdentifierValue(valueSpec); }; return (
{ rowIdentifierState.updateRowIdentifierValue(val); }} graph={ tableRowIdentifierState.connectionTestDataState.editorStore .graphManagerState.graph } observerContext={ tableRowIdentifierState.connectionTestDataState.editorStore .changeDetectionState.observerContext } typeCheckOption={{ expectedType: getPrimitiveTypeFromRelationalType( guaranteeNonNullable(rowIdentifierState.column.type), ), }} resetValue={resetNode} />
); }, ); export const TableRowIdentifierEditor = observer( (props: { connectionTestDataState: ConnectionTestDataState; tableRowIdentifierState: TableRowIdentifierState; }) => { const { connectionTestDataState, tableRowIdentifierState } = props; const applicationStore = useApplicationStore(); const tables = connectionTestDataState.getAvailableTables(); const tableOptions = tables.map((_t) => ({ label: `${_t.schema.name}.${_t.name}`, value: _t, })); const changeTable = (val: TableOption): void => { if (tableRowIdentifierState.table !== val.value) { tableRowIdentifierState.updateTable(val.value); tableRowIdentifierState.setNewRowIdentifierState([]); } }; const addNewRow = (): void => { tableRowIdentifierState.addNewRowIdentifierState( guaranteeNonNullable( tableRowIdentifierState.table.columns.filter(filterByType(Column))[0], ), ); }; return (
Table
create seed data below based on root tables
Seed Data
create value for primary key column
{tableRowIdentifierState.rowIdentifierStates.map( (rowIdentifierState) => ( ), )}
); }, ); export const SeedDataInputModal = observer( (props: { connectionTestDataState: ConnectionTestDataState }) => { const { connectionTestDataState } = props; const applicationStore = useApplicationStore(); const useSeedDataInputModal = connectionTestDataState.useSeedDataInputModal; const closeModal = (): void => connectionTestDataState.setUseSeedDataInputModal(false); const generateWithSeedData = (): void => { closeModal(); flowResult(connectionTestDataState.generateTestDataWithSeedData()).catch( applicationStore.alertUnhandledError, ); }; const addNewTable = (): void => { const tables = connectionTestDataState.getAvailableTables(); if (tables[0]) { connectionTestDataState.addNewTableIdentifierState(tables[0]); } }; return ( {connectionTestDataState.tableRowIdentifierStates.map( (tableRowIdentifierState) => ( ), )} ); }, ); export const UseDataElementModal = observer( (props: { connectionTestDataState: ConnectionTestDataState }) => { const { connectionTestDataState } = props; const editorStore = connectionTestDataState.editorStore; const applicationStore = editorStore.applicationStore; const useSharedModal = connectionTestDataState.useSharedModal; const closeModal = (): void => connectionTestDataState.setUseSharedModal(false); const dataElements = connectionTestDataState.editorStore.graphManagerState.graph.dataElements; const [dataElement, setDataElement] = useState(dataElements[0]); const dataElementOptions = editorStore.graphManagerState.usableDataElements.map(buildElementOption); const selectedDataElement = dataElement ? buildElementOption(dataElement) : null; const onDataElementChange = (val: { label: string; value?: DataElement; }): void => { if (val.value !== selectedDataElement?.value && val.value) { setDataElement(val.value); } }; const change = (): void => { if (dataElement) { const value = new DataElementReference(); value.dataElement = PackageableElementExplicitReference.create(dataElement); connectionTestDataState.changeEmbeddedData(value); } closeModal(); }; const isReadOnly = connectionTestDataState.testDataState.testSuiteState.testableState .serviceEditorState.isReadOnly; return (
Data Element
Change
); }, ); export const ConnectionTestDataEditor = observer( (props: { connectionTestDataState: ConnectionTestDataState }) => { const { connectionTestDataState } = props; const applicationStore = useApplicationStore(); const USER_ATTESTATION_MESSAGE = 'Data generated has not been anonmyized. I attest that I am aware of the sensitive data leakage risk when using real data in tests.'; const isReadOnly = connectionTestDataState.testDataState.testSuiteState.testableState .serviceEditorState.isReadOnly; const dataElements = connectionTestDataState.editorStore.graphManagerState.graph.dataElements; const openShared = (): void => { if (dataElements.length) { connectionTestDataState.setUseSharedModal(true); } }; // test data const anonymizeGeneratedData = connectionTestDataState.anonymizeGeneratedData; const toggleAnonymizeGeneratedData = (): void => { connectionTestDataState.setAnonymizeGeneratedData( !anonymizeGeneratedData, ); }; const confirmGenerateUnAnonmyizedData = (): void => { applicationStore.alertService.setActionAlertInfo({ message: USER_ATTESTATION_MESSAGE, type: ActionAlertType.CAUTION, actions: [ { label: 'Accept', type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: applicationStore.guardUnhandledError(() => flowResult(connectionTestDataState.generateTestData()), ), }, { label: 'Decline', type: ActionAlertActionType.PROCEED, default: true, }, ], }); }; const generateTestData = (): void => { if (!anonymizeGeneratedData) { confirmGenerateUnAnonmyizedData(); } else { flowResult(connectionTestDataState.generateTestData()).catch( applicationStore.alertUnhandledError, ); } }; const generateTestDataWithSeedData = (): void => { connectionTestDataState.setUseSeedDataInputModal(true); connectionTestDataState.setNewTableIdentifierState([]); const table = connectionTestDataState.getAvailableTables()[0]; if (table) { connectionTestDataState.addNewTableIdentifierState(table); } }; const generateQuerySchemas = (): void => { flowResult(connectionTestDataState.generateQuerySchemas()).catch( applicationStore.alertUnhandledError, ); }; return (
data
Generate Query Schemas Generate with Seed Data (Beta) } menuProps={{ anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, }} >
{connectionTestDataState.parametersState.parameterValuesEditorState .showModal && ( )} {connectionTestDataState.useSharedModal && ( )} {connectionTestDataState.useSeedDataInputModal && ( )}
); }, ); const ConnectionTestDataContextMenu = observer( forwardRef< HTMLDivElement, { connectionTestData: ConnectionTestData; deleteConnectionData: () => void; } >(function TestContainerContextMenu(props, ref) { const { deleteConnectionData } = props; const remove = (): void => deleteConnectionData(); return ( Delete ); }), ); const ConnectionTestDataItem = observer( (props: { serviceTestDataState: ServiceTestDataState; connectionTestData: ConnectionTestData; }) => { const { connectionTestData, serviceTestDataState } = props; const [isSelectedFromContextMenu, setIsSelectedFromContextMenu] = useState(false); const isReadOnly = serviceTestDataState.testSuiteState.testableState.serviceEditorState .isReadOnly; const openConnectionTestData = (): void => serviceTestDataState.openConnectionTestData(connectionTestData); const isActive = serviceTestDataState.selectedDataState?.connectionData === connectionTestData; const deleteConnectionTestData = (): void => serviceTestDataState.deleteConnectionTestData(connectionTestData); const onContextMenuOpen = (): void => setIsSelectedFromContextMenu(true); const onContextMenuClose = (): void => setIsSelectedFromContextMenu(false); return ( } menuProps={{ elevation: 7 }} onOpen={onContextMenuOpen} onClose={onContextMenuClose} > ); }, ); export const NewConnectionDataModal = observer( (props: { testDataState: ServiceTestDataState }) => { const { testDataState } = props; const editorStore = useEditorStore(); const applicationStore = editorStore.applicationStore; const dataElementOptions = editorStore.graphManagerState.usableDataElements.map(buildElementOption); const newConnectionState = testDataState.newConnectionDataState; const dataElement = newConnectionState.dataElement; const selectedDataElement = dataElement ? buildElementOption(dataElement) : null; const onDataElementChange = (val: { label: string; value?: DataElement; }): void => { if (val.value !== selectedDataElement?.value && val.value) { newConnectionState.setDataElement(val.value); } }; const isReadOnly = testDataState.testSuiteState.testableState.serviceEditorState.isReadOnly; const service = testDataState.testSuiteState.testableState.serviceEditorState.service; const closeModal = (): void => newConnectionState.setModal(false); const allIdentifiedConnections = getAllIdentifiedServiceConnections(service); const connectionOptions = allIdentifiedConnections.map((e) => ({ label: e.id, value: e, })); const selectedConnection = newConnectionState.connection ? { label: newConnectionState.connection.id, value: newConnectionState.connection, } : undefined; const isDisabled = isReadOnly || !allIdentifiedConnections.length || Boolean( testDataState.testData.connectionsTestData.find( (c) => c.connectionId === selectedConnection?.value.id, ), ); const onConnectionSelectionChange = (val: { label: string; value?: IdentifiedConnection; }): void => { if (val.value === undefined) { newConnectionState.setConnection(undefined); } else if (val.value !== selectedConnection?.value) { newConnectionState.setConnection(val.value); } }; // external format const selectedEmbeddedType = newConnectionState.embeddedDataType ? { label: prettyCONSTName(newConnectionState.embeddedDataType.value), value: newConnectionState.embeddedDataType.value, } : undefined; const extraOptionTypes = editorStore.pluginManager .getApplicationPlugins() .flatMap( (plugin) => ( plugin as DSL_Data_LegendStudioApplicationPlugin_Extension ).getExtraEmbeddedDataTypeOptions?.() ?? [], ); const embeddedOptions = [ ...Object.values(EmbeddedDataType).map((typeOption) => ({ label: prettyCONSTName(typeOption), value: typeOption, })), ...extraOptionTypes, ]; const onEmbeddedTypeChange = (val: EmbeddedDataTypeOption | null): void => { if (!val) { newConnectionState.setEmbeddedDataType(undefined); } else { newConnectionState.setEmbeddedDataType(val); } }; return (
{ event.preventDefault(); const connection = newConnectionState.connection; const data = newConnectionState.embeddedDataType; if (connection && data) { testDataState.createConnectionTestData(); closeModal(); } }} className="modal service-test-data-modal modal--dark" >
Connection ID
Connection in runtime to povide test data for
Data Type
Test data type that will be loaded to your test connection
{selectedEmbeddedType?.value === EmbeddedDataType.DATA_ELEMENT && (
Data Element
)}
); }, ); export const ServiceTestDataEditor = observer( (props: { testDataState: ServiceTestDataState }) => { const { testDataState } = props; const applicationStore = useApplicationStore(); const testData = testDataState.testData; const newConnectionDataState = testDataState.newConnectionDataState; const identifedConnections = getAllIdentifiedServiceConnections( testDataState.testSuiteState.testableState.service, ); const selectedDataState = testDataState.selectedDataState; const hideExplorer = identifedConnections.length === 1 && testData.connectionsTestData.length === 1 && testData.connectionsTestData[0]?.connectionId === identifedConnections[0]?.id; const addConnectionTestData = (): void => { testDataState.newConnectionDataState.openModal(); }; const seeDocumentation = (): void => applicationStore.assistantService.openDocumentationEntry( LEGEND_STUDIO_DOCUMENTATION_KEY.QUESTION_HOW_TO_WRITE_SERVICE_CONNECTION_TEST_DATA, ); return (
Test DATA
{!testData.connectionsTestData.length && ( )} {hideExplorer && selectedDataState ? ( <> ) : (
connections
{testData.connectionsTestData.map((connectionTestData) => ( ))}
{testDataState.selectedDataState && ( )} {newConnectionDataState.showModal && ( )}
)}
); }, );