@finos/legend-application-studio 28.21.13 → 28.21.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@ import {
24
24
  CustomSelectorInput,
25
25
  Dialog,
26
26
  ErrorWarnIcon,
27
+ FilledWindowMaximizeIcon,
27
28
  MenuContent,
28
29
  MenuContentItem,
29
30
  Modal,
@@ -40,6 +41,7 @@ import {
40
41
  PanelLoadingIndicator,
41
42
  PlayIcon,
42
43
  PlusIcon,
44
+ RefreshIcon,
43
45
  ResizablePanel,
44
46
  ResizablePanelGroup,
45
47
  ResizablePanelSplitter,
@@ -47,9 +49,19 @@ import {
47
49
  RunAllIcon,
48
50
  TimesIcon,
49
51
  } from '@finos/legend-art';
50
- import type { DataProductTestSuite } from '@finos/legend-graph';
52
+ import {
53
+ FunctionAccessPoint,
54
+ LakehouseAccessPoint,
55
+ PrimitiveInstanceValue,
56
+ PrimitiveType,
57
+ type RawLambda,
58
+ type DataProductTestSuite,
59
+ type ValueSpecification,
60
+ } from '@finos/legend-graph';
51
61
  import type { DataProductEditorState } from '../../../../../stores/editor/editor-state/element-editor-state/dataProduct/DataProductEditorState.js';
52
62
  import {
63
+ DataProductValueSpecificationTestParameterState,
64
+ type DataProductTestParameterState,
53
65
  type DataProductTestableState,
54
66
  type DataProductTestSuiteState,
55
67
  type DataProductTestState,
@@ -63,14 +75,24 @@ import {
63
75
  getTestableResultFromTestResult,
64
76
  } from '../../../../../stores/editor/sidebar-state/testable/GlobalTestRunnerState.js';
65
77
  import { RelationElementsDataEditor } from '../../data-editor/RelationElementsDataEditor.js';
66
- import { validateTestableId } from '../../../../../stores/editor/utils/TestableUtils.js';
78
+ import {
79
+ getContentTypeWithParamFromQuery,
80
+ type TestParamContentType,
81
+ validateTestableId,
82
+ } from '../../../../../stores/editor/utils/TestableUtils.js';
67
83
  import { useEditorStore } from '../../../EditorStoreProvider.js';
68
84
  import { guaranteeNonNullable } from '@finos/legend-shared';
69
85
  import {
86
+ ExternalFormatParameterEditorModal,
70
87
  RenameModal,
71
88
  TestAssertionEditor,
72
89
  } from '../../testable/TestableSharedComponents.js';
73
90
  import { testSuite_setId } from '../../../../../stores/graph-modifier/Testable_GraphModifierHelper.js';
91
+ import { TESTABLE_TEST_TAB } from '../../../../../stores/editor/editor-state/element-editor-state/testable/TestableEditorState.js';
92
+ import {
93
+ BasicValueSpecificationEditor,
94
+ instanceValue_setValue,
95
+ } from '@finos/legend-query-builder';
74
96
 
75
97
  // ──────────────────────────────────────────────────────────────────────────────
76
98
  // Create Suite Modal — test name + access point (datasets are auto-inferred)
@@ -489,11 +511,11 @@ const DataProductTestDataEditor = observer(
489
511
 
490
512
  return (
491
513
  <div
492
- className={clsx('service-test-data-editor panel', {
493
- 'service-test-data-editor--no-data': !hasTestData,
514
+ className={clsx('data-product-test-data-editor panel', {
515
+ 'data-product-test-data-editor--no-data': !hasTestData,
494
516
  })}
495
517
  >
496
- <div className="service-test-data-editor__data">
518
+ <div className="data-product-test-data-editor__data">
497
519
  <ResizablePanelGroup orientation="vertical">
498
520
  {/* Left: elements list — always visible */}
499
521
  <ResizablePanel minSize={100} size={180}>
@@ -515,7 +537,7 @@ const DataProductTestDataEditor = observer(
515
537
  )}
516
538
  </div>
517
539
  {!hasTestData ? (
518
- <div className="service-test-data-editor__warning">
540
+ <div className="data-product-test-data-editor__warning">
519
541
  <ErrorWarnIcon />
520
542
  <span>Add an element to configure test data</span>
521
543
  </div>
@@ -638,26 +660,352 @@ const TestItem = observer(
638
660
  // Test Editor — shows SETUP (input data) and ASSERTION (expected + result) tabs
639
661
  // ──────────────────────────────────────────────────────────────────────────────
640
662
 
663
+ const DataProductTestParameterEditor = observer(
664
+ (props: {
665
+ isReadOnly: boolean;
666
+ paramState: DataProductTestParameterState;
667
+ testState: DataProductTestState;
668
+ contentTypeParamPair: TestParamContentType | undefined;
669
+ }) => {
670
+ const { testState, paramState, isReadOnly, contentTypeParamPair } = props;
671
+ const [showPopUp, setShowPopUp] = useState(false);
672
+ const valueSpecificationParamState =
673
+ paramState instanceof DataProductValueSpecificationTestParameterState
674
+ ? paramState
675
+ : undefined;
676
+ const paramExpression = valueSpecificationParamState?.varExpression;
677
+ const paramIsRequired = (paramExpression?.multiplicity.lowerBound ?? 0) > 0;
678
+ const type = contentTypeParamPair
679
+ ? contentTypeParamPair.contentType
680
+ : (paramExpression?.genericType?.value.rawType.name ?? 'unknown');
681
+ const primitiveValueSpecification =
682
+ valueSpecificationParamState?.valueSpec instanceof PrimitiveInstanceValue
683
+ ? valueSpecificationParamState.valueSpec
684
+ : undefined;
685
+ const primitiveValue = primitiveValueSpecification?.values[0] as
686
+ | string
687
+ | undefined;
688
+ const paramValue =
689
+ paramExpression?.genericType?.value.rawType === PrimitiveType.BYTE &&
690
+ primitiveValue !== undefined
691
+ ? atob(primitiveValue)
692
+ : (primitiveValue ?? '');
693
+
694
+ const openInPopUp = (): void => setShowPopUp(!showPopUp);
695
+ const closePopUp = (): void => setShowPopUp(false);
696
+ const updateParamValue = (val: string): void => {
697
+ if (
698
+ primitiveValueSpecification &&
699
+ valueSpecificationParamState &&
700
+ paramExpression
701
+ ) {
702
+ instanceValue_setValue(
703
+ primitiveValueSpecification,
704
+ paramExpression.genericType?.value.rawType === PrimitiveType.BYTE
705
+ ? btoa(val)
706
+ : val,
707
+ 0,
708
+ testState.editorStore.changeDetectionState.observerContext,
709
+ );
710
+ valueSpecificationParamState.updateValueSpecification(
711
+ primitiveValueSpecification,
712
+ );
713
+ }
714
+ };
715
+
716
+ return (
717
+ <div
718
+ key={paramState.parameterValue.name}
719
+ className="panel__content__form__section"
720
+ >
721
+ <div className="panel__content__form__section__header__label">
722
+ {paramState.parameterValue.name}
723
+ <button
724
+ className={clsx('type-tree__node__type__label', {})}
725
+ tabIndex={-1}
726
+ title={type}
727
+ >
728
+ {type}
729
+ </button>
730
+ </div>
731
+ {!valueSpecificationParamState || !paramExpression ? (
732
+ <BlankPanelPlaceholder
733
+ text="Unsupported parameter value"
734
+ tooltipText="This parameter was preserved but cannot currently be edited in form mode"
735
+ />
736
+ ) : contentTypeParamPair && primitiveValueSpecification ? (
737
+ <div className="data-product-test-editor__parameter__code-editor">
738
+ <textarea
739
+ className="panel__content__form__section__textarea value-spec-editor__input"
740
+ spellCheck={false}
741
+ value={paramValue}
742
+ placeholder={primitiveValue === '' ? '(empty)' : undefined}
743
+ onChange={(event) => {
744
+ updateParamValue(event.target.value);
745
+ }}
746
+ />
747
+ {showPopUp && (
748
+ <ExternalFormatParameterEditorModal
749
+ valueSpec={valueSpecificationParamState.valueSpec}
750
+ varExpression={paramExpression}
751
+ isReadOnly={isReadOnly}
752
+ onClose={closePopUp}
753
+ updateParamValue={updateParamValue}
754
+ contentTypeParamPair={contentTypeParamPair}
755
+ />
756
+ )}
757
+ <div className="data-product-test-editor__parameter__value__actions">
758
+ <button
759
+ className="data-product-test-editor__parameter__code-editor__expand-btn"
760
+ onClick={openInPopUp}
761
+ tabIndex={-1}
762
+ title="Open in a popup..."
763
+ >
764
+ <FilledWindowMaximizeIcon />
765
+ </button>
766
+ <button
767
+ className="btn--icon btn--dark btn--sm data-product-test-editor__parameter__code-editor__expand-btn"
768
+ disabled={isReadOnly || paramIsRequired}
769
+ onClick={(): void =>
770
+ testState.removeParamValueState(paramState)
771
+ }
772
+ tabIndex={-1}
773
+ title={
774
+ paramIsRequired ? 'Parameter Required' : 'Remove Parameter'
775
+ }
776
+ >
777
+ <TimesIcon />
778
+ </button>
779
+ </div>
780
+ </div>
781
+ ) : (
782
+ <div className="data-product-test-editor__parameter__value">
783
+ <BasicValueSpecificationEditor
784
+ valueSpecification={valueSpecificationParamState.valueSpec}
785
+ setValueSpecification={(val: ValueSpecification): void => {
786
+ valueSpecificationParamState.updateValueSpecification(val);
787
+ }}
788
+ graph={testState.editorStore.graphManagerState.graph}
789
+ observerContext={
790
+ testState.editorStore.changeDetectionState.observerContext
791
+ }
792
+ typeCheckOption={{
793
+ expectedType:
794
+ paramExpression.genericType?.value.rawType ??
795
+ PrimitiveType.STRING,
796
+ }}
797
+ className="query-builder__parameters__value__editor"
798
+ resetValue={(): void => {
799
+ valueSpecificationParamState.resetValueSpec();
800
+ }}
801
+ />
802
+ <div className="data-product-test-editor__parameter__value__actions">
803
+ <button
804
+ className="btn--icon btn--dark btn--sm"
805
+ disabled={isReadOnly || paramIsRequired}
806
+ onClick={(): void =>
807
+ testState.removeParamValueState(paramState)
808
+ }
809
+ tabIndex={-1}
810
+ title={
811
+ paramIsRequired ? 'Parameter Required' : 'Remove Parameter'
812
+ }
813
+ >
814
+ <TimesIcon />
815
+ </button>
816
+ </div>
817
+ </div>
818
+ )}
819
+ </div>
820
+ );
821
+ },
822
+ );
823
+
824
+ const NewDataProductParameterModal = observer(
825
+ (props: { testState: DataProductTestState; isReadOnly: boolean }) => {
826
+ const { testState, isReadOnly } = props;
827
+ const applicationStore = testState.editorStore.applicationStore;
828
+ const currentOption = {
829
+ value: testState.newParameterValueName,
830
+ label: testState.newParameterValueName,
831
+ };
832
+ const options = testState.newParamOptions;
833
+ const closeModal = (): void => testState.setShowNewParameterModal(false);
834
+ const onChange = (val: { label: string; value: string } | null): void => {
835
+ if (val === null) {
836
+ testState.setNewParameterValueName('');
837
+ } else if (val.value !== testState.newParameterValueName) {
838
+ testState.setNewParameterValueName(val.value);
839
+ }
840
+ };
841
+ return (
842
+ <Dialog
843
+ open={testState.showNewParameterModal}
844
+ onClose={closeModal}
845
+ classes={{ container: 'search-modal__container' }}
846
+ slotProps={{
847
+ paper: {
848
+ classes: { root: 'search-modal__inner-container' },
849
+ },
850
+ }}
851
+ >
852
+ <form
853
+ onSubmit={(event) => {
854
+ event.preventDefault();
855
+ testState.addParameterValue();
856
+ }}
857
+ className="modal modal--dark search-modal"
858
+ >
859
+ <div className="modal__title">New Test Parameter Value</div>
860
+ <CustomSelectorInput
861
+ className="panel__content__form__section__dropdown"
862
+ options={options}
863
+ onChange={onChange}
864
+ value={currentOption}
865
+ escapeClearsValue={true}
866
+ darkMode={
867
+ !applicationStore.layoutService
868
+ .TEMPORARY__isLightColorThemeEnabled
869
+ }
870
+ disabled={isReadOnly}
871
+ />
872
+ <div className="search-modal__actions">
873
+ <button className="btn btn--dark" disabled={isReadOnly}>
874
+ Add
875
+ </button>
876
+ </div>
877
+ </form>
878
+ </Dialog>
879
+ );
880
+ },
881
+ );
882
+
641
883
  const DataProductTestEditor = observer(
642
884
  (props: { testState: DataProductTestState; isReadOnly: boolean }) => {
643
- const { testState } = props;
885
+ const { testState, isReadOnly } = props;
886
+ const selectedTab = testState.selectedTab;
644
887
  const selectedAssertion = testState.selectedAsertionState;
645
888
 
889
+ useEffect(() => {
890
+ testState.syncWithQuery();
891
+ }, [testState]);
892
+
893
+ const addParameter = (): void => {
894
+ testState.openNewParamModal();
895
+ };
896
+
897
+ const generateParameterValues = (): void => {
898
+ testState.generateTestParameterValues();
899
+ };
900
+
901
+ const accessPoint = testState.suiteState.testableState.ownAccessPoints.find(
902
+ (ap) => ap.id === testState.test.accessPointId,
903
+ );
904
+ let accessPointQuery: RawLambda | undefined;
905
+ if (accessPoint instanceof LakehouseAccessPoint) {
906
+ accessPointQuery = accessPoint.func;
907
+ } else if (accessPoint instanceof FunctionAccessPoint) {
908
+ accessPointQuery = accessPoint.query;
909
+ }
910
+ const hasQueryParameters = Boolean(
911
+ testState.queryVariableExpressions.length,
912
+ );
913
+
646
914
  return (
647
915
  <div className="function-test-editor panel">
648
916
  <div className="panel__header">
649
- <div className="panel__header service-test-editor__header--with-tabs">
917
+ <div className="panel__header data-product-test-editor__header--with-tabs">
650
918
  <div className="panel__header__title__content">Assertion</div>
651
919
  </div>
652
920
  </div>
653
921
  <div className="panel">
654
- {selectedAssertion && (
655
- <TestAssertionEditor testAssertionState={selectedAssertion} />
922
+ {selectedTab === TESTABLE_TEST_TAB.ASSERTION && (
923
+ <>
924
+ {selectedAssertion ? (
925
+ <div className="data-product-test-editor__assertion">
926
+ {hasQueryParameters && (
927
+ <div className="data-product-test-editor__parameters-panel-container">
928
+ <div className="panel data-product-test-editor__parameters-panel">
929
+ <div className="data-product-test-editor__parameters-header">
930
+ <div className="data-product-test-editor__parameters-header__title">
931
+ <div className="data-product-test-editor__parameters-header__title__label">
932
+ Parameters
933
+ </div>
934
+ </div>
935
+ <div className="panel__header__actions data-product-test-editor__parameters-header__actions">
936
+ <button
937
+ className="panel__header__action data-product-test-editor__generate-btn"
938
+ onClick={generateParameterValues}
939
+ disabled={!testState.newParamOptions.length}
940
+ title="Generate test parameter values"
941
+ tabIndex={-1}
942
+ >
943
+ <div className="data-product-test-editor__generate-btn__label">
944
+ <RefreshIcon className="data-product-test-editor__generate-btn__label__icon" />
945
+ <div className="data-product-test-editor__generate-btn__label__title">
946
+ Generate
947
+ </div>
948
+ </div>
949
+ </button>
950
+ <button
951
+ className="panel__header__action data-product-test-editor__parameters-header__action"
952
+ tabIndex={-1}
953
+ disabled={!testState.newParamOptions.length}
954
+ onClick={addParameter}
955
+ title="Add Parameter Value"
956
+ >
957
+ <PlusIcon />
958
+ </button>
959
+ </div>
960
+ </div>
961
+ <div className="data-product-test-editor__parameters">
962
+ {testState.parameterValueStates.map((paramState) => (
963
+ <DataProductTestParameterEditor
964
+ key={paramState.uuid}
965
+ isReadOnly={isReadOnly}
966
+ paramState={paramState}
967
+ testState={testState}
968
+ contentTypeParamPair={getContentTypeWithParamFromQuery(
969
+ accessPointQuery,
970
+ testState.editorStore,
971
+ ).find(
972
+ (pair) =>
973
+ pair.param === paramState.parameterValue.name,
974
+ )}
975
+ />
976
+ ))}
977
+ </div>
978
+ </div>
979
+ </div>
980
+ )}
981
+ <div
982
+ className={clsx(
983
+ 'data-product-test-editor__assertion-result',
984
+ {
985
+ 'data-product-test-editor__assertion-result--full':
986
+ !hasQueryParameters,
987
+ },
988
+ )}
989
+ >
990
+ <TestAssertionEditor
991
+ testAssertionState={selectedAssertion}
992
+ />
993
+ </div>
994
+ </div>
995
+ ) : null}
996
+ </>
656
997
  )}
657
- {!selectedAssertion && (
658
- <BlankPanelPlaceholder
659
- text="No assertion"
660
- tooltipText="No assertion configured for this test"
998
+ {selectedTab === TESTABLE_TEST_TAB.ASSERTION &&
999
+ !selectedAssertion && (
1000
+ <BlankPanelPlaceholder
1001
+ text="No assertion"
1002
+ tooltipText="No assertion configured for this test"
1003
+ />
1004
+ )}
1005
+ {testState.showNewParameterModal && (
1006
+ <NewDataProductParameterModal
1007
+ testState={testState}
1008
+ isReadOnly={isReadOnly}
661
1009
  />
662
1010
  )}
663
1011
  </div>
@@ -680,8 +1028,8 @@ const DataProductTestsEditor = observer(
680
1028
  const selectedTest = suiteState.selectTestState;
681
1029
 
682
1030
  return (
683
- <div className="panel service-test-editor">
684
- <div className="service-test-editor__content">
1031
+ <div className="panel data-product-test-editor__tests">
1032
+ <div className="data-product-test-editor__tests__content">
685
1033
  <ResizablePanelGroup orientation="vertical">
686
1034
  <ResizablePanel minSize={100} size={200}>
687
1035
  <div className="binding-editor__header">
@@ -765,7 +1113,7 @@ const DataProductTestSuiteEditor = observer(
765
1113
  const isReadOnly = testableState.dataProductEditorState.isReadOnly;
766
1114
 
767
1115
  return (
768
- <div className="service-test-suite-editor">
1116
+ <div className="data-product-test-suite-editor">
769
1117
  <ResizablePanelGroup orientation="horizontal">
770
1118
  <ResizablePanel size={580} minSize={28}>
771
1119
  <DataProductTestDataEditor
@@ -851,7 +1199,7 @@ export const DataProductTestableEditor = observer(
851
1199
  testSuite_setId(guaranteeNonNullable(testableState.suiteToRename), val);
852
1200
 
853
1201
  return (
854
- <Panel className="service-test-suite-editor">
1202
+ <Panel className="data-product-test-suite-editor">
855
1203
  <PanelLoadingIndicator
856
1204
  isLoading={testableState.runningAllTestsState.isInProgress}
857
1205
  />
@@ -872,14 +1220,14 @@ export const DataProductTestableEditor = observer(
872
1220
 
873
1221
  <PanelHeader>
874
1222
  {dp.tests.length ? (
875
- <PanelHeader className="service-test-suite-editor__header service-test-suite-editor__header--with-tabs">
1223
+ <PanelHeader className="data-product-test-suite-editor__header data-product-test-suite-editor__header--with-tabs">
876
1224
  <div className="uml-element-editor__tabs">
877
1225
  {dp.tests.map((suite) => (
878
1226
  <div
879
1227
  key={suite.id}
880
1228
  onClick={(): void => changeSuite(suite)}
881
- className={clsx('service-test-suite-editor__tab', {
882
- 'service-test-suite-editor__tab--active':
1229
+ className={clsx('data-product-test-suite-editor__tab', {
1230
+ 'data-product-test-suite-editor__tab--active':
883
1231
  selectedSuiteState?.suite === suite,
884
1232
  })}
885
1233
  >
@@ -907,7 +1255,7 @@ export const DataProductTestableEditor = observer(
907
1255
  </PanelHeaderActionItem>
908
1256
  </PanelHeaderActions>
909
1257
  </PanelHeader>
910
- <Panel className="service-test-suite-editor">
1258
+ <Panel className="data-product-test-suite-editor">
911
1259
  {selectedSuiteState && (
912
1260
  <DataProductTestSuiteEditor suiteState={selectedSuiteState} />
913
1261
  )}