datajunction-ui 0.0.44 → 0.0.46
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.
- package/package.json +1 -1
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +349 -1
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +46 -1
- package/src/app/pages/QueryPlannerPage/ResultsView.jsx +281 -0
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +225 -100
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +193 -0
- package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +388 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +31 -51
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +720 -34
- package/src/app/pages/QueryPlannerPage/index.jsx +237 -117
- package/src/app/pages/QueryPlannerPage/styles.css +765 -15
- package/src/app/services/DJService.js +29 -6
- package/src/app/services/__tests__/DJService.test.jsx +163 -0
|
@@ -324,37 +324,6 @@ describe('QueryPlannerPage', () => {
|
|
|
324
324
|
expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
|
|
325
325
|
});
|
|
326
326
|
});
|
|
327
|
-
|
|
328
|
-
it('shows clear button when search has value', async () => {
|
|
329
|
-
renderPage();
|
|
330
|
-
|
|
331
|
-
await waitFor(() => {
|
|
332
|
-
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
const searchInput = screen.getByPlaceholderText('Search metrics...');
|
|
336
|
-
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
337
|
-
|
|
338
|
-
// Clear button should appear
|
|
339
|
-
const clearButton = screen.getAllByText('×')[0];
|
|
340
|
-
expect(clearButton).toBeInTheDocument();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it('clears search when clear button is clicked', async () => {
|
|
344
|
-
renderPage();
|
|
345
|
-
|
|
346
|
-
await waitFor(() => {
|
|
347
|
-
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
const searchInput = screen.getByPlaceholderText('Search metrics...');
|
|
351
|
-
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
352
|
-
|
|
353
|
-
const clearButton = screen.getAllByText('×')[0];
|
|
354
|
-
fireEvent.click(clearButton);
|
|
355
|
-
|
|
356
|
-
expect(searchInput.value).toBe('');
|
|
357
|
-
});
|
|
358
327
|
});
|
|
359
328
|
|
|
360
329
|
describe('Cube Preset Loading', () => {
|
|
@@ -678,7 +647,7 @@ describe('QueryPlannerPage', () => {
|
|
|
678
647
|
});
|
|
679
648
|
|
|
680
649
|
describe('Clear Selection', () => {
|
|
681
|
-
it('clears all selections when Clear
|
|
650
|
+
it('clears all selections when Clear is clicked', async () => {
|
|
682
651
|
mockDjClient.cubeForPlanner.mockResolvedValue(mockCubeData);
|
|
683
652
|
|
|
684
653
|
renderPage();
|
|
@@ -700,8 +669,8 @@ describe('QueryPlannerPage', () => {
|
|
|
700
669
|
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
|
|
701
670
|
});
|
|
702
671
|
|
|
703
|
-
// Click Clear all
|
|
704
|
-
const clearButton =
|
|
672
|
+
// Click the global Clear button (clear-all-btn class)
|
|
673
|
+
const clearButton = document.querySelector('.clear-all-btn');
|
|
705
674
|
fireEvent.click(clearButton);
|
|
706
675
|
|
|
707
676
|
// Should show "Load from Cube" again (cube unloaded)
|
|
@@ -739,4 +708,721 @@ describe('QueryPlannerPage', () => {
|
|
|
739
708
|
});
|
|
740
709
|
});
|
|
741
710
|
});
|
|
711
|
+
|
|
712
|
+
describe('Filter Handling', () => {
|
|
713
|
+
it('adds a filter when add filter button is clicked', async () => {
|
|
714
|
+
renderPage();
|
|
715
|
+
|
|
716
|
+
await waitFor(() => {
|
|
717
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Find filter input and add a filter
|
|
721
|
+
const filterInput = screen.getByPlaceholderText(
|
|
722
|
+
/e\.g\. v3\.date\.date_id/i,
|
|
723
|
+
);
|
|
724
|
+
fireEvent.change(filterInput, {
|
|
725
|
+
target: { value: "date_id >= '2024-01-01'" },
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
const addButton = screen.getByText('Add');
|
|
729
|
+
fireEvent.click(addButton);
|
|
730
|
+
|
|
731
|
+
// Filter should be added (check for chip)
|
|
732
|
+
await waitFor(() => {
|
|
733
|
+
expect(screen.getByText("date_id >= '2024-01-01'")).toBeInTheDocument();
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('removes a filter when remove button is clicked', async () => {
|
|
738
|
+
renderPage();
|
|
739
|
+
|
|
740
|
+
await waitFor(() => {
|
|
741
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Add a filter first
|
|
745
|
+
const filterInput = screen.getByPlaceholderText(
|
|
746
|
+
/e\.g\. v3\.date\.date_id/i,
|
|
747
|
+
);
|
|
748
|
+
fireEvent.change(filterInput, {
|
|
749
|
+
target: { value: "status = 'active'" },
|
|
750
|
+
});
|
|
751
|
+
fireEvent.click(screen.getByText('Add'));
|
|
752
|
+
|
|
753
|
+
await waitFor(() => {
|
|
754
|
+
expect(screen.getByText("status = 'active'")).toBeInTheDocument();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// Remove the filter (button has generic title "Remove filter")
|
|
758
|
+
const removeButton = screen.getByTitle('Remove filter');
|
|
759
|
+
fireEvent.click(removeButton);
|
|
760
|
+
|
|
761
|
+
await waitFor(() => {
|
|
762
|
+
expect(screen.queryByText("status = 'active'")).not.toBeInTheDocument();
|
|
763
|
+
});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('disables add button when filter input is empty', async () => {
|
|
767
|
+
renderPage();
|
|
768
|
+
|
|
769
|
+
await waitFor(() => {
|
|
770
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const addButton = screen.getByText('Add');
|
|
774
|
+
expect(addButton).toBeDisabled();
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe('Run Query', () => {
|
|
779
|
+
beforeEach(() => {
|
|
780
|
+
mockDjClient.metricsV3.mockResolvedValue({
|
|
781
|
+
sql: 'SELECT * FROM metrics',
|
|
782
|
+
dialect: 'SPARK',
|
|
783
|
+
cube_name: null,
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it('shows Run Query button when metrics and dimensions selected', async () => {
|
|
788
|
+
renderPage();
|
|
789
|
+
|
|
790
|
+
await waitFor(() => {
|
|
791
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Select metric
|
|
795
|
+
fireEvent.click(screen.getByText('default'));
|
|
796
|
+
await waitFor(() => {
|
|
797
|
+
fireEvent.click(
|
|
798
|
+
screen.getByRole('checkbox', { name: /num_repair_orders/i }),
|
|
799
|
+
);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
await waitFor(() => {
|
|
803
|
+
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Select dimension
|
|
807
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /dateint/i }));
|
|
808
|
+
|
|
809
|
+
// Run Query button should exist
|
|
810
|
+
await waitFor(() => {
|
|
811
|
+
expect(screen.getByText('Run Query')).toBeInTheDocument();
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('disables Run Query button when no metrics selected', async () => {
|
|
816
|
+
renderPage();
|
|
817
|
+
|
|
818
|
+
await waitFor(() => {
|
|
819
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
const runButton = screen.getByText('Run Query');
|
|
823
|
+
expect(runButton).toBeDisabled();
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe('Materialization Handlers', () => {
|
|
828
|
+
const mockMeasuresResult = {
|
|
829
|
+
grainGroups: [
|
|
830
|
+
{
|
|
831
|
+
node: 'default.repair_orders',
|
|
832
|
+
grain_columns: ['default.date_dim.dateint'],
|
|
833
|
+
measures: [
|
|
834
|
+
{ name: 'sum_revenue', expression: 'SUM(revenue)' },
|
|
835
|
+
{ name: 'count_orders', expression: 'COUNT(*)' },
|
|
836
|
+
],
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
metricFormulas: [
|
|
840
|
+
{
|
|
841
|
+
metric: 'default.num_repair_orders',
|
|
842
|
+
formula: 'default.repair_orders.count_orders',
|
|
843
|
+
},
|
|
844
|
+
],
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
beforeEach(() => {
|
|
848
|
+
mockDjClient.measuresV3.mockResolvedValue(mockMeasuresResult);
|
|
849
|
+
mockDjClient.metricsV3.mockResolvedValue({
|
|
850
|
+
sql: 'SELECT * FROM metrics',
|
|
851
|
+
dialect: 'SPARK',
|
|
852
|
+
cube_name: null,
|
|
853
|
+
});
|
|
854
|
+
mockDjClient.listPreaggs.mockResolvedValue({ items: [] });
|
|
855
|
+
mockDjClient.getNodeColumnsWithPartitions.mockResolvedValue({
|
|
856
|
+
columns: [],
|
|
857
|
+
temporalPartitions: [],
|
|
858
|
+
});
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('calls planPreaggs when materialization is planned', async () => {
|
|
862
|
+
mockDjClient.planPreaggs.mockResolvedValue({
|
|
863
|
+
preaggs: [
|
|
864
|
+
{
|
|
865
|
+
id: 1,
|
|
866
|
+
node_name: 'default.repair_orders',
|
|
867
|
+
grain_columns: ['default.date_dim.dateint'],
|
|
868
|
+
},
|
|
869
|
+
],
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
renderPage();
|
|
873
|
+
|
|
874
|
+
await waitFor(() => {
|
|
875
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
// Select metric and dimension
|
|
879
|
+
fireEvent.click(screen.getByText('default'));
|
|
880
|
+
await waitFor(() => {
|
|
881
|
+
fireEvent.click(
|
|
882
|
+
screen.getByRole('checkbox', { name: /num_repair_orders/i }),
|
|
883
|
+
);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
await waitFor(() => {
|
|
887
|
+
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /dateint/i }));
|
|
891
|
+
|
|
892
|
+
await waitFor(() => {
|
|
893
|
+
expect(mockDjClient.measuresV3).toHaveBeenCalled();
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it('handles planPreaggs error gracefully', async () => {
|
|
898
|
+
mockDjClient.planPreaggs.mockResolvedValue({
|
|
899
|
+
_error: true,
|
|
900
|
+
message: 'Failed to plan',
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
renderPage();
|
|
904
|
+
|
|
905
|
+
await waitFor(() => {
|
|
906
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
907
|
+
});
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('calls materializePreagg when workflow is created', async () => {
|
|
911
|
+
mockDjClient.planPreaggs.mockResolvedValue({
|
|
912
|
+
preaggs: [
|
|
913
|
+
{
|
|
914
|
+
id: 123,
|
|
915
|
+
node_name: 'default.repair_orders',
|
|
916
|
+
grain_columns: ['default.date_dim.dateint'],
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
});
|
|
920
|
+
mockDjClient.materializePreagg.mockResolvedValue({
|
|
921
|
+
workflow_urls: ['http://workflow.example.com'],
|
|
922
|
+
workflow_status: 'active',
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
renderPage();
|
|
926
|
+
|
|
927
|
+
await waitFor(() => {
|
|
928
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it('handles materializePreagg error', async () => {
|
|
933
|
+
mockDjClient.materializePreagg.mockResolvedValue({
|
|
934
|
+
_error: true,
|
|
935
|
+
message: 'Failed to create workflow',
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
renderPage();
|
|
939
|
+
|
|
940
|
+
await waitFor(() => {
|
|
941
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('calls runPreaggBackfill when backfill is triggered', async () => {
|
|
946
|
+
mockDjClient.runPreaggBackfill.mockResolvedValue({
|
|
947
|
+
job_url: 'http://job.example.com',
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
renderPage();
|
|
951
|
+
|
|
952
|
+
await waitFor(() => {
|
|
953
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it('handles runPreaggBackfill error', async () => {
|
|
958
|
+
mockDjClient.runPreaggBackfill.mockResolvedValue({
|
|
959
|
+
_error: true,
|
|
960
|
+
message: 'Backfill failed',
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
renderPage();
|
|
964
|
+
|
|
965
|
+
await waitFor(() => {
|
|
966
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
it('calls updatePreaggConfig for config updates', async () => {
|
|
971
|
+
mockDjClient.updatePreaggConfig.mockResolvedValue({
|
|
972
|
+
id: 123,
|
|
973
|
+
strategy: 'incremental_time',
|
|
974
|
+
schedule: '0 6 * * *',
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
renderPage();
|
|
978
|
+
|
|
979
|
+
await waitFor(() => {
|
|
980
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('handles updatePreaggConfig error', async () => {
|
|
985
|
+
mockDjClient.updatePreaggConfig.mockResolvedValue({
|
|
986
|
+
_error: true,
|
|
987
|
+
message: 'Config update failed',
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
renderPage();
|
|
991
|
+
|
|
992
|
+
await waitFor(() => {
|
|
993
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
it('calls deactivatePreaggWorkflow when workflow is deactivated', async () => {
|
|
998
|
+
mockDjClient.deactivatePreaggWorkflow.mockResolvedValue({
|
|
999
|
+
success: true,
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
renderPage();
|
|
1003
|
+
|
|
1004
|
+
await waitFor(() => {
|
|
1005
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
it('handles deactivatePreaggWorkflow error', async () => {
|
|
1010
|
+
mockDjClient.deactivatePreaggWorkflow.mockResolvedValue({
|
|
1011
|
+
_error: true,
|
|
1012
|
+
message: 'Failed to deactivate',
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
renderPage();
|
|
1016
|
+
|
|
1017
|
+
await waitFor(() => {
|
|
1018
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
describe('Cube Materialization Flow', () => {
|
|
1024
|
+
beforeEach(() => {
|
|
1025
|
+
mockDjClient.listCubesForPreset.mockResolvedValue([
|
|
1026
|
+
{ name: 'default.test_cube', display_name: 'Test Cube' },
|
|
1027
|
+
]);
|
|
1028
|
+
mockDjClient.cubeForPlanner.mockResolvedValue({
|
|
1029
|
+
name: 'default.test_cube',
|
|
1030
|
+
display_name: 'Test Cube',
|
|
1031
|
+
cube_node_metrics: ['default.num_repair_orders'],
|
|
1032
|
+
cube_node_dimensions: ['default.date_dim.dateint'],
|
|
1033
|
+
cubeMaterialization: {
|
|
1034
|
+
strategy: 'full',
|
|
1035
|
+
schedule: '0 6 * * *',
|
|
1036
|
+
lookbackWindow: null,
|
|
1037
|
+
druidDatasource: 'test_ds',
|
|
1038
|
+
preaggTables: [],
|
|
1039
|
+
workflowUrls: ['http://workflow.url'],
|
|
1040
|
+
},
|
|
1041
|
+
availability: null,
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('calls createCube when new cube is created', async () => {
|
|
1046
|
+
mockDjClient.createCube.mockResolvedValue({
|
|
1047
|
+
status: 200,
|
|
1048
|
+
json: { name: 'new.cube' },
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
renderPage();
|
|
1052
|
+
|
|
1053
|
+
await waitFor(() => {
|
|
1054
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it('handles createCube error - cube already exists', async () => {
|
|
1059
|
+
mockDjClient.createCube.mockResolvedValue({
|
|
1060
|
+
status: 400,
|
|
1061
|
+
json: { message: 'Cube already exists' },
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
renderPage();
|
|
1065
|
+
|
|
1066
|
+
await waitFor(() => {
|
|
1067
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
it('handles createCube error - other error', async () => {
|
|
1072
|
+
mockDjClient.createCube.mockResolvedValue({
|
|
1073
|
+
status: 500,
|
|
1074
|
+
json: { message: 'Server error' },
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
renderPage();
|
|
1078
|
+
|
|
1079
|
+
await waitFor(() => {
|
|
1080
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
it('calls materializeCubeV2 for cube materialization', async () => {
|
|
1085
|
+
mockDjClient.materializeCubeV2.mockResolvedValue({
|
|
1086
|
+
status: 200,
|
|
1087
|
+
json: {
|
|
1088
|
+
workflow_urls: ['http://cube-workflow.example.com'],
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
renderPage();
|
|
1093
|
+
|
|
1094
|
+
await waitFor(() => {
|
|
1095
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('handles materializeCubeV2 error', async () => {
|
|
1100
|
+
mockDjClient.materializeCubeV2.mockResolvedValue({
|
|
1101
|
+
status: 500,
|
|
1102
|
+
json: { message: 'Cube materialization failed' },
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
renderPage();
|
|
1106
|
+
|
|
1107
|
+
await waitFor(() => {
|
|
1108
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
it('calls refreshCubeWorkflow for config updates', async () => {
|
|
1113
|
+
mockDjClient.refreshCubeWorkflow.mockResolvedValue({
|
|
1114
|
+
status: 200,
|
|
1115
|
+
json: { workflow_urls: ['http://updated-workflow.url'] },
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
renderPage();
|
|
1119
|
+
|
|
1120
|
+
await waitFor(() => {
|
|
1121
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it('handles refreshCubeWorkflow error', async () => {
|
|
1126
|
+
mockDjClient.refreshCubeWorkflow.mockResolvedValue({
|
|
1127
|
+
status: 500,
|
|
1128
|
+
json: { message: 'Refresh failed' },
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
renderPage();
|
|
1132
|
+
|
|
1133
|
+
await waitFor(() => {
|
|
1134
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it('calls deactivateCubeWorkflow when deactivating', async () => {
|
|
1139
|
+
mockDjClient.deactivateCubeWorkflow.mockResolvedValue({
|
|
1140
|
+
status: 200,
|
|
1141
|
+
json: { success: true },
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
renderPage();
|
|
1145
|
+
|
|
1146
|
+
await waitFor(() => {
|
|
1147
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
// Load cube first
|
|
1151
|
+
fireEvent.click(screen.getByText('Load from Cube'));
|
|
1152
|
+
fireEvent.click(screen.getByText('Test Cube'));
|
|
1153
|
+
|
|
1154
|
+
await waitFor(() => {
|
|
1155
|
+
expect(mockDjClient.cubeForPlanner).toHaveBeenCalled();
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it('handles deactivateCubeWorkflow error', async () => {
|
|
1160
|
+
mockDjClient.deactivateCubeWorkflow.mockResolvedValue({
|
|
1161
|
+
status: 500,
|
|
1162
|
+
json: { message: 'Deactivation failed' },
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
renderPage();
|
|
1166
|
+
|
|
1167
|
+
await waitFor(() => {
|
|
1168
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('calls runCubeBackfill for backfill', async () => {
|
|
1173
|
+
mockDjClient.runCubeBackfill.mockResolvedValue({
|
|
1174
|
+
job_url: 'http://backfill-job.example.com',
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
renderPage();
|
|
1178
|
+
|
|
1179
|
+
await waitFor(() => {
|
|
1180
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// Load cube
|
|
1184
|
+
fireEvent.click(screen.getByText('Load from Cube'));
|
|
1185
|
+
fireEvent.click(screen.getByText('Test Cube'));
|
|
1186
|
+
|
|
1187
|
+
await waitFor(() => {
|
|
1188
|
+
expect(mockDjClient.cubeForPlanner).toHaveBeenCalled();
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it('handles runCubeBackfill error', async () => {
|
|
1193
|
+
mockDjClient.runCubeBackfill.mockResolvedValue({
|
|
1194
|
+
_error: true,
|
|
1195
|
+
message: 'Backfill failed',
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
renderPage();
|
|
1199
|
+
|
|
1200
|
+
await waitFor(() => {
|
|
1201
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
it('handles runCubeBackfill with missing cube name', async () => {
|
|
1206
|
+
// Don't load any cube - loadedCubeName will be null
|
|
1207
|
+
renderPage();
|
|
1208
|
+
|
|
1209
|
+
await waitFor(() => {
|
|
1210
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
// No cube loaded, so runCubeBackfill shouldn't be callable
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
describe('Workflow Status Updates', () => {
|
|
1218
|
+
const mockMeasuresWithPreagg = {
|
|
1219
|
+
grainGroups: [
|
|
1220
|
+
{
|
|
1221
|
+
node: 'default.repair_orders',
|
|
1222
|
+
grain_columns: ['default.date_dim.dateint'],
|
|
1223
|
+
measures: [{ name: 'count_orders', expression: 'COUNT(*)' }],
|
|
1224
|
+
},
|
|
1225
|
+
],
|
|
1226
|
+
metricFormulas: [
|
|
1227
|
+
{
|
|
1228
|
+
metric: 'default.num_repair_orders',
|
|
1229
|
+
formula: 'count_orders',
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
beforeEach(() => {
|
|
1235
|
+
mockDjClient.measuresV3.mockResolvedValue(mockMeasuresWithPreagg);
|
|
1236
|
+
mockDjClient.metricsV3.mockResolvedValue({
|
|
1237
|
+
sql: 'SELECT * FROM metrics',
|
|
1238
|
+
dialect: 'SPARK',
|
|
1239
|
+
cube_name: null,
|
|
1240
|
+
});
|
|
1241
|
+
mockDjClient.listPreaggs.mockResolvedValue({ items: [] });
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('updates state when workflow is created successfully', async () => {
|
|
1245
|
+
mockDjClient.materializePreagg.mockResolvedValue({
|
|
1246
|
+
workflow_urls: ['http://new-workflow.example.com'],
|
|
1247
|
+
workflow_status: 'active',
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
renderPage();
|
|
1251
|
+
|
|
1252
|
+
await waitFor(() => {
|
|
1253
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it('updates state when workflow is deactivated', async () => {
|
|
1258
|
+
mockDjClient.deactivatePreaggWorkflow.mockResolvedValue({
|
|
1259
|
+
success: true,
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
renderPage();
|
|
1263
|
+
|
|
1264
|
+
await waitFor(() => {
|
|
1265
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1266
|
+
});
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
it('handles network errors in workflow operations', async () => {
|
|
1270
|
+
mockDjClient.materializePreagg.mockRejectedValue(
|
|
1271
|
+
new Error('Network error'),
|
|
1272
|
+
);
|
|
1273
|
+
|
|
1274
|
+
renderPage();
|
|
1275
|
+
|
|
1276
|
+
await waitFor(() => {
|
|
1277
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
describe('Raw SQL Fetching', () => {
|
|
1283
|
+
it('fetches raw SQL when needed', async () => {
|
|
1284
|
+
mockDjClient.metricsV3.mockResolvedValue({
|
|
1285
|
+
sql: 'SELECT * FROM raw_tables',
|
|
1286
|
+
dialect: 'SPARK',
|
|
1287
|
+
cube_name: null,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
renderPage();
|
|
1291
|
+
|
|
1292
|
+
await waitFor(() => {
|
|
1293
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// Select metric and dimension to trigger SQL fetch
|
|
1297
|
+
fireEvent.click(screen.getByText('default'));
|
|
1298
|
+
await waitFor(() => {
|
|
1299
|
+
fireEvent.click(
|
|
1300
|
+
screen.getByRole('checkbox', { name: /num_repair_orders/i }),
|
|
1301
|
+
);
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
await waitFor(() => {
|
|
1305
|
+
expect(mockDjClient.commonDimensions).toHaveBeenCalled();
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
fireEvent.click(screen.getByRole('checkbox', { name: /dateint/i }));
|
|
1309
|
+
|
|
1310
|
+
await waitFor(() => {
|
|
1311
|
+
expect(mockDjClient.metricsV3).toHaveBeenCalled();
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
describe('Cube Workflow Handlers', () => {
|
|
1317
|
+
beforeEach(() => {
|
|
1318
|
+
mockDjClient.listCubesForPreset.mockResolvedValue([
|
|
1319
|
+
{ name: 'default.test_cube', display_name: 'Test Cube' },
|
|
1320
|
+
]);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('handles cube deactivation', async () => {
|
|
1324
|
+
mockDjClient.cubeForPlanner.mockResolvedValue({
|
|
1325
|
+
name: 'default.test_cube',
|
|
1326
|
+
display_name: 'Test Cube',
|
|
1327
|
+
cube_node_metrics: ['default.num_repair_orders'],
|
|
1328
|
+
cube_node_dimensions: ['default.date_dim.dateint'],
|
|
1329
|
+
cubeMaterialization: {
|
|
1330
|
+
strategy: 'full',
|
|
1331
|
+
schedule: '0 6 * * *',
|
|
1332
|
+
lookbackWindow: null,
|
|
1333
|
+
druidDatasource: 'test_ds',
|
|
1334
|
+
preaggTables: [],
|
|
1335
|
+
workflowUrls: ['http://workflow.url'],
|
|
1336
|
+
},
|
|
1337
|
+
availability: null,
|
|
1338
|
+
});
|
|
1339
|
+
mockDjClient.deactivateCubeWorkflow.mockResolvedValue({ success: true });
|
|
1340
|
+
|
|
1341
|
+
renderPage();
|
|
1342
|
+
|
|
1343
|
+
await waitFor(() => {
|
|
1344
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
// Load cube
|
|
1348
|
+
fireEvent.click(screen.getByText('Load from Cube'));
|
|
1349
|
+
fireEvent.click(screen.getByText('Test Cube'));
|
|
1350
|
+
|
|
1351
|
+
await waitFor(() => {
|
|
1352
|
+
expect(mockDjClient.cubeForPlanner).toHaveBeenCalled();
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it('handles cube backfill', async () => {
|
|
1357
|
+
mockDjClient.cubeForPlanner.mockResolvedValue({
|
|
1358
|
+
name: 'default.test_cube',
|
|
1359
|
+
display_name: 'Test Cube',
|
|
1360
|
+
cube_node_metrics: ['default.num_repair_orders'],
|
|
1361
|
+
cube_node_dimensions: ['default.date_dim.dateint'],
|
|
1362
|
+
cubeMaterialization: {
|
|
1363
|
+
strategy: 'full',
|
|
1364
|
+
schedule: '0 6 * * *',
|
|
1365
|
+
lookbackWindow: null,
|
|
1366
|
+
druidDatasource: 'test_ds',
|
|
1367
|
+
preaggTables: [],
|
|
1368
|
+
workflowUrls: [],
|
|
1369
|
+
},
|
|
1370
|
+
availability: null,
|
|
1371
|
+
});
|
|
1372
|
+
mockDjClient.runCubeBackfill.mockResolvedValue({ job_url: 'http://job' });
|
|
1373
|
+
|
|
1374
|
+
renderPage();
|
|
1375
|
+
|
|
1376
|
+
await waitFor(() => {
|
|
1377
|
+
expect(mockDjClient.listCubesForPreset).toHaveBeenCalled();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
// Load cube
|
|
1381
|
+
fireEvent.click(screen.getByText('Load from Cube'));
|
|
1382
|
+
fireEvent.click(screen.getByText('Test Cube'));
|
|
1383
|
+
|
|
1384
|
+
await waitFor(() => {
|
|
1385
|
+
expect(mockDjClient.cubeForPlanner).toHaveBeenCalled();
|
|
1386
|
+
});
|
|
1387
|
+
});
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
describe('Partition Handling', () => {
|
|
1391
|
+
it('fetches node partitions when needed', async () => {
|
|
1392
|
+
mockDjClient.getNodeColumnsWithPartitions.mockResolvedValue({
|
|
1393
|
+
columns: [{ name: 'date_col', type: 'date' }],
|
|
1394
|
+
temporalPartitions: [{ column: 'date_col' }],
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
renderPage();
|
|
1398
|
+
|
|
1399
|
+
await waitFor(() => {
|
|
1400
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
it('handles setPartition call', async () => {
|
|
1405
|
+
mockDjClient.setPartition.mockResolvedValue({ success: true });
|
|
1406
|
+
|
|
1407
|
+
renderPage();
|
|
1408
|
+
|
|
1409
|
+
await waitFor(() => {
|
|
1410
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1411
|
+
});
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
describe('Results View Navigation', () => {
|
|
1416
|
+
it('shows results view hint when no selection', async () => {
|
|
1417
|
+
renderPage();
|
|
1418
|
+
|
|
1419
|
+
await waitFor(() => {
|
|
1420
|
+
expect(mockDjClient.metrics).toHaveBeenCalled();
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
expect(
|
|
1424
|
+
screen.getByText('Select metrics and dimensions to run a query'),
|
|
1425
|
+
).toBeInTheDocument();
|
|
1426
|
+
});
|
|
1427
|
+
});
|
|
742
1428
|
});
|