datajunction-ui 0.0.27 → 0.0.29
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/NamespaceHeader.jsx +96 -26
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +165 -0
- package/src/app/pages/NamespacePage/Explorer.jsx +68 -10
- package/src/app/pages/NamespacePage/__tests__/index.test.jsx +21 -11
- package/src/app/pages/NamespacePage/index.jsx +316 -47
- package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +28 -0
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +20 -20
- package/src/app/pages/QueryPlannerPage/index.jsx +1 -1
- package/src/app/pages/Root/__tests__/index.test.jsx +99 -4
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +177 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +50 -0
- package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +95 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +315 -28
- package/src/app/services/DJService.js +35 -2
- package/src/app/utils/__tests__/date.test.js +60 -140
- package/src/styles/index.css +51 -10
|
@@ -9,6 +9,7 @@ import AddNodeDropdown from '../../components/AddNodeDropdown';
|
|
|
9
9
|
import NodeListActions from '../../components/NodeListActions';
|
|
10
10
|
import LoadingIcon from '../../icons/LoadingIcon';
|
|
11
11
|
import CompactSelect from './CompactSelect';
|
|
12
|
+
import { getDJUrl } from '../../services/DJService';
|
|
12
13
|
|
|
13
14
|
import 'styles/node-list.css';
|
|
14
15
|
import 'styles/sorted-table.css';
|
|
@@ -166,6 +167,8 @@ export function NamespacePage() {
|
|
|
166
167
|
const [retrieved, setRetrieved] = useState(false);
|
|
167
168
|
|
|
168
169
|
const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
|
|
170
|
+
const [namespaceSources, setNamespaceSources] = useState({});
|
|
171
|
+
const [currentNamespaceSources, setCurrentNamespaceSources] = useState(null);
|
|
169
172
|
|
|
170
173
|
const [sortConfig, setSortConfig] = useState({
|
|
171
174
|
key: 'updatedAt',
|
|
@@ -230,10 +233,32 @@ export function NamespacePage() {
|
|
|
230
233
|
const namespaces = await djClient.namespaces();
|
|
231
234
|
const hierarchy = createNamespaceHierarchy(namespaces);
|
|
232
235
|
setNamespaceHierarchy(hierarchy);
|
|
236
|
+
|
|
237
|
+
// Fetch sources for all namespaces in bulk
|
|
238
|
+
const allNamespaceNames = namespaces.map(ns => ns.namespace);
|
|
239
|
+
if (allNamespaceNames.length > 0) {
|
|
240
|
+
const sourcesResponse = await djClient.namespaceSourcesBulk(
|
|
241
|
+
allNamespaceNames,
|
|
242
|
+
);
|
|
243
|
+
if (sourcesResponse && sourcesResponse.sources) {
|
|
244
|
+
setNamespaceSources(sourcesResponse.sources);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
233
247
|
};
|
|
234
248
|
fetchData().catch(console.error);
|
|
235
249
|
}, [djClient, djClient.namespaces]);
|
|
236
250
|
|
|
251
|
+
// Fetch sources for the current namespace (for the header badge)
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
const fetchCurrentSources = async () => {
|
|
254
|
+
if (namespace) {
|
|
255
|
+
const sources = await djClient.namespaceSources(namespace);
|
|
256
|
+
setCurrentNamespaceSources(sources);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
fetchCurrentSources().catch(console.error);
|
|
260
|
+
}, [djClient, namespace]);
|
|
261
|
+
|
|
237
262
|
useEffect(() => {
|
|
238
263
|
const fetchData = async () => {
|
|
239
264
|
setRetrieved(false);
|
|
@@ -458,7 +483,6 @@ export function NamespacePage() {
|
|
|
458
483
|
}}
|
|
459
484
|
>
|
|
460
485
|
<h2 style={{ margin: 0 }}>Explore</h2>
|
|
461
|
-
<AddNodeDropdown namespace={namespace} />
|
|
462
486
|
</div>
|
|
463
487
|
|
|
464
488
|
{/* Unified Filter Bar */}
|
|
@@ -466,7 +490,7 @@ export function NamespacePage() {
|
|
|
466
490
|
style={{
|
|
467
491
|
marginBottom: '1rem',
|
|
468
492
|
padding: '1rem',
|
|
469
|
-
backgroundColor: '#
|
|
493
|
+
backgroundColor: '#f8fafc',
|
|
470
494
|
borderRadius: '8px',
|
|
471
495
|
}}
|
|
472
496
|
>
|
|
@@ -482,7 +506,17 @@ export function NamespacePage() {
|
|
|
482
506
|
<div
|
|
483
507
|
style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
|
|
484
508
|
>
|
|
485
|
-
<span
|
|
509
|
+
<span
|
|
510
|
+
style={{
|
|
511
|
+
fontSize: '11px',
|
|
512
|
+
fontWeight: '600',
|
|
513
|
+
textTransform: 'uppercase',
|
|
514
|
+
letterSpacing: '0.5px',
|
|
515
|
+
color: '#64748b',
|
|
516
|
+
}}
|
|
517
|
+
>
|
|
518
|
+
Quick
|
|
519
|
+
</span>
|
|
486
520
|
{presets.map(preset => (
|
|
487
521
|
<button
|
|
488
522
|
key={preset.id}
|
|
@@ -745,18 +779,23 @@ export function NamespacePage() {
|
|
|
745
779
|
</div>
|
|
746
780
|
|
|
747
781
|
<div className="table-responsive">
|
|
748
|
-
<div
|
|
782
|
+
<div
|
|
783
|
+
className={`sidebar`}
|
|
784
|
+
style={{ borderRight: '1px solid #e2e8f0', paddingRight: '1rem' }}
|
|
785
|
+
>
|
|
749
786
|
<div
|
|
750
787
|
style={{
|
|
751
|
-
|
|
788
|
+
paddingBottom: '12px',
|
|
789
|
+
marginBottom: '8px',
|
|
752
790
|
}}
|
|
753
791
|
>
|
|
754
792
|
<span
|
|
755
793
|
style={{
|
|
756
|
-
|
|
757
|
-
fontSize: '0.8125rem',
|
|
794
|
+
fontSize: '11px',
|
|
758
795
|
fontWeight: '600',
|
|
759
|
-
|
|
796
|
+
textTransform: 'uppercase',
|
|
797
|
+
letterSpacing: '0.5px',
|
|
798
|
+
color: '#64748b',
|
|
760
799
|
}}
|
|
761
800
|
>
|
|
762
801
|
Namespaces
|
|
@@ -770,54 +809,284 @@ export function NamespacePage() {
|
|
|
770
809
|
defaultExpand={true}
|
|
771
810
|
isTopLevel={true}
|
|
772
811
|
key={child.namespace}
|
|
812
|
+
namespaceSources={namespaceSources}
|
|
773
813
|
/>
|
|
774
814
|
))
|
|
775
815
|
: null}
|
|
776
816
|
</div>
|
|
777
|
-
<
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
817
|
+
<div style={{ flex: 1, minWidth: 0, marginLeft: '1.5rem' }}>
|
|
818
|
+
{/* Namespace Header */}
|
|
819
|
+
<div
|
|
820
|
+
style={{
|
|
821
|
+
display: 'flex',
|
|
822
|
+
justifyContent: 'space-between',
|
|
823
|
+
alignItems: 'center',
|
|
824
|
+
paddingBottom: '12px',
|
|
825
|
+
marginBottom: '16px',
|
|
826
|
+
borderBottom: '1px solid #e2e8f0',
|
|
827
|
+
}}
|
|
828
|
+
>
|
|
829
|
+
<div
|
|
830
|
+
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
831
|
+
>
|
|
832
|
+
<a href="/" style={{ display: 'flex', alignItems: 'center' }}>
|
|
833
|
+
<svg
|
|
834
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
835
|
+
width="16"
|
|
836
|
+
height="16"
|
|
837
|
+
fill="currentColor"
|
|
838
|
+
viewBox="0 0 16 16"
|
|
839
|
+
>
|
|
840
|
+
<path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z" />
|
|
841
|
+
</svg>
|
|
842
|
+
</a>
|
|
843
|
+
<span style={{ color: '#6c757d' }}>/</span>
|
|
844
|
+
{namespace ? (
|
|
845
|
+
namespace.split('.').map((part, index, arr) => (
|
|
846
|
+
<span
|
|
847
|
+
key={index}
|
|
848
|
+
style={{
|
|
849
|
+
display: 'flex',
|
|
850
|
+
alignItems: 'center',
|
|
851
|
+
gap: '8px',
|
|
852
|
+
}}
|
|
853
|
+
>
|
|
854
|
+
<a
|
|
855
|
+
href={`/namespaces/${arr
|
|
856
|
+
.slice(0, index + 1)
|
|
857
|
+
.join('.')}`}
|
|
858
|
+
style={{
|
|
859
|
+
fontWeight: '400',
|
|
860
|
+
color: '#1e293b',
|
|
861
|
+
textDecoration: 'none',
|
|
862
|
+
}}
|
|
787
863
|
>
|
|
788
|
-
{
|
|
789
|
-
</
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
864
|
+
{part}
|
|
865
|
+
</a>
|
|
866
|
+
{index < arr.length - 1 && (
|
|
867
|
+
<span style={{ color: '#94a3b8', fontWeight: '400' }}>
|
|
868
|
+
/
|
|
869
|
+
</span>
|
|
870
|
+
)}
|
|
871
|
+
</span>
|
|
872
|
+
))
|
|
873
|
+
) : (
|
|
874
|
+
<span style={{ fontWeight: '600', color: '#1e293b' }}>
|
|
875
|
+
All Namespaces
|
|
876
|
+
</span>
|
|
877
|
+
)}
|
|
878
|
+
{currentNamespaceSources &&
|
|
879
|
+
currentNamespaceSources.total_deployments > 0 &&
|
|
880
|
+
(currentNamespaceSources.primary_source?.type === 'git' ? (
|
|
801
881
|
<a
|
|
802
|
-
|
|
803
|
-
|
|
882
|
+
href={`https://${currentNamespaceSources.primary_source.repository}`}
|
|
883
|
+
target="_blank"
|
|
884
|
+
rel="noopener noreferrer"
|
|
885
|
+
title={`${
|
|
886
|
+
currentNamespaceSources.primary_source.repository
|
|
887
|
+
}${
|
|
888
|
+
currentNamespaceSources.primary_source.branch
|
|
889
|
+
? ` (${currentNamespaceSources.primary_source.branch})`
|
|
890
|
+
: ''
|
|
891
|
+
}`}
|
|
892
|
+
style={{
|
|
893
|
+
display: 'inline-flex',
|
|
894
|
+
alignItems: 'center',
|
|
895
|
+
gap: '4px',
|
|
896
|
+
padding: '4px 8px',
|
|
897
|
+
marginLeft: '8px',
|
|
898
|
+
fontSize: '13px',
|
|
899
|
+
borderRadius: '12px',
|
|
900
|
+
backgroundColor: '#fff4de',
|
|
901
|
+
border: '1px solid #d4edda',
|
|
902
|
+
color: '#155724',
|
|
903
|
+
textDecoration: 'none',
|
|
904
|
+
cursor: 'pointer',
|
|
905
|
+
}}
|
|
804
906
|
>
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
907
|
+
<svg
|
|
908
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
909
|
+
width="12"
|
|
910
|
+
height="12"
|
|
911
|
+
viewBox="0 0 24 24"
|
|
912
|
+
fill="none"
|
|
913
|
+
stroke="currentColor"
|
|
914
|
+
strokeWidth="2"
|
|
915
|
+
strokeLinecap="round"
|
|
916
|
+
strokeLinejoin="round"
|
|
917
|
+
style={{ marginRight: '2px' }}
|
|
918
|
+
>
|
|
919
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
920
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
921
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
922
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
923
|
+
</svg>
|
|
924
|
+
Git Managed
|
|
813
925
|
</a>
|
|
814
926
|
) : (
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
927
|
+
<span
|
|
928
|
+
title={
|
|
929
|
+
currentNamespaceSources.has_multiple_sources
|
|
930
|
+
? `Warning: ${currentNamespaceSources.sources.length} deployment sources`
|
|
931
|
+
: currentNamespaceSources.sources?.[0]
|
|
932
|
+
?.last_deployed_by
|
|
933
|
+
? `Last deployed by ${currentNamespaceSources.sources[0].last_deployed_by}`
|
|
934
|
+
: 'Local/adhoc deployment'
|
|
935
|
+
}
|
|
936
|
+
style={{
|
|
937
|
+
display: 'inline-flex',
|
|
938
|
+
alignItems: 'center',
|
|
939
|
+
gap: '4px',
|
|
940
|
+
padding: '2px 8px',
|
|
941
|
+
marginLeft: '8px',
|
|
942
|
+
fontSize: '11px',
|
|
943
|
+
borderRadius: '12px',
|
|
944
|
+
backgroundColor:
|
|
945
|
+
currentNamespaceSources.has_multiple_sources
|
|
946
|
+
? '#fff3cd'
|
|
947
|
+
: '#e2e3e5',
|
|
948
|
+
color: currentNamespaceSources.has_multiple_sources
|
|
949
|
+
? '#856404'
|
|
950
|
+
: '#383d41',
|
|
951
|
+
cursor: 'help',
|
|
952
|
+
}}
|
|
953
|
+
>
|
|
954
|
+
{currentNamespaceSources.has_multiple_sources
|
|
955
|
+
? `⚠️ ${currentNamespaceSources.sources.length} sources`
|
|
956
|
+
: currentNamespaceSources.sources?.[0]
|
|
957
|
+
?.last_deployed_by
|
|
958
|
+
? `Local deploy by ${currentNamespaceSources.sources[0].last_deployed_by}`
|
|
959
|
+
: 'Local'}
|
|
960
|
+
</span>
|
|
961
|
+
))}
|
|
962
|
+
</div>
|
|
963
|
+
<div
|
|
964
|
+
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
965
|
+
>
|
|
966
|
+
<a
|
|
967
|
+
href={`${getDJUrl()}/namespaces/${namespace}/export/yaml`}
|
|
968
|
+
download
|
|
969
|
+
style={{
|
|
970
|
+
display: 'inline-flex',
|
|
971
|
+
alignItems: 'center',
|
|
972
|
+
gap: '4px',
|
|
973
|
+
// padding: '6px 12px',
|
|
974
|
+
fontSize: '13px',
|
|
975
|
+
fontWeight: '500',
|
|
976
|
+
color: '#475569',
|
|
977
|
+
// backgroundColor: '#f8fafc',
|
|
978
|
+
// border: '1px solid #e2e8f0',
|
|
979
|
+
borderRadius: '6px',
|
|
980
|
+
textDecoration: 'none',
|
|
981
|
+
cursor: 'pointer',
|
|
982
|
+
transition: 'all 0.15s ease',
|
|
983
|
+
margin: '0.5em 0px 0px 1em',
|
|
984
|
+
}}
|
|
985
|
+
onMouseOver={e => {
|
|
986
|
+
e.currentTarget.style.color = '#333333';
|
|
987
|
+
}}
|
|
988
|
+
onMouseOut={e => {
|
|
989
|
+
e.currentTarget.style.color = '#475569';
|
|
990
|
+
}}
|
|
991
|
+
title="Export namespace to YAML"
|
|
992
|
+
>
|
|
993
|
+
<svg
|
|
994
|
+
width="14"
|
|
995
|
+
height="14"
|
|
996
|
+
viewBox="0 0 24 24"
|
|
997
|
+
fill="none"
|
|
998
|
+
stroke="currentColor"
|
|
999
|
+
strokeWidth="2"
|
|
1000
|
+
strokeLinecap="round"
|
|
1001
|
+
strokeLinejoin="round"
|
|
1002
|
+
>
|
|
1003
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
1004
|
+
<polyline points="7 10 12 15 17 10"></polyline>
|
|
1005
|
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
1006
|
+
</svg>
|
|
1007
|
+
</a>
|
|
1008
|
+
<AddNodeDropdown namespace={namespace} />
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
<table className="card-table table" style={{ marginBottom: 0 }}>
|
|
1012
|
+
<thead>
|
|
1013
|
+
<tr>
|
|
1014
|
+
{fields.map(field => {
|
|
1015
|
+
const thStyle = {
|
|
1016
|
+
fontFamily:
|
|
1017
|
+
"'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
1018
|
+
fontSize: '11px',
|
|
1019
|
+
fontWeight: '600',
|
|
1020
|
+
textTransform: 'uppercase',
|
|
1021
|
+
letterSpacing: '0.5px',
|
|
1022
|
+
color: '#64748b',
|
|
1023
|
+
padding: '12px 16px',
|
|
1024
|
+
borderBottom: '1px solid #e2e8f0',
|
|
1025
|
+
backgroundColor: 'transparent',
|
|
1026
|
+
};
|
|
1027
|
+
return (
|
|
1028
|
+
<th key={field} style={thStyle}>
|
|
1029
|
+
<button
|
|
1030
|
+
type="button"
|
|
1031
|
+
onClick={() => requestSort(field)}
|
|
1032
|
+
className={'sortable ' + getClassNamesFor(field)}
|
|
1033
|
+
style={{
|
|
1034
|
+
fontSize: 'inherit',
|
|
1035
|
+
fontWeight: 'inherit',
|
|
1036
|
+
letterSpacing: 'inherit',
|
|
1037
|
+
textTransform: 'inherit',
|
|
1038
|
+
fontFamily: 'inherit',
|
|
1039
|
+
}}
|
|
1040
|
+
>
|
|
1041
|
+
{field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
|
|
1042
|
+
</button>
|
|
1043
|
+
</th>
|
|
1044
|
+
);
|
|
1045
|
+
})}
|
|
1046
|
+
<th
|
|
1047
|
+
style={{
|
|
1048
|
+
fontFamily:
|
|
1049
|
+
"'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
|
|
1050
|
+
fontSize: '11px',
|
|
1051
|
+
fontWeight: '600',
|
|
1052
|
+
textTransform: 'uppercase',
|
|
1053
|
+
letterSpacing: '0.5px',
|
|
1054
|
+
color: '#64748b',
|
|
1055
|
+
padding: '12px 16px',
|
|
1056
|
+
borderBottom: '1px solid #e2e8f0',
|
|
1057
|
+
backgroundColor: 'transparent',
|
|
1058
|
+
}}
|
|
1059
|
+
>
|
|
1060
|
+
Actions
|
|
1061
|
+
</th>
|
|
1062
|
+
</tr>
|
|
1063
|
+
</thead>
|
|
1064
|
+
<tbody className="nodes-table-body">{nodesList}</tbody>
|
|
1065
|
+
<tfoot>
|
|
1066
|
+
<tr>
|
|
1067
|
+
<td>
|
|
1068
|
+
{retrieved && hasPrevPage ? (
|
|
1069
|
+
<a
|
|
1070
|
+
onClick={loadPrev}
|
|
1071
|
+
className="previous round pagination"
|
|
1072
|
+
>
|
|
1073
|
+
← Previous
|
|
1074
|
+
</a>
|
|
1075
|
+
) : (
|
|
1076
|
+
''
|
|
1077
|
+
)}
|
|
1078
|
+
{retrieved && hasNextPage ? (
|
|
1079
|
+
<a onClick={loadNext} className="next round pagination">
|
|
1080
|
+
Next →
|
|
1081
|
+
</a>
|
|
1082
|
+
) : (
|
|
1083
|
+
''
|
|
1084
|
+
)}
|
|
1085
|
+
</td>
|
|
1086
|
+
</tr>
|
|
1087
|
+
</tfoot>
|
|
1088
|
+
</table>
|
|
1089
|
+
</div>
|
|
821
1090
|
</div>
|
|
822
1091
|
</div>
|
|
823
1092
|
</div>
|
|
@@ -284,4 +284,32 @@ describe('<NotificationsPage />', () => {
|
|
|
284
284
|
|
|
285
285
|
consoleSpy.mockRestore();
|
|
286
286
|
});
|
|
287
|
+
|
|
288
|
+
it('handles null response from getSubscribedHistory', async () => {
|
|
289
|
+
const mockDjClient = createMockDjClient({
|
|
290
|
+
getSubscribedHistory: jest.fn().mockResolvedValue(null),
|
|
291
|
+
});
|
|
292
|
+
renderWithContext(mockDjClient);
|
|
293
|
+
|
|
294
|
+
await waitFor(() => {
|
|
295
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Should show empty state when API returns null
|
|
299
|
+
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('handles undefined response from getSubscribedHistory', async () => {
|
|
303
|
+
const mockDjClient = createMockDjClient({
|
|
304
|
+
getSubscribedHistory: jest.fn().mockResolvedValue(undefined),
|
|
305
|
+
});
|
|
306
|
+
renderWithContext(mockDjClient);
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Should show empty state when API returns undefined
|
|
313
|
+
expect(screen.getByText(/No notifications yet/i)).toBeInTheDocument();
|
|
314
|
+
});
|
|
287
315
|
});
|
|
@@ -2517,26 +2517,6 @@ export function PreAggDetailsPanel({
|
|
|
2517
2517
|
</div>
|
|
2518
2518
|
</div>
|
|
2519
2519
|
|
|
2520
|
-
{/* Metrics Using This */}
|
|
2521
|
-
<div className="details-section">
|
|
2522
|
-
<h3 className="section-title">
|
|
2523
|
-
<span className="section-icon">◈</span>
|
|
2524
|
-
Metrics Using This
|
|
2525
|
-
</h3>
|
|
2526
|
-
<div className="metrics-list">
|
|
2527
|
-
{relatedMetrics.length > 0 ? (
|
|
2528
|
-
relatedMetrics.map((m, i) => (
|
|
2529
|
-
<div key={i} className="related-metric">
|
|
2530
|
-
<span className="metric-name">{m.short_name}</span>
|
|
2531
|
-
{m.is_derived && <span className="derived-badge">Derived</span>}
|
|
2532
|
-
</div>
|
|
2533
|
-
))
|
|
2534
|
-
) : (
|
|
2535
|
-
<span className="empty-text">No metrics found</span>
|
|
2536
|
-
)}
|
|
2537
|
-
</div>
|
|
2538
|
-
</div>
|
|
2539
|
-
|
|
2540
2520
|
{/* Components Table */}
|
|
2541
2521
|
<div
|
|
2542
2522
|
className="details-section details-section-full"
|
|
@@ -2585,6 +2565,26 @@ export function PreAggDetailsPanel({
|
|
|
2585
2565
|
</div>
|
|
2586
2566
|
</div>
|
|
2587
2567
|
|
|
2568
|
+
{/* Metrics Using This */}
|
|
2569
|
+
<div className="details-section">
|
|
2570
|
+
<h3 className="section-title">
|
|
2571
|
+
<span className="section-icon">◈</span>
|
|
2572
|
+
Metrics Using This
|
|
2573
|
+
</h3>
|
|
2574
|
+
<div className="metrics-list">
|
|
2575
|
+
{relatedMetrics.length > 0 ? (
|
|
2576
|
+
relatedMetrics.map((m, i) => (
|
|
2577
|
+
<div key={i} className="related-metric">
|
|
2578
|
+
<span className="metric-name">{m.short_name}</span>
|
|
2579
|
+
{m.is_derived && <span className="derived-badge">Derived</span>}
|
|
2580
|
+
</div>
|
|
2581
|
+
))
|
|
2582
|
+
) : (
|
|
2583
|
+
<span className="empty-text">No metrics found</span>
|
|
2584
|
+
)}
|
|
2585
|
+
</div>
|
|
2586
|
+
</div>
|
|
2587
|
+
|
|
2588
2588
|
{/* SQL Section */}
|
|
2589
2589
|
{preAgg.sql && (
|
|
2590
2590
|
<div className="details-section details-section-full details-sql-section">
|
|
@@ -1026,7 +1026,7 @@ export function QueryPlannerPage() {
|
|
|
1026
1026
|
selectedMetrics,
|
|
1027
1027
|
selectedDimensions,
|
|
1028
1028
|
'',
|
|
1029
|
-
false, //
|
|
1029
|
+
false, // useMaterialized = false for raw SQL
|
|
1030
1030
|
);
|
|
1031
1031
|
return result.sql;
|
|
1032
1032
|
} catch (err) {
|
|
@@ -70,7 +70,7 @@ describe('<Root />', () => {
|
|
|
70
70
|
expect(navRight).toBeInTheDocument();
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
it('handles notification dropdown toggle', async () => {
|
|
73
|
+
it('handles notification dropdown toggle and closes user menu', async () => {
|
|
74
74
|
renderRoot();
|
|
75
75
|
|
|
76
76
|
await waitFor(() => {
|
|
@@ -80,16 +80,21 @@ describe('<Root />', () => {
|
|
|
80
80
|
// Find the notification bell button and click it
|
|
81
81
|
const bellButton = document.querySelector('[aria-label="Notifications"]');
|
|
82
82
|
if (bellButton) {
|
|
83
|
+
// First click opens notifications
|
|
84
|
+
fireEvent.click(bellButton);
|
|
85
|
+
await waitFor(() => {
|
|
86
|
+
expect(bellButton).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Click again to close
|
|
83
90
|
fireEvent.click(bellButton);
|
|
84
|
-
// The dropdown should open
|
|
85
91
|
await waitFor(() => {
|
|
86
|
-
// After clicking, the dropdown state changes
|
|
87
92
|
expect(bellButton).toBeInTheDocument();
|
|
88
93
|
});
|
|
89
94
|
}
|
|
90
95
|
});
|
|
91
96
|
|
|
92
|
-
it('handles user menu dropdown toggle', async () => {
|
|
97
|
+
it('handles user menu dropdown toggle and closes notification dropdown', async () => {
|
|
93
98
|
renderRoot();
|
|
94
99
|
|
|
95
100
|
await waitFor(() => {
|
|
@@ -99,6 +104,17 @@ describe('<Root />', () => {
|
|
|
99
104
|
// The nav-right container should be present for auth-enabled mode
|
|
100
105
|
const navRight = document.querySelector('.nav-right');
|
|
101
106
|
expect(navRight).toBeInTheDocument();
|
|
107
|
+
|
|
108
|
+
// Find the user menu button (look for avatar or user icon)
|
|
109
|
+
const userMenuBtn = document.querySelector(
|
|
110
|
+
'.user-menu-button, .user-avatar, [aria-label*="user"], [aria-label*="menu"]',
|
|
111
|
+
);
|
|
112
|
+
if (userMenuBtn) {
|
|
113
|
+
fireEvent.click(userMenuBtn);
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(userMenuBtn).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
102
118
|
});
|
|
103
119
|
|
|
104
120
|
it('renders logo link correctly', async () => {
|
|
@@ -112,4 +128,83 @@ describe('<Root />', () => {
|
|
|
112
128
|
const logoLink = screen.getByRole('link', { name: /data.*junction/i });
|
|
113
129
|
expect(logoLink).toHaveAttribute('href', '/');
|
|
114
130
|
});
|
|
131
|
+
|
|
132
|
+
it('toggles between notification and user dropdowns exclusively', async () => {
|
|
133
|
+
renderRoot();
|
|
134
|
+
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(document.title).toEqual('DataJunction');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const navRight = document.querySelector('.nav-right');
|
|
140
|
+
expect(navRight).toBeInTheDocument();
|
|
141
|
+
|
|
142
|
+
// Find both dropdown triggers
|
|
143
|
+
const bellButton = document.querySelector('[aria-label="Notifications"]');
|
|
144
|
+
const userMenuTrigger = navRight?.querySelector('button, [role="button"]');
|
|
145
|
+
|
|
146
|
+
if (bellButton && userMenuTrigger && bellButton !== userMenuTrigger) {
|
|
147
|
+
// Click notification first
|
|
148
|
+
fireEvent.click(bellButton);
|
|
149
|
+
await waitFor(() => {
|
|
150
|
+
expect(bellButton).toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Now click user menu - should close notifications
|
|
154
|
+
fireEvent.click(userMenuTrigger);
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(userMenuTrigger).toBeInTheDocument();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Click notifications again - should close user menu
|
|
160
|
+
fireEvent.click(bellButton);
|
|
161
|
+
await waitFor(() => {
|
|
162
|
+
expect(bellButton).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('sets openDropdown state correctly for notification toggle', async () => {
|
|
168
|
+
renderRoot();
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(document.title).toEqual('DataJunction');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const bellButton = document.querySelector('[aria-label="Notifications"]');
|
|
175
|
+
if (bellButton) {
|
|
176
|
+
// Open notifications dropdown
|
|
177
|
+
fireEvent.click(bellButton);
|
|
178
|
+
|
|
179
|
+
// The dropdown toggle handler should have been called with isOpen=true
|
|
180
|
+
// which sets openDropdown to 'notifications'
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
expect(bellButton).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('sets openDropdown state correctly for user menu toggle', async () => {
|
|
188
|
+
renderRoot();
|
|
189
|
+
|
|
190
|
+
await waitFor(() => {
|
|
191
|
+
expect(document.title).toEqual('DataJunction');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const navRight = document.querySelector('.nav-right');
|
|
195
|
+
if (navRight) {
|
|
196
|
+
// Find user menu element (usually second clickable element in nav-right)
|
|
197
|
+
const buttons = navRight.querySelectorAll('button, [role="button"]');
|
|
198
|
+
if (buttons.length > 0) {
|
|
199
|
+
const userButton = buttons[buttons.length - 1]; // Last button is usually user menu
|
|
200
|
+
fireEvent.click(userButton);
|
|
201
|
+
|
|
202
|
+
// The dropdown toggle handler should have been called with isOpen=true
|
|
203
|
+
// which sets openDropdown to 'user'
|
|
204
|
+
await waitFor(() => {
|
|
205
|
+
expect(userButton).toBeInTheDocument();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
115
210
|
});
|