Merge for 2.2.0 release

This commit is contained in:
Frikky
2026-02-10 13:29:26 +01:00
parent 487927da3e
commit f51848fe65
64 changed files with 12124 additions and 2657 deletions
@@ -0,0 +1,5 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.59995 4.7C1.44015 4.7 0.499951 3.7598 0.499951 2.6C0.499951 1.4402 1.44015 0.5 2.59995 0.5C3.75975 0.5 4.69995 1.4402 4.69995 2.6C4.69995 3.7598 3.75975 4.7 2.59995 4.7Z" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.4 14.4998C11.2402 14.4998 10.3 13.5596 10.3 12.3998C10.3 11.24 11.2402 10.2998 12.4 10.2998C13.5598 10.2998 14.5 11.24 14.5 12.3998C14.5 13.5596 13.5598 14.4998 12.4 14.4998Z" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.69995 2.6001H5.52832C6.62175 2.6001 7.5125 3.47821 7.52811 4.57153L7.61179 10.4287C7.62741 11.522 8.51815 12.4001 9.61158 12.4001H10.3" stroke="#1A1A1A"/>
</svg>

After

Width:  |  Height:  |  Size: 774 B

@@ -0,0 +1,5 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.3001 4.1C1.30598 4.1 0.500098 3.29411 0.500098 2.3C0.500098 1.30589 1.30598 0.5 2.3001 0.5C3.29421 0.5 4.1001 1.30589 4.1001 2.3C4.1001 3.29411 3.29421 4.1 2.3001 4.1Z" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.7002 12.5001C9.70613 12.5001 8.90024 11.6943 8.90024 10.7001C8.90024 9.70603 9.70613 8.90015 10.7002 8.90015C11.6944 8.90015 12.5002 9.70603 12.5002 10.7001C12.5002 11.6943 11.6944 12.5001 10.7002 12.5001Z" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.1001 2.2998H4.52847C5.62189 2.2998 6.51264 3.17792 6.52826 4.27124L6.59193 8.72837C6.60755 9.82169 7.4983 10.6998 8.59173 10.6998H8.9001" stroke="#F1F1F1"/>
</svg>

After

Width:  |  Height:  |  Size: 805 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.33325 9.99992C3.33325 6.31802 6.31802 3.33325 9.99992 3.33325V3.33325C13.6818 3.33325 16.6666 6.31802 16.6666 9.99992V14.2423C16.6666 14.6365 16.6666 14.8336 16.6389 14.9982C16.4975 15.8389 15.8389 16.4975 14.9982 16.6389C14.8336 16.6666 14.6365 16.6666 14.2423 16.6666H9.99992C6.31802 16.6666 3.33325 13.6818 3.33325 9.99992V9.99992Z" stroke="#1A1A1A"/>
<path d="M7.5 10L12.5 10" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 7.5L10 12.5" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 654 B

@@ -0,0 +1,5 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.68579 8.05757C2.68579 5.09082 5.09082 2.68579 8.05757 2.68579V2.68579C11.0243 2.68579 13.4293 5.09082 13.4293 8.05757V11.476C13.4293 11.7936 13.4293 11.9524 13.407 12.085C13.2931 12.7624 12.7624 13.2931 12.085 13.407C11.9524 13.4293 11.7936 13.4293 11.476 13.4293H8.05757C5.09082 13.4293 2.68579 11.0243 2.68579 8.05757V8.05757Z" stroke="#9E9E9E" stroke-width="0.805766"/>
<path d="M6.04321 8.05762L10.072 8.05762" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.05762 6.04321L8.05762 10.072" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 5.41675H17.5C16.6463 5.41675 16.2194 5.41675 15.8788 5.54494C15.3398 5.74781 14.9144 6.17322 14.7115 6.71225C14.5833 7.05284 14.5833 7.4797 14.5833 8.33341V12.2501C14.5833 14.1357 14.5833 15.0785 13.9975 15.6643C13.4118 16.2501 12.469 16.2501 10.5833 16.2501H9.41667C7.53105 16.2501 6.58824 16.2501 6.00245 15.6643C5.41667 15.0785 5.41667 14.1357 5.41667 12.2501V8.33341C5.41667 7.47969 5.41667 7.05284 5.28848 6.71225C5.0856 6.17322 4.6602 5.74781 4.12117 5.54494C3.78058 5.41675 3.35372 5.41675 2.5 5.41675Z" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.91659 2.91679C7.91659 2.91679 8.33325 2.08325 9.99992 2.08325C11.6666 2.08325 12.0833 2.91659 12.0833 2.91659" stroke="#1A1A1A" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

@@ -0,0 +1,4 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.0144 4.3645H14.1009C13.413 4.3645 13.0691 4.3645 12.7946 4.46779C12.3603 4.63126 12.0175 4.97404 11.854 5.40837C11.7507 5.68281 11.7507 6.02676 11.7507 6.71465V9.87057C11.7507 11.3899 11.7507 12.1496 11.2787 12.6216C10.8067 13.0936 10.047 13.0936 8.52768 13.0936H7.58762C6.06825 13.0936 5.30857 13.0936 4.83656 12.6216C4.36456 12.1496 4.36456 11.3899 4.36456 9.87057V6.71465C4.36456 6.02676 4.36456 5.68281 4.26127 5.40837C4.0978 4.97404 3.75502 4.63126 3.32069 4.46779C3.04625 4.3645 2.7023 4.3645 2.0144 4.3645Z" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.37897 2.35035C6.37897 2.35035 6.7147 1.67871 8.05765 1.67871C9.40059 1.67871 9.73633 2.35018 9.73633 2.35018" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 908 B

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.8335 7.50008L10.0002 11.6667M10.0002 11.6667L14.1668 7.50008M10.0002 11.6667L10.0002 4.16675" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.1665 13.3333L4.1665 14.1666C4.1665 15.0871 4.9127 15.8333 5.83317 15.8333L14.1665 15.8333C15.087 15.8333 15.8332 15.0871 15.8332 14.1666V13.3333" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

@@ -0,0 +1,4 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.7002 6.04328L8.05755 9.40063M8.05755 9.40063L11.4149 6.04328M8.05755 9.40063L8.05756 3.35739" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.35742 10.7437L3.35742 11.4151C3.35742 12.1568 3.95868 12.7581 4.70037 12.7581L11.4151 12.7581C12.1568 12.7581 12.758 12.1568 12.758 11.4151V10.7437" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 550 B

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6665 11.6667V16.6667M16.6665 16.6667H11.6665M16.6665 16.6667L12.4998 12.5001" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.3335 11.6667V16.6667M3.3335 16.6667H8.3335M3.3335 16.6667L7.50016 12.5001" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.6665 8.33325V3.33325M16.6665 3.33325H11.6665M16.6665 3.33325L12.4998 7.49992" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.3335 8.33325V3.33325M3.3335 3.33325H8.3335M3.3335 3.33325L7.50016 7.49992" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6665 11.6667V16.6667M16.6665 16.6667H11.6665M16.6665 16.6667L12.4998 12.5001" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.3335 11.6667V16.6667M3.3335 16.6667H8.3335M3.3335 16.6667L7.50016 12.5001" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.6665 8.33325V3.33325M16.6665 3.33325H11.6665M16.6665 3.33325L12.4998 7.49992" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.3335 8.33325V3.33325M3.3335 3.33325H8.3335M3.3335 3.33325L7.50016 7.49992" stroke="#9E9E9E" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3.77051V8.43717H6.66666" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.95222 12.3261C4.45653 13.7575 5.41237 14.9862 6.67573 15.8271C7.93909 16.6681 9.44152 17.0756 10.9567 16.9884C12.4718 16.9012 13.9175 16.324 15.0761 15.3437C16.2346 14.3633 17.0432 13.033 17.3799 11.5532C17.7167 10.0734 17.5634 8.52426 16.9431 7.13915C16.3229 5.75404 15.2693 4.60803 13.9411 3.87378C12.6129 3.13954 11.082 2.85684 9.57919 3.06828C8.07635 3.27972 6.68293 3.97385 5.60889 5.04608L2 8.43718" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.4678 6.01172V10.095L12.4122 11.0673" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 806 B

@@ -0,0 +1,5 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.61157 3.03809V6.79833H5.37181" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.18461 9.93187C3.59096 11.0852 4.36114 12.0753 5.37912 12.7529C6.39709 13.4305 7.6077 13.7589 8.82855 13.6886C10.0494 13.6184 11.2143 13.1532 12.1478 12.3633C13.0814 11.5734 13.7329 10.5015 14.0042 9.30915C14.2755 8.11677 14.152 6.8685 13.6522 5.75243C13.1525 4.63635 12.3035 3.71293 11.2333 3.1213C10.1631 2.52967 8.92956 2.30188 7.71862 2.47225C6.50769 2.64263 5.38492 3.20193 4.51949 4.0659L1.61157 6.79834" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.43481 4.84399V8.1342L10.0016 8.91759" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 888 B

@@ -0,0 +1,3 @@
<svg width="11" height="13" viewBox="0 0 11 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.402832 1.21052C0.402832 0.580157 1.09404 0.193914 1.6309 0.524288L9.76266 5.52845C10.2739 5.84309 10.2739 6.58628 9.76266 6.90092L1.6309 11.9051C1.09404 12.2355 0.402832 11.8492 0.402832 11.2188V1.21052Z" stroke="#1A1A1A" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

@@ -0,0 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.2002 11.4463L6.48223 12.1027L7.46684 9.14885L8.45145 4.55396" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.4668 9.47681L9.43602 10.7896L9.76422 14.3999" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.45147 5.21021L5.16943 6.19483V8.16407" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.7793 2.00342C9.46296 2.00342 10.0174 2.55709 10.0176 3.24072C10.0176 3.92454 9.4631 4.479 8.7793 4.479C8.09568 4.47879 7.54199 3.92441 7.54199 3.24072C7.54221 2.55723 8.09582 2.00363 8.7793 2.00342Z" fill="#1A1A1A" stroke="#1A1A1A" stroke-width="0.805766"/>
<path d="M8.45117 5.21021L10.7486 7.50765L12.3896 8.16407" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 879 B

@@ -0,0 +1,7 @@
<svg width="11" height="14" viewBox="0 0 11 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 9.8462L3.78203 10.5026L4.76664 7.54875L5.75125 2.95386" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.7666 7.87671L6.73582 9.18954L7.06403 12.7998" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.75103 3.61011L2.46899 4.59473V6.56397" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.07886 0.40332C6.76252 0.40332 7.31692 0.956997 7.31714 1.64062C7.31714 2.32444 6.76266 2.87891 6.07886 2.87891C5.39524 2.87869 4.84155 2.32431 4.84155 1.64062C4.84177 0.957128 5.39538 0.403532 6.07886 0.40332Z" fill="#F1F1F1" stroke="#F1F1F1" stroke-width="0.805766"/>
<path d="M5.75098 3.61011L8.0484 5.90756L9.68942 6.56397" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 885 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.50016 12.5L3.3335 8.33333M3.3335 8.33333L7.50016 4.16667M3.3335 8.33333L10.6668 8.33333C13.9805 8.33333 16.6668 11.0196 16.6668 14.3333L16.6668 15" stroke="#1A1A1A" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

@@ -0,0 +1,3 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.04315 10.072L2.68579 6.71465M2.68579 6.71465L6.04315 3.35729M2.68579 6.71465L8.59475 6.71465C11.2648 6.71465 13.4293 8.87917 13.4293 11.5492L13.4293 12.0864" stroke="#9E9E9E" stroke-width="0.805766" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 363 B

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6666 15.8333H12.9999C14.1045 15.8333 14.9999 14.9378 14.9999 13.8333V12.3948C14.9999 11.4771 15.6245 10.6771 16.5148 10.4545L17.3631 10.2425C17.6156 10.1793 17.6156 9.8205 17.3631 9.75738L16.5148 9.54532C15.6245 9.32273 14.9999 8.52277 14.9999 7.60503V6.16658C14.9999 5.06202 14.1045 4.16658 12.9999 4.16658H11.6666" stroke="#1A1A1A" stroke-linejoin="round"/>
<path d="M8.33341 4.16675H7.00008C5.89551 4.16675 5.00008 5.06218 5.00008 6.16675V7.6052C5.00008 8.52293 4.37549 9.3229 3.48515 9.54548L2.63689 9.75755C2.3844 9.82067 2.3844 10.1795 2.63689 10.2426L3.48515 10.4547C4.37549 10.6773 5.00008 11.4772 5.00008 12.395V13.8334C5.00008 14.938 5.89551 15.8334 7.00008 15.8334H8.33341" stroke="#1A1A1A" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B

@@ -0,0 +1,4 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.40056 12.7581H10.4749C11.3649 12.7581 12.0865 12.0365 12.0865 11.1465V9.98747C12.0865 9.24799 12.5897 8.60341 13.3071 8.42406L13.9906 8.25318C14.1941 8.20232 14.1941 7.91319 13.9906 7.86233L13.3071 7.69145C12.5897 7.5121 12.0865 6.86751 12.0865 6.12803V4.96898C12.0865 4.07896 11.3649 3.35745 10.4749 3.35745H9.40056" stroke="#9E9E9E" stroke-width="0.805766" stroke-linejoin="round"/>
<path d="M6.71467 3.35742H5.64032C4.75029 3.35742 4.02878 4.07893 4.02878 4.96895V6.12801C4.02878 6.86749 3.52551 7.51207 2.8081 7.69142L2.1246 7.8623C1.92116 7.91316 1.92116 8.20229 2.1246 8.25315L2.8081 8.42403C3.52551 8.60338 4.02878 9.24796 4.02878 9.98744V11.1465C4.02878 12.0365 4.75029 12.758 5.64032 12.758H6.71467" stroke="#9E9E9E" stroke-width="0.805766" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 891 B

+1 -9
View File
@@ -211,15 +211,7 @@ const AdminNavBar = (props) => {
} else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams?.get('admin_tab')?.toLowerCase();
if (tabName === "sso") {
toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab.");
setTimeout(() => {
setSelectedItem("Organization");
navigate(`?admin_tab=org_config`, { replace: true });
window.location.reload();
}
, 3000);
}
// SSO tab is now accessible to non-admins (removed restriction)
const params = new URLSearchParams(location.search);
const tab = params?.get('tab')?.toLowerCase();
+28 -18
View File
@@ -1050,7 +1050,7 @@ const AppAuthTab = memo((props) => {
<ListItem style={{ width: "100%", paddingTop: 10, paddingBottom: 10, paddingRight: 10, borderBottom: theme.palette.defaultBorder, display: 'table-row'}}>
{["Valid", "Label", "App Name", "Workflows", "Fields", "Edited", "Actions", "Distribution"].map((header, index) => (
{["Logo", "Valid", "Label", "App Name", "Fields", "Edited", "Actions", "Distribute"].map((header, index) => (
<ListItemText
key={index}
primary={header}
@@ -1145,9 +1145,33 @@ const AppAuthTab = memo((props) => {
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor, borderBottomLeftRadius: authentication?.length - 1 === index ? 8 : 0, borderBottomRightRadius: authentication?.length - 1 === index ? 8 : 0, display: 'table-row' }}>
<ListItemText
primary= {(
<Tooltip title={data.validation !== null && data.validation !== undefined && data.validation.valid === true ? "Valid. Click to explore." : "Configuration failed. Click to learn why"} placement="top">
<Tooltip title={`Auth for ${data.app.name}. Click to explore.`} placement="top">
<IconButton>
<img
style={{height: 30, width: 30, borderRadius: theme.palette?.borderRadius }}
src={data.app.large_image ? data.app.large_image : '/images/no_image.png'}
/>
</IconButton>
</Tooltip>
)}
style={{ display: "table-cell", verticalAlign: 'middle', minWidth: 60 }}
primaryTypographyProps={{
style: {
padding: "8px 8px 8px 15px",
}
}}
onClick={() => {
const url = `/apps/${data.app.id}`
window.open(url, "_blank")
}}
/>
<ListItemText
primary= {(
<Tooltip title={data.validation !== null && data.validation !== undefined && data.validation.valid === true ? `Last Valid ${new Date(data?.validation?.last_valid).toISOString()}. Click to explore.` : "Configuration failed. Click to learn why"} placement="top">
<IconButton>
{validIcon}
</IconButton>
@@ -1202,22 +1226,7 @@ const AppAuthTab = memo((props) => {
}}
style={{ marginLeft: 10, display: "table-cell", textAlign: 'center', verticalAlign: 'middle', padding: 8 }}
/>
<ListItemText
primary={
data.workflow_count === null ? 0 : data.workflow_count
}
primaryTypographyProps={{
style: {
padding: 8
}
}}
style={{
display: "table-cell",
textAlign: "center",
overflow: "hidden",
verticalAlign: 'middle',
}}
/>
<ListItemText
primary={
data.fields === null || data.fields === undefined
@@ -1239,6 +1248,7 @@ const AppAuthTab = memo((props) => {
verticalAlign: 'middle'
}}
/>
<ListItemText
style={{
overflow: "hidden",
+27 -3
View File
@@ -22,10 +22,10 @@ import {
import aa from 'search-insights'
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true");
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
const [formMail, setFormMail] = React.useState("");
@@ -75,7 +75,7 @@ const Appsearch = props => {
color="primary"
defaultValue={defaultSearch}
// placeholder={`Find ${defaultSearch} Apps...`}
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Cases "}
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Apps "}
id="shuffle_workflow_search_field"
onChange={(event) => {
refine(event.currentTarget.value)
@@ -92,6 +92,30 @@ const Appsearch = props => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
var counted = 0
if (hits?.length < 20 && apps !== undefined && apps !== null && apps?.length > 0) {
// Find the textbox html
const findId = "shuffle_workflow_search_field"
const searchField = document.getElementById(findId)
const searchValue = searchField === null || searchField === undefined ? "" : searchField?.value
if (searchValue?.length > 0) {
const parsedsearch = searchValue.toLowerCase().replaceAll(" ", "")
for (var appkey in apps) {
const app = apps[appkey]
const parsedAppname = app?.name?.toLowerCase().replaceAll(" ", "")
if (parsedAppname.includes(parsedsearch)) {
hits.push({
objectID: app.id,
id: app.id,
name: app.name,
image_url: app.large_image,
})
}
}
} else {
console.log("No value in textfield: ", searchValue)
}
}
return (
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: parsedInputHeight, minHeight: parsedInputHeight, overflowY: "auto", overflowX: "hidden", }}>
{hits.map((data, index) => {
@@ -97,6 +97,12 @@ const AuthenticationModal = (props) => {
}
if (authenticationModalOpen === false) {
// Add 5px for every character over 12
var addednumber = selectedAppData?.name?.length > 12 ? (selectedAppData?.name?.length - 12) * 5 : 0
if (addednumber > 50) {
addednumber = 50
}
return (
<Button
fullWidth
@@ -111,8 +117,8 @@ const AuthenticationModal = (props) => {
backgroundColor: "#ffffff",
color: "#2f2f2f",
borderRadius: theme.palette?.borderRadius,
minWidth: 275,
maxWidth: 275,
minWidth: 275+addednumber,
maxWidth: 275+addednumber,
maxHeight: 50,
overflow: "hidden",
border: `1px solid ${theme.palette.inputColor}`,
@@ -128,10 +134,10 @@ const AuthenticationModal = (props) => {
<span style={{display: "flex"}}>
<img
alt={selectedAppData?.name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
style={{ margin: "4px 4px 4px 0px", minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={selectedAppData?.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
<Typography style={{ margin: 0, marginLeft: 5, marginTop: 8, color: "#2f2f2f",}} variant="body1">
Authenticate {selectedAppData?.name?.replaceAll("_", " ", -1)}
</Typography>
</span>
+300 -41
View File
@@ -50,6 +50,15 @@ import {
Search as SearchIcon,
CheckCircle as CheckCircleIcon,
Cancel as CancelIcon,
Shield as ShieldIcon,
Cancel as XCircleIcon,
FlashOn as ZapIcon,
People as UsersIcon,
FmdGoodOutlined as FmdGoodOutlinedIcon,
Palette as PaletteIcon,
Info as InfoIcon,
Email as MailIcon,
ArrowForward as ArrowRightIcon,
} from "@mui/icons-material";
//import { useAlert
@@ -70,51 +79,301 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
} else {
isProdStatusOn = false;
}
const rows = [
{ label: 'Licensed', ok: isProdStatusOn },
{ label: 'Multi-Tenant', ok: isProdStatusOn },
{ label: 'High Availability', ok: isProdStatusOn },
{ label: 'Robust Infrastructure', ok: isProdStatusOn },
const themeMode = theme.palette.mode;
const workflowActive = selectedOrganization?.sync_features?.workflow_executions?.active;
const multiTenantActive = selectedOrganization?.sync_features?.multi_tenant?.active;
const multiEnvActive = selectedOrganization?.sync_features?.multi_env?.active;
const brandingActive = selectedOrganization?.sync_features?.branding?.active;
const colors = {
textPrimary: theme.palette.text.primary,
textSecondary: theme.palette.text.secondary,
textMuted: themeMode === "dark" ? '#6e7681' : '#9ca3af',
border: themeMode === "dark" ? '#30363d' : '#e1e4e8',
divider: themeMode === "dark" ? '#21262d' : '#e5e7eb',
success: themeMode === "dark" ? '#10b981' : '#059669',
successBg: themeMode === "dark" ? 'rgba(16, 185, 129, 0.12)' : 'rgba(5, 150, 105, 0.08)',
warning: '#f59e0b',
warningBg: themeMode === "dark" ? 'rgba(245, 158, 11, 0.12)' : 'rgba(245, 158, 11, 0.08)',
disabled: themeMode === "dark" ? '#6e7681' : '#d1d5db',
disabledBg: themeMode === "dark" ? 'rgba(110, 118, 129, 0.1)' : 'rgba(156, 163, 175, 0.08)',
accent: '#f85a3e',
cardBg: theme.palette.surfaceColor,
};
const features = [
{
icon: ZapIcon,
label: 'Workflow Executions',
licensed: `${selectedOrganization?.sync_features?.workflow_executions?.limit}/month limit`,
unlicensed: '10,000/month limit',
isActive: workflowActive,
},
{
icon: UsersIcon,
label: 'Multi-Tenant',
licensed: `${selectedOrganization?.sync_features?.multi_tenant?.limit} tenants`,
unlicensed: '3 tenants maximum',
isActive: multiTenantActive,
},
{
icon: FmdGoodOutlinedIcon,
label: 'Runtime Locations',
licensed: `${selectedOrganization?.sync_features?.multi_env?.limit} Runtime Locations`,
unlicensed: '1 Runtime Location only',
isActive: multiEnvActive,
},
{
icon: PaletteIcon,
label: 'Custom Branding',
licensed: `${brandingActive ? "Full branding control" : "Branding not available"}`,
unlicensed: 'Branding not available',
isActive: brandingActive,
},
{
icon: ShieldIcon,
label: 'High Availability',
licensed: 'Enterprise SLA guarantee',
unlicensed: 'Standard availability',
isActive: isProdStatusOn, // High availability is tied to license status
},
];
return (
<div style={{ width: '100%', maxWidth: 800, padding: '0px 24px 24px 0px', height: 445 , display: 'flex', flexDirection: 'column', alignItems: 'flex-start'}}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, marginBottom: 8 }}>
<Typography variant="h5" style={{ fontWeight: 600, fontFamily: theme.typography.fontFamily }}>Production Status</Typography>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 14px', borderRadius: 16, background: isProdStatusOn ? 'rgba(43,192,126,0.1)' : 'rgba(253,76,98,0.1)' }}>
<span style={{ width: 8, height: 8, borderRadius: 999, background: isProdStatusOn ? '#2BC07E' : '#FD4C62' }} />
<Typography variant="caption" style={{ color: isProdStatusOn ? '#2BC07E' : '#FD4C62', fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{isProdStatusOn ? "ON" : "OFF"}</Typography>
</div>
<div
style={{
width: '100%',
maxWidth: 800,
padding: "0px 24px 24px 0px",
display: 'flex',
flexDirection: 'column',
fontFamily: theme.typography.fontFamily,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<Typography variant="h5" style={{ fontWeight: 600, margin: 0 }}>
License Status
</Typography>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '8px 16px',
borderRadius: 20,
background: isProdStatusOn ? colors.successBg : colors.warningBg,
border: `2px solid ${isProdStatusOn ? colors.success : colors.warning}`,
boxShadow: isProdStatusOn
? `0 2px 8px ${colors.success}30`
: `0 2px 8px ${colors.warning}30`,
height: 12,
}}
>
<span
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: isProdStatusOn ? colors.success : colors.warning,
boxShadow: `0 0 10px ${isProdStatusOn ? colors.success : colors.warning}`,
}}
/>
<span
style={{
fontSize: 14,
fontWeight: 700,
color: isProdStatusOn ? colors.success : colors.warning,
letterSpacing: '0.5px',
}}
>
{isProdStatusOn ? 'Licensed' : 'Unlicensed'}
</span>
</div>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 18, fontFamily: theme.typography.fontFamily }}>
Monitor your production status to stay informed about available features.
</Typography>
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
{rows.map((row) => (
<div key={row.label} style={{ display: 'flex', alignItems: 'center', gap: 12, fontFamily: theme.typography.fontFamily }}>
{row.ok ? (
<CheckCircleIcon style={{ color: '#2BC07E' }} />
) : (
<CancelIcon style={{ color: '#FD4C62' }} />
)}
<Typography variant="body1" style={{ fontWeight: 400, fontFamily: theme.typography.fontFamily }}>{row.label}</Typography>
</div>
))}
</div>
<Divider style={{
width: '100%',
marginTop: 32,
marginBottom: 16,
borderColor: theme.palette.defaultBorder,
}} />
<Typography variant="body1" color="textPrimary" style={{ marginTop: 24, fontFamily: theme.typography.fontFamily }}>
Shuffle Enterprise is designed for organizations that require scalability, high availability, dedicated support and more to run mission-critical workflows in production environments.
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 24, fontFamily: theme.typography.fontFamily }}>
More about upgrading below. If you want to know more, please contact <a href="mailto:support@shuffler.io?subject=Tell%20me%20about%20Production%20readiness" style={{ color: '#f85a3e', textDecoration: 'none' }}>support@shuffler.io</a> directly.
</Typography>
{/* Subtitle */}
<Typography
variant="body2"
color="textSecondary"
style={{
margin: '0 0 24px 0',
lineHeight: 1.5,
}}
>
{isProdStatusOn
? 'Your organization has full access to all enterprise features and capabilities.'
: 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'}
</Typography>
{/* Features Grid */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 12,
marginBottom: 32,
}}
>
{features
.sort((a, b) => {
const aActive = isProdStatusOn && a.isActive;
const bActive = isProdStatusOn && b.isActive;
return bActive - aActive;
})
.map((feature, index) => {
const Icon = feature.icon;
const isAvailable = isProdStatusOn && feature.isActive;
const statusColor = isAvailable ? colors.success : colors.disabled;
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
return (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
gap: 14,
padding: '14px 16px',
borderRadius: 10,
background: bgColor,
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
transition: 'all 0.2s',
}}
>
{/* Icon */}
<div
style={{
width: 40,
height: 40,
borderRadius: 8,
background: isAvailable ? colors.success + '20' : colors.disabledBg,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<Icon style={{ color: statusColor, fontSize: 20 }} />
</div>
{/* Content */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 15,
fontWeight: 600,
color: colors.textPrimary,
marginBottom: 4,
}}
>
{feature.label}
</div>
<div
style={{
fontSize: 13,
color: colors.textSecondary,
lineHeight: 1.4,
}}
>
{isProdStatusOn ? feature.licensed : feature.unlicensed}
</div>
</div>
{isAvailable ? (
<CheckCircleIcon
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
/>
) : (
<XCircleIcon
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }}
/>
)}
</div>
);
})}
</div>
<div
style={{
width: '100%',
height: 1,
background: colors.divider,
margin: '8px 0 24px 0',
}}
/>
{!isProdStatusOn && (
<div
style={{
padding: 20,
borderRadius: 12,
background: themeMode == "dark" ? "#212121" : "#ffffff",
border: `1px solid ${colors.border}`,
marginBottom: 20,
}}
>
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} />
<div>
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
About Shuffle Enterprise
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{
margin: 0,
lineHeight: 1.6,
}}
>
Shuffle Enterprise is designed for organizations that require scalability, high
availability, dedicated support, and robust infrastructure to run mission-critical
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a>
</Typography>
</div>
</div>
</div>)}
{/* CTA Section */}
{!isProdStatusOn && (
<div
style={{
padding: 20,
borderRadius: 12,
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`,
border: `1px solid #FF854440`,
marginBottom: 24,
}}
>
<div style={{ marginBottom: 16 }}>
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
Ready to upgrade?
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{
margin: 0,
lineHeight: 1.6,
}}
>
Contact our team to learn more about enterprise features and pricing.
</Typography>
</div>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
startIcon={<MailIcon sx={{ fontSize: 16}} />}
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
>
Contact Sales
</Button>
</div>
)}
</div>
);
};
@@ -2143,14 +2402,14 @@ const Billing = memo((props) => {
return (
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
<div style={{ width: "100%", padding: 24, }}>
<div style={{ width: "100%", padding: 24, paddingTop: 0}}>
<div style={{ width: "100%", maxWidth: 800, }}>
{isCloud ? null : <ProductionStatus selectedOrganization={selectedOrganization} userdata={userdata} isCloud={isCloud} theme={theme} />}
{addDealModal}
{clickedFromOrgTab ?
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 24, }}>Billing & Licensing</Typography>
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, }}>Billing & Licensing</Typography>
:
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
Billing & Licensing
+367 -82
View File
@@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
import ReactJson from "react-json-view-ssr";
import { validateJson, handleReactJsonClipboard, GetIconInfo, } from "../views/Workflows2.jsx";
import { red } from "../views/AngularWorkflow.jsx";
import { green, red } from "../views/AngularWorkflow.jsx";
import CollectIngestModal from "../components/CollectIngestModal.jsx";
import CorrelationGraph from "../components/CorrelationGraph.jsx";
import { useNavigate, Link, useParams } from "react-router-dom";
@@ -37,9 +37,12 @@ import {
Pagination,
PaginationItem,
Avatar,
ListSubheader,
} from "@mui/material";
import { DataGrid } from '@mui/x-data-grid';
import {
DataGrid,
} from '@mui/x-data-grid';
import {
Link as LinkIcon,
@@ -52,6 +55,7 @@ import {
OpenInNew as OpenInNewIcon,
CloudDownload as CloudDownloadIcon,
Description as DescriptionIcon,
Polymer as PolymerIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon,
Apps as AppsIcon,
@@ -59,6 +63,7 @@ import {
Cached as CachedIcon,
AccessibilityNew as AccessibilityNewIcon,
Lock as LockIcon,
Eco as EcoIcon,
Schedule as ScheduleIcon,
Cloud as CloudIcon,
Business as BusinessIcon,
@@ -66,6 +71,7 @@ import {
VisibilityOff as VisibilityOffIcon,
Clear as ClearIcon,
Add as AddIcon,
Remove as RemoveIcon,
Rocket as RocketIcon,
Webhook as WebhookIcon,
Air as AirIcon,
@@ -76,6 +82,8 @@ import {
FilterAlt as FilterAltIcon,
CompareArrows as CompareArrowsIcon,
Hub as HubIcon,
Key as KeyIcon,
FlashOn as FlashOnIcon,
} from "@mui/icons-material";
import { Context } from "../context/ContextApi.jsx";
@@ -133,6 +141,7 @@ const CacheView = memo((props) => {
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
const [renderTextBox, setRenderTextBox] = React.useState(false);
const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
const [datastoreCategoryGroups, setDatastoreCategoryGroups] = React.useState([]);
const [selectedCategory, setSelectedCategory] = React.useState("default");
const [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
@@ -163,6 +172,18 @@ const CacheView = memo((props) => {
var to_be_copied = "";
const defaultAutomation = [
{
"name": "Security Rules",
"description": "Describes security rules that are validated BEFORE an update occurs. This is in order for bad writes to be avoided. Control: allow, deny, merge, overwrite. Logic: if, or, and. Functions: same_shape, is_superset, has_deleted_field",
"options": [{
"key": "rule",
"value": "",
}],
"icon": <KeyIcon />,
"beta": true,
"disabled": false,
"enabled": false,
},
{
"name": "Run workflow",
"description": "Runs one or more workflows with the updated value as runtime argument",
@@ -200,14 +221,16 @@ const CacheView = memo((props) => {
{
"name": "Run AI Agent",
"description": "Runs an AI Agent to process the updated value. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
"description": "Runs an AI Agent to process the updated value. Controllable based on your 'action' input, where the full updated value is added on the end. All actions run in order, meaning it needs to finish the previous one before the next is ran. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
"type": "singul",
"options": [{
"key": "",
"key": "action-1",
"description": "Describe the action to perform on this item",
"value": "",
}],
"beta": true,
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
"disabled": false,
},
{
@@ -218,22 +241,10 @@ const CacheView = memo((props) => {
"key": "",
"value": "",
}],
"beta": true,
"icon": "/images/logos/singul.svg",
"disabled": false,
},
{
"name": "Send message",
"description": "",
"type": "singul",
"options": [{
"key": "app",
"value": "",
}],
"icon": <SendIcon />,
"disabled": true,
"enabled": false,
},
]
const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
@@ -317,8 +328,21 @@ const CacheView = memo((props) => {
var chosenCategory = selectedCategory
const urlParams = new URLSearchParams(window.location.search)
const categoryParam = urlParams.get("category")
var categoryParam = urlParams.get("category")
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
// In order to make linking weird urls from workflow page work.
if (urlParams.get("src") == "workflow") {
if (categoryParam?.toLowerCase().startsWith("list")) {
const newParam = categoryParam.substring(5).replaceAll("%20", "_")
// Replace the url param. There may be more params.
urlParams.set("category", newParam)
window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`)
categoryParam = newParam
}
}
chosenCategory = categoryParam
setSelectedCategory(categoryParam)
}
@@ -447,7 +471,31 @@ const CacheView = memo((props) => {
}
}
var foundstartwords = {}
var categorygroups = []
for (var key in newcategories) {
const category = newcategories[key]
if (!category.includes("_")) {
continue
}
const startword = category.split("_")[0]
if (startword.length <= 2) {
continue
}
if (!foundstartwords.hasOwnProperty(startword)) {
foundstartwords[startword] = 1
} else {
foundstartwords[startword] += 1
if (foundstartwords[startword] === 2) {
categorygroups.push(startword)
}
}
}
setDatastoreCategories(newcategories)
setDatastoreCategoryGroups(categorygroups)
}
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
@@ -463,6 +511,8 @@ const CacheView = memo((props) => {
//if (responseJson.category_config.automations[key].icon === undefined || responseJson.category_config.automations[key].icon === null || responseJson.category_config.automations[key].icon === "") {
const foundItem = defaultAutomation.find((automation) => automation.name === responseJson.category_config.automations[key].name)
if (foundItem) {
responseJson.category_config.automations[key].description = foundItem.description
responseJson.category_config.automations[key].beta = foundItem.beta
responseJson.category_config.automations[key].disabled = foundItem.disabled
responseJson.category_config.automations[key].icon = foundItem.icon
responseJson.category_config.automations[key].type = foundItem?.type
@@ -542,6 +592,7 @@ const CacheView = memo((props) => {
key: dataValue.key,
value: value,
category: selectedCategory,
tags: dataValue?.tags || [],
}
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
@@ -558,7 +609,9 @@ const CacheView = memo((props) => {
setCacheInput([entry]);
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
//entry = [entry]
const parsedUrl = `${globalUrl}/api/v2/datastore`
fetch(parsedUrl, {
method: "POST",
headers: {
@@ -780,6 +833,37 @@ const CacheView = memo((props) => {
Category: {dataValue.category}
</Typography>
: null}
{dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ?
<div style={{display: "flex", marginTop: 12, }}>
<Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}>
Tags
</Typography>
{dataValue.tags?.map((tag, index) => {
return (
<Chip
key={index}
label={tag}
style={{ marginRight: 5, }}
onDelete={() => {
var newTags = dataValue.tags.filter((t) => t !== tag)
if (newTags.length === 0) {
newTags = ["none"]
} else if (newTags.length > 1) {
newTags = newTags.filter((t) => t !== "none")
}
setDataValue({
...dataValue,
tags: newTags,
})
}}
/>
)
})}
</div>
: null}
</div>
: null}
@@ -1065,7 +1149,6 @@ const CacheView = memo((props) => {
saveAutomation(newAutomations)
}
console.log("AUTO: ", automation)
return (
<Tooltip title={
<Typography style={{margin: 10, }}>
@@ -1089,7 +1172,7 @@ const CacheView = memo((props) => {
style={{ marginRight: 10, marginTop: -10, }}
checked={automation.enabled}
// Check if automation options have a value
disabled={automation.name !== "Enrich" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
disabled={automation.name !== "Enrich" && automation.name != "Run AI Agent" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
onChange={(e) => {
e.stopPropagation()
e.preventDefault()
@@ -1140,6 +1223,19 @@ const CacheView = memo((props) => {
}}>
{automation.name}
</Typography>
{automation?.beta !== true ? null :
<Chip
label="Beta"
size="small"
style={{
marginLeft: 10,
height: 20,
fontSize: 12,
border: `1px solid ${green}`,
color: "rgba(255,255,255,0.7)",
}}
/>
}
</div>
{automation?.disabled !== true ?
@@ -1166,8 +1262,9 @@ const CacheView = memo((props) => {
: null}
</div>
{showOptions && (
updatedAutomation.options.map((option, optionIndex) => {
{showOptions &&
<div style={{ marginTop: 20, marginBottom: 20, }}>
{updatedAutomation.options.map((option, optionIndex) => {
if (option?.key === "datastore_categories") {
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length <= 1) {
return (
@@ -1446,6 +1543,87 @@ const CacheView = memo((props) => {
}}
/>
)
} else if (updatedAutomation?.name === "Run AI Agent") {
return (
<ButtonGroup style={{display: "flex", }}>
<TextField
key={optionIndex}
fullWidth
disabled={option?.disabled === true}
style={theme.palette.textFieldStyle}
InputProps={{
disableUnderline: true,
style: {
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
color: theme.palette.textFieldStyle.color,
height: 40,
},
}}
label={option.key}
defaultValue={option.value}
onBlur={(e) => {
if (e.target.value === "") {
updatedAutomation.enabled = false
} else {
updatedAutomation.enabled = true
}
updatedAutomation.options[optionIndex].value = e.target.value;
setUpdatedAutomation(updatedAutomation)
setUpdated(true)
}}
/>
{optionIndex === updatedAutomation.options.length - 1 ?
<Button color="secondary" variant="outlined" style={{
height: 40,
maxWidth: 40,
}}
disabled={option?.value?.length < 20}
onClick={() => {
var newindex = optionIndex + 2
if (updatedAutomation.options.length > 0) {
const lastOption = updatedAutomation.options[updatedAutomation.options.length - 1]
const lastIndex = parseInt(lastOption.key.split("-")[1])
if (!isNaN(lastIndex)) {
newindex = lastIndex + 1
}
}
updatedAutomation.options.push({
"key": `action-${newindex}`,
"description": "Describe the action to perform on this item",
"value": "",
})
setUpdatedAutomation(updatedAutomation)
setUpdate(Math.random())
}}>
<AddIcon/>
</Button>
:
<Tooltip title={option?.disabled !== true ? "Disable action (won't run until re-enabled)" : "Re-Enable action (runs on all new edits)"} placement="top">
<Button color="secondary" variant="outlined" style={{
height: 40,
maxWidth: 40,
}} onClick={() => {
if (option?.disabled === true) {
updatedAutomation.options[optionIndex].disabled = false
} else {
updatedAutomation.options[optionIndex].disabled = true
}
setUpdatedAutomation(updatedAutomation)
setUpdate(Math.random())
}}>
{option?.disabled !== true ? <RemoveIcon /> : <FlashOnIcon />}
</Button>
</Tooltip>
}
</ButtonGroup>
)
}
return (
@@ -1478,8 +1656,9 @@ const CacheView = memo((props) => {
}}
/>
)
})
)}
})}
</div>
}
</div>
</Tooltip>
)
@@ -1506,6 +1685,8 @@ const CacheView = memo((props) => {
)
}
const urlParams = new URLSearchParams(window?.location?.search)
const highlightedKey = urlParams.get("key") || ""
const columns = [
{
field: 'key',
@@ -1513,6 +1694,24 @@ const CacheView = memo((props) => {
width: 200,
filterable: true,
sortable: true,
renderCell: (props) => {
return (
<Typography
variant="body2"
style={{
maxWidth: 180,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
cursor: "default",
border: props.row.key === highlightedKey ? `2px solid ${theme.palette.primary.main}` : "none",
}}
>
{props.row.key}
</Typography>
)
}
},
{
width: 540,
@@ -1552,8 +1751,10 @@ const CacheView = memo((props) => {
backgroundColor: theme.palette.platformColor,
border: theme.palette.defaultBorder,
padding: 5,
maxWidth: 500,
minWidth: 500,
maxHeight: 500,
overflowX: "auto",
overflowY: "auto",
}}
collapsed={true}
@@ -1735,6 +1936,7 @@ const CacheView = memo((props) => {
"created": data.created,
"workflow_id": data.workflow_id,
"category": data.category,
"tags": data.tags,
})
setValue(newvalue)
setModalOpen(true)
@@ -1914,7 +2116,7 @@ const CacheView = memo((props) => {
},
];
var previousgroup = ""
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
return (
<div style={{
@@ -2067,56 +2269,53 @@ const CacheView = memo((props) => {
</Button>
</ButtonGroup>
<ButtonGroup style={{marginTop: 0, }}>
{datastoreCategories !== undefined &&
<ButtonGroup style={{ position: "absolute", top: -6, }}>
{datastoreCategories !== undefined &&
datastoreCategories !== null &&
datastoreCategories.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150, marginTop: 8, }}>
<InputLabel id="category-choice" style={{
color: "rgba(255, 255, 255, 0.65)",
}}>
Category
</InputLabel>
<Select
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
<Autocomplete
labelId="category-choice"
style={{
minWidth: 150,
maxWidth: 150,
height: 35,
borderRadius: "5px 0px 0px 5px",
minWidth: 250,
maxWidth: 250,
}}
ListboxProps={{
style: {
maxHeight: "70vh",
border: "1px solid rgba(255,255,255,0.3)",
}
}}
value={selectedCategory}
onChange={(event) => {
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
//if (selectAllChecked || listCache.length > 0) {
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(event.target.value)
return
}
setSelectedCategory(event.target.value)
if (event.target.value === "all" || event.target.value === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, event.target.value, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
}
options={datastoreCategories}
getOptionLabel={(data) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
return fixedname
}}
>
{datastoreCategories.map((data, index) => {
// Should find the icon for things
// Fix uppercase at start of words
groupBy={(data) => {
if (!data.includes("_")) {
return undefined
}
const firstword = data.split("_")[0]
if (datastoreCategoryGroups.includes(firstword)) {
return firstword.charAt(0).toUpperCase() + firstword.slice(1)
}
return undefined
}}
renderInput={(params) => {
return (
<TextField
{...params}
label="Select Category"
variant="outlined"
size="small"
/>
)
}}
renderOption={(props, data, state) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
@@ -2125,13 +2324,40 @@ const CacheView = memo((props) => {
return (
<MenuItem
key={index}
key={data}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(data)
return
}
setSelectedCategory(data)
if (data === "all" || data === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, data, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${data}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data}`)
}
}}
>
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
@@ -2143,9 +2369,69 @@ const CacheView = memo((props) => {
{fixedname}
</Typography>
</MenuItem>
)
}}
/>
{/*
{datastoreCategories.map((data, index) => {
var addition = ""
if (data.includes("_")) {
const firstword = data.split("_")[0]
if (firstword !== previousgroup) {
if (datastoreCategoryGroups.includes(firstword)) {
previousgroup = firstword
addition =
<ListSubheader>
{firstword.charAt(0).toUpperCase() + firstword.slice(1)}
</ListSubheader>
} else {
if (previousgroup !== "") {
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
previousgroup = ""
}
}
}
} else {
if (previousgroup !== "") {
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
previousgroup = ""
}
}
// Should find the icon for things
// Fix uppercase at start of words
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
"name": fixedname,
})
return (
<MenuItem
key={data}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
>
{addition}
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem>
);
})}
</Select>
*/}
</FormControl>
) : null}
@@ -2154,7 +2440,7 @@ const CacheView = memo((props) => {
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{
height: 35,
height: 40,
borderRadius: 4,
textTransform: 'none',
fontSize: 16,
@@ -2173,14 +2459,13 @@ const CacheView = memo((props) => {
</Button>
</Tooltip>
:
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
<Tooltip title={"Add new category object"} style={{}} aria-label={""}>
<Button
style={{
whiteSpace: "nowrap",
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
height: 35,
height: 40,
textTransform: 'none',
fontSize: 16,
borderRadius: "0px 5px 5px 0px",
}}
@@ -2211,14 +2496,14 @@ const CacheView = memo((props) => {
}}
style={{
height: 35,
height: 40,
width: 200,
marginTop: 0,
}}
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: 35,
height: 40,
fontSize: 16,
borderRadius: 4,
paddingTop: 0,
@@ -2245,7 +2530,7 @@ const CacheView = memo((props) => {
height: 35,
textTransform: 'none',
border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null,
border: isAutomating ? `1px solid ${green}` : null,
}}
variant="contained"
color="secondary"
@@ -2259,8 +2544,8 @@ const CacheView = memo((props) => {
setShowAutomationMenu(true)
}}
>
{isAutomating ? <RocketLaunchIcon style={{color: theme.palette.primary.main, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
Automate (beta)
{isAutomating ? <RocketLaunchIcon style={{color: green, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
Automate
</Button>
</span>
</Tooltip>
@@ -2403,7 +2688,7 @@ const CacheView = memo((props) => {
Subscribing
</Typography>
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet.
Enabling subscriptions allows other organizations to subscribe to this category and receive updates when keys are added or modified. Makes the category searchable.
</Typography>
</div>
</div>
+2 -2
View File
@@ -223,14 +223,14 @@ const DiscordChat = props => {
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
</span> */}
</div>
);
};
+2 -2
View File
@@ -349,14 +349,14 @@ const DocsGrid = props => {
: null
}
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}>
Search by
</Typography>
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a>
</span>
</span> */}
</div>
</div>
)
+4 -2
View File
@@ -432,7 +432,7 @@ const EnvironmentTab = memo((props) => {
-e SHUFFLE_SWARM_CONFIG=run \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \ \n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
-e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \\\n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
-v /tmp:/tmp \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest
`)
@@ -1225,7 +1225,9 @@ const EnvironmentTab = memo((props) => {
:
(environment.running_ip === undefined ||
environment.running_ip === null ||
environment.running_ip.length === 0)
environment.running_ip.length === 0) ||
environment?.checkin === 0 ||
Date.now() / 1000 - environment?.checkin > 180
?
<Chip
key={index}
+7 -3
View File
@@ -110,7 +110,11 @@ const [filesLoaded, setFilesLoaded] = useState(false);
return (
<Tooltip
title={params.row.filename}
title={<Typography variant="b1">
{params?.row?.filename}<br/>
{params?.row?.tags}
</Typography>
}
placement="left"
arrow
>
@@ -1580,8 +1584,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
columns={columns}
checkboxSelection
disableRowSelectionOnClick
selectionModel={selectedRows}
onSelectionModelChange={(newSelection) => {
rowSelectionModel={selectedRows}
onRowSelectionModelChange={(newSelection) => {
setSelectedRows(newSelection);
}}
sx={{
+51 -78
View File
@@ -2,14 +2,13 @@ import React, { useEffect, useRef, useState, useContext, useCallback, useMemo, m
import {
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
GridView as GridViewIcon,
ShieldOutlined as ShieldOutlinedIcon,
Add as AddIcon,
BorderColor,
Close as CloseIcon,
ConstructionOutlined as ConstructionOutlinedIcon,
Toc as TocIcon,
Settings as SettingsIcon
Settings as SettingsIcon,
Business as BusinessIcon,
HelpOutline as HelpOutlineIcon,
MeetingRoom as MeetingRoomIcon,
Search as SearchIcon,
Add as AddIcon
} from "@mui/icons-material";
import LightModeIcon from '@mui/icons-material/LightMode';
import DarkModeIcon from '@mui/icons-material/DarkMode';
@@ -44,23 +43,17 @@ import { useNavigate } from "react-router";
import { getTheme } from "../theme.jsx";
import { Link } from "react-router-dom";
import {
Business as BusinessIcon,
Notifications as NotificationsIcon,
HelpOutline as HelpOutlineIcon,
MeetingRoom as MeetingRoomIcon,
Lightbulb as LightbulbIcon,
Search as SearchIcon
} from "@mui/icons-material";
import { toast } from "react-toastify";
import { Context } from "../context/ContextApi.jsx";
import Licensed from "./Licensed.jsx";
const ShuffleLogo = "/images/Shuffle_logo.png";
const detectionIcon = "/icons/detection.svg";
const documentationIcon = "/icons/documentation.svg";
const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
const navigate = useNavigate();
@@ -206,7 +199,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
setOpenAutocomplete(false)
}}
>
Add suborg
<Button variant="outlined" startIcon={<AddIcon />} sx={{
textTransform: 'none',
}}>
Add suborg
</Button>
</Box>
</Link>
</Popper>
@@ -1926,72 +1923,48 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
style={{
display: "flex",
flexDirection: "column",
padding: 10,
marginLeft: 5,
padding: "0 8px",
marginBottom: 8,
}}
>
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && !isProdStatusOn &&
<div style={{display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", }}>
<Button
variant="outlined"
style={{marginBottom: 15, borderWidth: 2, }}
onClick={() => {
navigate("/admin?admin_tab=billingstats&ref=left_sidebar_upgrade")
if (isCloud) {
window.open("https://shuffler.io/contact?category=book_a_demo&ref=cloud", "_blank")
} else {
window.open("https://shuffler.io/contact?category=book_a_demo&ref=onprem", "_blank")
}
}}
>
Book a Demo
</Button>
</div>
}
{!isCloud && activeOrgData?.old_org? (
<div
style={{
display: "flex",
alignItems: "center",
gap: 20,
padding: "4px 10px",
marginLeft: "5px",
marginRight: "5px",
borderRadius: 20,
marginBottom: "14px",
background: isProdStatusOn
? "rgba(43, 192, 126, 0.1)"
: "rgba(255, 82, 82, 0.1)",
cursor: "pointer",
}}
onClick={() => {
navigate("/admin?admin_tab=billingstats")
}}
>
<span
<>
{userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && !isProdStatusOn && (
<Button
variant="outlined"
color="primary"
fullWidth
style={{
width: 8,
height: 8,
marginLeft: 10,
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
borderRadius: 999,
display: expandLeftNav ? "inline" : "none",
marginBottom: 30,
marginRight: 8,
height: 40,
borderWidth: 1.5,
borderRadius: 10,
fontSize: 13,
fontWeight: 600,
textTransform: 'none',
width: "95%" ,
display: expandLeftNav ? 'flex' : 'none',
}}
/>
<Typography
style={{
fontFamily: "12px",
opacity: 0.9,
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
}}
>
{expandLeftNav ? isProdStatusOn ? "Production" : "NOT production" : isProdStatusOn ? "ON" : "OFF"}
</Typography>
</div>
) : null}
onClick={() => {
if (isCloud) {
window.open("https://shuffler.io/contact?category=book_a_demo&ref=cloud", "_blank")
} else {
window.open("https://shuffler.io/contact?category=book_a_demo&ref=onprem", "_blank")
}
}}
>
Book a Demo
</Button>
)}
{!isCloud && (activeOrgData?.old_org || isProdStatusOn) && (
<Licensed
expanded={expandLeftNav}
licensed={isProdStatusOn}
/>
)}
</>
<Box ref={autocompleteRef}>
<Autocomplete
@@ -2317,4 +2290,4 @@ const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, s
</Dialog>
)
)
});
});
+8 -2
View File
@@ -1319,8 +1319,14 @@ const LicencePopup = (props) => {
>
{`${
isPaidPlan ? "Next billing: " : "App runs resets on "
}${new Date(
(localSub.enddate || localSub.Enddate) * 1000
}${(
isPaidPlan
? new Date((localSub.enddate || localSub.Enddate) * 1000)
: new Date(
new Date().getFullYear(),
new Date().getMonth() + 1,
1
)
).toLocaleDateString(undefined, {
day: "2-digit",
month: "short",
+1 -1
View File
@@ -183,7 +183,7 @@ const menuData = {
title: "Articles",
icon: "/images/icons/articles.svg",
hoverIcon: "/images/icons/articles_hover.svg",
link: "/articles/2.0_release",
link: "/articles",
gaData: {
category: "navbar",
action: "resources_click",
+1 -1
View File
@@ -535,7 +535,7 @@ const AuthenticationOauth2 = (props) => {
}
}
toast("Authentication successful!")
toast.info("Authentication window closed")
// This is more a guess than anything
// Should be handled in getAppAuthentication()
@@ -343,7 +343,7 @@ console.log("defatult in handleEditOrg", defaults)
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
const data = {
org_id: selectedOrganization?.id,
sso_test: true,
sso: true,
};
fetch(url, {
+2 -5
View File
@@ -62,11 +62,8 @@ const OrganizationTab = (props) => {
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO'));
} else {
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
setVisibleTabs(items);
} else {
setVisibleTabs(items.filter((item) => item !== 'SSO'));
}
// Show SSO tab for all users now
setVisibleTabs(items);
}
//if (isCloud) {
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -13,9 +13,9 @@ const PartnerApps = () => {
minHeight: "500px",
}}
>
<Typography>Partner Apps</Typography>
<Typography>Partner's Apps</Typography>
</Box>
)
}
export default PartnerApps
export default PartnerApps
File diff suppressed because one or more lines are too long
+5 -7
View File
@@ -126,9 +126,9 @@ const PartnerTab = (props) => {
case `apps`:
return <PartnersApps isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'articles' :
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case `ai_agents`:
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
return <PartnersUsecasesTab isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case `aiagents`:
return <PartnerArticles tabName="AI Agents, Coming Soon.." isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'branding':
return <Branding
isCloud={isCloud}
@@ -157,8 +157,7 @@ const PartnerTab = (props) => {
!isCloud &&
(tabName === "Usecases" ||
tabName === "Apps" ||
tabName === "AI Agents" ||
tabName === "Articles")
tabName === "AI Agents")
) {
return true;
}
@@ -166,7 +165,6 @@ const PartnerTab = (props) => {
// Disable Apps, Articles, and AI Agents for all non-support users
if (
tabName === "Apps" ||
tabName === "Articles" ||
tabName === "AI Agents"
) {
return true;
@@ -196,7 +194,7 @@ const PartnerTab = (props) => {
}
const isSupportOnlyTab = (tabName) => {
return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents";
return tabName === "Apps" || tabName === "AI Agents";
}
return (
+4 -3
View File
@@ -1,7 +1,8 @@
import { Box, Typography } from '@mui/material'
import React from 'react'
const PartnersArticles = () => {
const PartnersArticles = (props) => {
const {tabName = "Partner's Articles"} = props;
return (
<Box
sx={{
@@ -13,9 +14,9 @@ const PartnersArticles = () => {
minHeight: "500px",
}}
>
<Typography>Partner Articles</Typography>
<Typography>{tabName}</Typography>
</Box>
)
}
export default PartnersArticles
export default PartnersArticles
+658 -32
View File
@@ -24,6 +24,7 @@ import {
import React, { useContext, useEffect, useState } from "react";
import { getTheme } from "../theme.jsx";
import { Context } from "../context/ContextApi.jsx";
import { triggers } from "../views/AngularWorkflow.jsx";
import AddIcon from "@mui/icons-material/Add";
import CloseIcon from "@mui/icons-material/Close";
import { toast } from "react-toastify";
@@ -37,6 +38,35 @@ import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import InfoIcon from "@mui/icons-material/Info";
import { Link, useNavigate } from "react-router-dom";
import { grey } from "../views/AngularWorkflow.jsx"
const IMAGE_UPLOAD_PLACEHOLDER = "Uploading image...";
// Helper to extract the first markdown image URL from navigation content
export const getFirstImageFromNavigation = (navigation) => {
if (!navigation || !Array.isArray(navigation.items)) {
return "";
}
for (const item of navigation.items) {
if (!item || !Array.isArray(item.content)) {
continue;
}
for (const paragraph of item.content) {
if (typeof paragraph !== "string") {
continue;
}
const match = paragraph.match(/!\[[^\]]*]\(([^)]+)\)/);
if (match && match[1]) {
return match[1];
}
}
}
return "";
};
// Helper function to get the correct image path based on app category
export const getCategoryImagePath = (category) => {
@@ -62,16 +92,132 @@ export const getCategoryImagePath = (category) => {
return "/images/appCategories/intel.svg";
} else if (lowerCategory.includes("email")) {
return "/images/appCategories/email.svg";
} else if (lowerCategory.includes("webhook")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "webhook")?.large_image;
} else if (lowerCategory.includes("schedule")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "schedule")?.large_image;
} else if (lowerCategory.includes("pipelines")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "pipelines")?.large_image;
} else if (lowerCategory.includes("shuffle workflow")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "shuffle workflow")?.large_image;
} else if (lowerCategory.includes("user input")) {
return triggers.find((trigger) => trigger?.name.toLowerCase() === "user input")?.large_image;
} else {
return "/images/appCategories/other.svg";
}
};
// Skeleton component for loading state
const UsecaseCardSkeleton = () => {
const UsecaseCardSkeleton = ({ isArticlesTab }) => {
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
// Skeleton for Articles Tab
if (isArticlesTab) {
return (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
width: {
xs: "230px",
sm: "320px",
},
minWidth: {
xs: "230px",
sm: "320px",
},
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
{/* Header image skeleton */}
<Skeleton
variant="rectangular"
height={160}
sx={{
width: "100%",
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
{/* Body skeleton */}
<Box
sx={{
p: 2,
pt: 2.5,
display: "flex",
flexDirection: "column",
gap: 1.5,
}}
>
<Skeleton
variant="text"
width="80%"
height={26}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.08)",
}}
/>
<Skeleton
variant="text"
width="40%"
height={20}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
<Box
sx={{
mt: "auto",
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
gap: 1,
}}
>
<Skeleton
variant="rectangular"
width={38}
height={22}
sx={{
borderRadius: "12px",
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
<Skeleton
variant="circular"
width={24}
height={24}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
</Box>
</Box>
</Box>
);
}
// Skeleton for Usecases Tab
return (
<Box
sx={{
@@ -177,7 +323,8 @@ const UsecaseCardSkeleton = () => {
);
};
const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase }) => {
// Card renderer for both usecases and articles
const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase, isArticlesTab, partnerData }) => {
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const { themeMode } = useContext(Context);
@@ -209,9 +356,215 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
navigate(`/usecases/${usecase.id}`);
};
// Article Card Layout
if (isArticlesTab) {
return (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
width: {
xs: "230px",
sm: "320px",
},
minWidth: {
xs: "230px",
sm: "320px",
},
overflow: "hidden",
display: "flex",
flexDirection: "column",
"&:hover": {
backgroundColor: theme.palette.usecaseCardHoverColor,
},
}}
>
<Box
component="img"
src={usecase.headerImage || "/images/no_image.png"}
alt={usecase.name}
sx={{
width: "100%",
height: 160,
objectFit: "cover",
}}
/>
<Box
sx={{
p: 2,
pt: 2.5,
display: "flex",
flexDirection: "column",
gap: 1.5,
height: "100%",
}}
>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: {
xs: "15px",
lg: "16px",
},
fontWeight: 600,
fontFamily: theme.typography.fontFamily,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{usecase.name}
</Typography>
{usecase.created && (
<Typography
sx={{
color: theme.palette.text.secondary,
fontSize: "13px",
}}
>
{new Date(usecase.created * 1000).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</Typography>
)}
<Box
sx={{
mt: -1,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 0.5,
}}
onClick={(e) => e.stopPropagation()}
>
<OpenInNewIcon
fontSize="small"
onClick={(e) => {
e.stopPropagation();
window.open(`${window.location.origin}/partners/${partnerData?.name.toLowerCase().replaceAll(" ", "_")}/articles/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
}}
sx={{
color: theme.palette.primary.main,
cursor: "pointer",
}}
/>
<Switch
checked={usecase.public}
onClick={(e) => {
e.stopPropagation();
}}
onChange={(e) => {
e.preventDefault();
e.stopPropagation();
handleToggle(usecase.id);
}}
size="medium"
sx={{
"& .MuiSwitch-switchBase.Mui-checked": {
color: "#4CAF50",
},
"& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": {
backgroundColor: "#4CAF50",
},
}}
/>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleClick(e);
}}
size="small"
sx={{
color: theme.palette.text.primary,
"&:hover": {
backgroundColor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.05)",
},
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</Box>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onClick={(e) => e.stopPropagation()}
PaperProps={{
sx: {
backgroundColor: theme.palette.DialogStyle.backgroundColor,
border: theme.palette.defaultBorder,
borderRadius: "8px",
boxShadow: theme.palette.DialogStyle.boxShadow,
"& .MuiMenuItem-root": {
fontSize: "14px",
color: theme.palette.text.primary,
"&:hover": {
backgroundColor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.05)",
},
},
},
}}
transformOrigin={{ horizontal: "right", vertical: "top" }}
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleOpenDialog(usecase);
setAnchorEl(null);
}}
>
<ListItemIcon>
<EditIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Edit Article</ListItemText>
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDeleteUsecase(usecase.id);
}}
>
<ListItemIcon>
<DeleteIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Delete Article</ListItemText>
</MenuItem>
</Menu>
</Box>
</Box>
);
}
// Usecase Card Layout
return (
<Box
onClick={handleCardClick}
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
@@ -241,7 +594,6 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
sm: "300px",
},
gap: "20px",
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.usecaseCardHoverColor,
},
@@ -302,6 +654,17 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
</Tooltip>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<OpenInNewIcon
fontSize="small"
onClick={(e) => {
e.stopPropagation();
window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
}}
sx={{
color: theme.palette.primary.main,
cursor: "pointer",
}}
/>
<Switch
checked={usecase.public}
onClick={(e) => {
@@ -414,9 +777,11 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
);
};
const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData }) => {
const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData, selectedTab }) => {
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
const isArticlesTab = selectedTab?.toLowerCase() === "articles";
const entityLabel = isArticlesTab ? "Article" : "Usecase";
// Dialog state
const [openDialog, setOpenDialog] = useState(false);
@@ -438,12 +803,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
navigation: {
items: [
{
name: "About Usecase",
name: `About ${entityLabel}`,
content: [""],
},
],
},
public: false,
created: null,
});
// App categories for dropdowns
@@ -458,6 +824,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
{ value: "assets", label: "Assets" },
{ value: "intel", label: "Intel" },
{ value: "email", label: "Email" },
{value: "webhook", label: "Webhook"},
{value: "schedule", label: "Schedule"},
{value: "pipelines", label: "Pipelines"},
{value: "shuffle workflow", label: "Shuffle Workflow"},
{value: "user input", label: "User Input"},
{ value: "other", label: "Other" }
];
@@ -505,7 +876,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
description: usecase.mainContent?.description,
navigation: usecase.navigation || { items: [] },
categories: usecase.mainContent?.categories || [],
public: usecase?.public || false,
public: usecase?.public || false,
created: usecase?.created || null,
headerImage: getFirstImageFromNavigation(usecase.navigation),
}));
if (usecases.length > 0) {
setUsecaseData(usecases);
@@ -526,7 +899,195 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
// Sample categories
const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"];
// Uploading image to the public bucket
const handleImageUpload = async (imageData) => {
const folderName = "usecase_images";
const usecaseId = formData?.id || partnerData?.id || "usecase";
try {
const response = await fetch(`${globalUrl}/api/v1/image_upload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify({
imageData,
folder: folderName,
id: usecaseId,
}),
});
// if (response.status !== 200) {
// toast.error("Failed to upload image" + response?.reason);
// return null;
// }
const responseJson = await response.json();
if (!responseJson?.success || !responseJson?.url) {
toast.error("Failed to upload image: " + responseJson?.reason);
return null;
}
return responseJson.url;
} catch (error) {
toast.error("Failed to upload image" + error?.message);
return null;
}
};
// This function is used to update the content with the public image url
const updateContentWithImageUrl = (itemIndex, contentIndex, imageUrl) => {
setFormData((prevData) => {
const newItems = [...(prevData.navigation.items || [])];
const item = newItems[itemIndex];
if (!item) {
return prevData;
}
const contents = [...(item.content || [])];
const existingContent = contents[contentIndex] || "";
const markdownImage = imageUrl ? `![image](${imageUrl})` : "";
const placeholderIndex = existingContent.indexOf(IMAGE_UPLOAD_PLACEHOLDER);
if (placeholderIndex !== -1) {
const before = existingContent.slice(0, placeholderIndex);
const after = existingContent.slice(
placeholderIndex + IMAGE_UPLOAD_PLACEHOLDER.length,
);
const trimmedBefore = before.replace(/\s*$/, "");
const needsNewline =
trimmedBefore.length > 0 && !trimmedBefore.endsWith("\n");
const separator = needsNewline ? "\n" : "";
contents[contentIndex] = `${trimmedBefore}${separator}${markdownImage}${after}`;
} else {
const needsNewline =
existingContent.length > 0 && !existingContent.endsWith("\n");
const separator = needsNewline ? "\n" : "";
contents[contentIndex] = `${existingContent}${separator}${markdownImage}`;
}
newItems[itemIndex] = {
...item,
content: contents,
};
return {
...prevData,
navigation: {
items: newItems,
},
};
});
};
// This function is used to insert the image placeholder (Uploading image...) in the content to show that the image is being uploaded
const insertImagePlaceholder = (itemIndex, contentIndex) => {
setFormData((prevData) => {
const newItems = [...(prevData.navigation.items || [])];
const item = newItems[itemIndex];
if (!item) {
return prevData;
}
const contents = [...(item.content || [])];
const existingContent = contents[contentIndex] || "";
const needsNewline =
existingContent.length > 0 && !existingContent.endsWith("\n");
const separator = needsNewline ? "\n" : "";
contents[contentIndex] = `${existingContent}${separator}${IMAGE_UPLOAD_PLACEHOLDER}`;
newItems[itemIndex] = {
...item,
content: contents,
};
return {
...prevData,
navigation: {
items: newItems,
},
};
});
};
// This function is used to process the image file and upload it to the public bucket
const processImageFile = (imageFile, itemIndex, contentIndex) => {
const reader = new FileReader();
reader.onload = async (loadEvent) => {
const imageData = loadEvent.target?.result;
if (!imageData) {
toast.error("Failed to read image");
updateContentWithImageUrl(itemIndex, contentIndex, "");
return;
}
const imageUrl = await handleImageUpload(imageData);
if (!imageUrl) {
// Clean up the placeholder if upload failed
updateContentWithImageUrl(itemIndex, contentIndex, "");
return;
}
updateContentWithImageUrl(itemIndex, contentIndex, imageUrl);
};
reader.readAsDataURL(imageFile);
};
// This function is used to handle the image paste event
const handleContentPaste = async (event, itemIndex, contentIndex) => {
const items = event.clipboardData?.items;
if (!items || items.length === 0) {
return;
}
let imageFile = null;
for (let i = 0; i < items.length; i += 1) {
const item = items[i];
if (item.kind === "file" && item.type.startsWith("image/")) {
imageFile = item.getAsFile();
break;
}
}
if (!imageFile) {
return;
}
event.preventDefault();
insertImagePlaceholder(itemIndex, contentIndex);
processImageFile(imageFile, itemIndex, contentIndex);
};
// This function is used to handle the image drop event
const handleContentDrop = async (event, itemIndex, contentIndex) => {
event.preventDefault();
const files = event.dataTransfer?.files;
if (!files || files.length === 0) {
return;
}
const imageFile = Array.from(files).find((file) => file.type.startsWith("image/"));
if (!imageFile) {
toast.error("Failed to upload image, only images are supported");
return;
}
insertImagePlaceholder(itemIndex, contentIndex);
processImageFile(imageFile, itemIndex, contentIndex);
};
const getUserProfileWorkflows = (orgId) => {
if (selectedTab === "articles") {
return;
}
setIsWorkflowLoading(true);
fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
method: "GET",
@@ -713,12 +1274,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
navigation: usecase?.navigation || {
items: [
{
name: "About Usecase",
name: `About ${entityLabel}`,
content: [""],
},
],
},
public: usecase?.public || false,
created: usecase?.created || Math.floor(Date.now() / 1000),
});
setOpenDialog(true);
};
@@ -753,15 +1315,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
const validationErrors = [];
// Check for public workflow selection
if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") {
if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) {
validationErrors.push("Please select a public workflow");
}
if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") {
if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) {
validationErrors.push("Source app type is required");
}
if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") {
if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) {
validationErrors.push("Destination app type is required");
}
@@ -783,7 +1345,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}
// Check categories
if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) {
if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) {
validationErrors.push("At least one category must be selected");
}
@@ -846,7 +1408,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
return res.json();
})
.then((responseData) => {
console.log("Response data:", responseData);
// Reset loading state
setIsSubmitting(false);
@@ -863,7 +1424,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
description: formData.mainContent?.description.trim(),
categories: formData.mainContent?.categories || [],
navigation: formData.navigation || {},
public: formData?.public || false,
public: formData?.public || false,
created: formData?.created || null,
headerImage: getFirstImageFromNavigation(formData.navigation),
}
setUsecaseData((prevData) => {
@@ -921,6 +1484,28 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
});
};
// This is used to filter the usecases based on the selected tab
const filteredUsecases = Array.isArray(usecaseData)
? usecaseData.filter((usecase) => {
const hasWorkflowId =
!!usecase.publicWorkflowId &&
usecase.publicWorkflowId.toString().trim() !== "";
if (selectedTab === "articles") {
// Articles: only items without a public workflow ID
return !hasWorkflowId;
}
if (selectedTab === "usecases") {
// Usecases: only items with a public workflow ID
return hasWorkflowId;
}
// Fallback: show all
return true;
})
: [];
return (
<Box
sx={{
@@ -934,7 +1519,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
justifyContent: "flex-start",
}}
>
{/* Add Usecase Button */}
{/* Add Usecase / Article Button */}
<Box
sx={{
width: "100%",
@@ -946,13 +1531,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
<Typography sx={{ fontSize: 20, fontWeight: 500, color: "#FFFFFF" }}>
Usecases
{selectedTab === "usecases" && "Usecases"}
{selectedTab === "articles" && "Articles"}
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
disabled={isWorkflowLoading}
onClick={handleOpenDialog}
sx={{
px: 3,
@@ -961,11 +1546,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
fontWeight: 600,
}}
>
Add Usecase
{isArticlesTab ? "Add Article" : "Add Usecase"}
</Button>
</Box>
{/* Usecases Grid */}
{/* Usecases / Articles Grid */}
{isLoading ? (
// Skeleton loading state
<Box
@@ -981,11 +1566,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
>
{/* Display 4 skeleton cards while loading */}
{[...Array(4)].map((_, index) => (
<UsecaseCardSkeleton key={index} />
<UsecaseCardSkeleton key={index} isArticlesTab={isArticlesTab} partnerData={partnerData} />
))}
</Box>
) : usecaseData.length > 0 ? (
// Actual data display
) : filteredUsecases.length > 0 ? (
// Actual data display (filtered based on public workflow ID)
<Box
sx={{
display: "flex",
@@ -997,13 +1582,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
pt: 4,
}}
>
{usecaseData.map((usecase) => (
{filteredUsecases.map((usecase) => (
<UsecaseCard
handleOpenDialog={handleOpenDialog}
key={usecase.id}
usecase={usecase}
handleToggle={handleToggle}
handleDeleteUsecase={handleDeleteUsecase}
isArticlesTab={isArticlesTab}
partnerData={partnerData}
/>
))}
</Box>
@@ -1027,7 +1614,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
color: "#FFFFFF",
}}
>
No usecases found
{isArticlesTab ? "No articles found" : "No usecases found"}
</Typography>
</Box>
)}
@@ -1064,7 +1651,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
<Typography sx={{ fontSize: 20, fontWeight: 500, color: theme.palette.text.primary }}>
{formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"}
{formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"}
</Typography>
<IconButton
onClick={handleCloseDialog}
@@ -1088,6 +1675,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
paddingTop: 3,
}}
>
{!isArticlesTab && (
<FormControl>
<Box
sx={{
@@ -1207,9 +1795,40 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
)}
</Select>
</FormControl>
)}
{/* Other data selection (use all data types) */}
{!isArticlesTab && (
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
<FormControl fullWidth>
<Typography
sx={{
color: grey,
mb: 1,
fontSize: "16px",
}}
>
<b>Coming soon:</b> Files (detection)
</Typography>
</FormControl>
<FormControl fullWidth>
<Typography
sx={{
color: grey,
mb: 1,
fontSize: "16px",
}}
>
<b>Coming soon:</b> Datastore category (threatlists)
</Typography>
</FormControl>
</Box>
)}
{/* App Type Selection */}
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
{!isArticlesTab && (
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: isArticlesTab ? 0 : 2 }}>
<FormControl fullWidth>
<Typography
sx={{
@@ -1286,6 +1905,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
</Select>
</FormControl>
<FormControl fullWidth>
<Typography
sx={{
@@ -1362,6 +1982,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
</Select>
</FormControl>
</Box>
)}
<Box sx={{ mb: 2, mt: 2 }}>
<Typography
@@ -1433,6 +2054,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
/>
</FormControl>
{!isArticlesTab && (
<FormControl>
<Typography
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
@@ -1513,6 +2135,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
))}
</Select>
</FormControl>
)}
</Box>
</Box>
@@ -1648,7 +2271,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
<Box key={contentIndex} sx={{ mb: 1 }}>
<TextField
multiline
rows={6}
rows={14}
value={content}
onChange={(e) => {
const newItems = [...formData.navigation.items];
@@ -1659,12 +2282,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
navigation: { items: newItems },
});
}}
onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)}
onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)}
onDragOver={(event) => event.preventDefault()}
placeholder="Content (Markdown supported)"
helperText={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
<Typography variant="caption" sx={{ opacity: 0.7 }}>
Markdown syntax is supported
Markdown syntax is supported and Use ### for subItem for Table of Contents. Drag and drop or paste images to the content to upload them.
</Typography>
</Box>
}
@@ -1708,7 +2334,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
)}
</Box>
))}
{/*
<Button
startIcon={<AddCircleOutlineIcon />}
onClick={() => {
@@ -1725,7 +2351,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
Add Paragraph
</Button>
</Button> */}
</Box>
))}
@@ -1791,7 +2417,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
>
{isSubmitting
? (formData?.id ? "Updating..." : "Adding...")
: (formData?.id ? "Update Usecase" : "Add Usecase")
: (formData?.id ? `Update ${entityLabel}` : `Add ${entityLabel}`)
}
</Button>
</DialogActions>
+3 -1
View File
@@ -168,11 +168,13 @@ const RunDetectionTest = (props) => {
return foundCorrect
}
console.log("DETECTION CHECK: ", haveDetectionPipelines().length, ticketWebhook, detectionWorkflowId, detectionTestRunning)
return (
<div style={{display: "flex", }}>
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
<Tooltip title={
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCP Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
} style={{}} aria-label={"Run detection test"}>
<div>
<Button
+258 -56
View File
@@ -1,7 +1,25 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Typography, ToggleButton, ToggleButtonGroup, Select, MenuItem, FormControl, InputLabel, Box } from '@mui/material';
import { makeStyles, } from "@mui/styles";
import {
Typography,
ToggleButton,
ToggleButtonGroup,
Select,
MenuItem,
FormControl,
InputLabel,
Box,
Autocomplete,
TextField,
Tooltip,
IconButton,
} from '@mui/material';
import { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz';
import theme from '../theme.jsx';
import {green, yellow, red} from "../views/AngularWorkflow.jsx"
import {
OpenInNew as OpenInNewIcon,
} from '@mui/icons-material';
// Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M)
function formatCompactNumber(value) {
@@ -59,34 +77,128 @@ function computeTodayValueForOrg(key, orgStats) {
const RunsOverTimeWidget = (props) => {
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
const [selectedStatType, setSelectedStatType] = useState({
"value": 'total_workflow_executions',
"label": 'Workflows',
"amount": 0,
})
const [series, setSeries] = useState([]);
const [days, setDays] = useState(365); // aggregate to last 12 months by default
const [selectedMonth, setSelectedMonth] = useState(null); // Date representing first day of target month, or null for yearly view
const [statTypeOptions, setStatTypeOptions] = useState([])
const [loading, setLoading] = useState(false);
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#FF8544 !important",
},
root: {
"& .MuiAutocomplete-listbox": {
border: "2px solid #FF8544",
color: theme.palette.text.primary,
fontSize: 18,
"& li:nth-child(even)": {
backgroundColor: "#CCC",
},
"& li:nth-child(odd)": {
backgroundColor: "#FFF",
},
},
},
inputRoot: {
color: theme.palette.text.primary,
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "#f86a3e",
},
},
});
const classes = useStyles();
// Helper: fetch time series for a specific statistics key
const fetchSeriesForKey = async (key) => {
if (statTypeOptions?.length < 3) {
var foundkeys = {}
for (var key in orgStats) {
if (!key.startsWith("total_")) {
continue
}
// Remove total
foundkeys[key] = orgStats[key]
}
for (var foundKey in orgStats?.daily_statistics) {
const dailyStats = orgStats?.daily_statistics[foundKey]
for (var additionKey in dailyStats?.additions) {
const addition = dailyStats?.additions[additionKey];
if (foundkeys[addition?.key] === undefined) {
foundkeys[addition?.key] = addition?.value
} else {
foundkeys[addition?.key] += addition?.value
}
}
}
var newarray = []
for (var key in foundkeys) {
const foundkey = key
var parsedname = key
if (parsedname?.startsWith("total_")) {
parsedname = parsedname.replace("total_", "")
}
if (parsedname?.startsWith("categorylabel")) {
parsedname = parsedname.replace("categorylabel", "")
}
parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ")
newarray.push({
"value": key,
"label": parsedname,
"amount": foundkeys[key],
})
}
if (newarray.length > 0) {
setStatTypeOptions(newarray)
}
}
try {
// If specific org is selected and pre-fetched stats are available, use them directly
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
const dailyStats = orgStats.daily_statistics;
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
if (key.startsWith("total_")) {
key = key.replace("total_", "")
}
for (const day of dailyStats) {
if (!day?.date) continue;
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue;
let value = 0;
if (key === 'workflow_executions') {
const finished = Number(day?.workflow_executions_finished || 0);
const failed = Number(day?.workflow_executions_failed || 0);
value = finished + failed;
} else {
value = Number(day?.[key] || 0);
}
if (day[key] === undefined) {
for (var additionKey in day?.additions) {
const addition = day?.additions[additionKey];
if (addition?.key === key) {
value += Number(addition?.value || 0);
break
}
}
} else {
value = Number(day[key] || 0);
}
processedEntries.push({ date: day.date, value });
}
@@ -185,7 +297,7 @@ function computeTodayValueForOrg(key, orgStats) {
};
// Load and transform into monthly aggregation for last 12 months
const load = async (curMode) => {
const loadStats = async (curMode, inputType) => {
setLoading(true);
try {
// Clear current series immediately to avoid any visual overlap while switching views
@@ -203,8 +315,10 @@ function computeTodayValueForOrg(key, orgStats) {
setSeries(dummy);
return;
}
const key = curMode === 'apps' ? 'app_executions' : 'workflow_executions';
const key = inputType !== undefined ? inputType : selectedStatType?.value?.length > 0 ? selectedStatType?.value : curMode === 'apps' ? 'app_executions' : 'workflow_executions';
const entries = await fetchSeriesForKey(key);
// Normalize variants: {Date, Value} or {date, value}
const normalized = (entries || []).map((d) => ({
date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()),
@@ -272,6 +386,23 @@ function computeTodayValueForOrg(key, orgStats) {
}
};
useEffect(() => {
setTimeout(() => {
const starterWidgetStatType = localStorage.getItem("runsOverTimeWidgetStatType")
if (starterWidgetStatType) {
setSelectedStatType({
"value": starterWidgetStatType,
"label": (starterWidgetStatType.charAt(0).toUpperCase() + starterWidgetStatType.substring(1)).replaceAll("_", " "),
"amount": 0,
})
loadStats(mode, starterWidgetStatType)
}
}, 1500)
}, [])
// Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect
useEffect(() => {
if (monthOverride instanceof Date) {
@@ -287,8 +418,9 @@ function computeTodayValueForOrg(key, orgStats) {
setSeries([]);
return;
}
load(mode);
}, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
loadStats(mode);
}, [selectedStatType, mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
// Notify parent on loading changes
useEffect(() => {
@@ -341,55 +473,125 @@ function computeTodayValueForOrg(key, orgStats) {
}
/>;
const barColorscheme = [
"#f85a3e", // anchor orange
"#ff7a57", // brighter, more playful
"#e14b2e", // slightly darker + redder
"#ff9b6b", // soft peachy highlight
"#c83f24", // deep burnt orange
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>Runs over time ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>{selectedStatType?.label} ({selectedStatType?.amount})</Typography>
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'center' }}>
<ToggleButtonGroup
exclusive
size="large"
value={mode}
onChange={(e, v) => v && setMode(v)}
sx={{
height: 37,
backgroundColor: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.22)',
borderRadius: '30px',
padding: '2px',
"& .MuiToggleButton-root": {
border: "none",
borderRadius: "30px",
color: "#fff",
padding: "6px 16px",
textTransform: "none",
fontSize: "14px",
"&.Mui-selected": {
backgroundColor: "#fff",
color: "#222",
fontWeight: "600",
"&:hover": {
backgroundColor: "#fff",
},
},
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.2)",
},
<Tooltip title="Learn about custom stats" arrow>
<a href="/docs/API#count-stats-for-custom-key" target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
<IconButton>
<OpenInNewIcon style={{ color: theme.palette.text.secondary, }} />
</IconButton>
</a>
</Tooltip>
<FormControl size="small" variant="outlined" style={{ minWidth: 350, }} sx={{
'& .MuiInputBase-root': {
height: 40,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: '20px',
},
}}
>
<ToggleButton value="workflows">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Workflows
</Box>
</ToggleButton>
<ToggleButton value="apps">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Apps
</Box>
</ToggleButton>
</ToggleButtonGroup>
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
}}>
<Autocomplete
labelId="data-type-choice"
label="Select Datatype"
autoHighlight
value={selectedStatType}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
height: 40, // Adjust the input height
},
'& .MuiAutocomplete-input': {
padding: '8px', // Adjust the text padding
},
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => {
if (
option === undefined ||
option === null ||
option.label === undefined ||
option.label === null
) {
if (option.value !== undefined && option.value !== null) {
return option.value
} else {
return option
}
}
const newname = (
option.label.charAt(0).toUpperCase() + option.label.substring(1)
).replaceAll("_", " ");
return newname;
}}
options={statTypeOptions}
fullWidth
style={{
backgroundColor: theme.palette?.inputColor,
borderRadius: theme.palette?.borderRadius,
}}
onChange={(event, newValue) => {
console.log("CHANGE: ", newValue)
}}
renderOption={(props, data, state) => {
// Format to thousand or million
const formattedamount = formatCompactNumber(data?.amount)
const numbercolor = data?.amount >= 1000000 ? red : (data?.amount >= 100000 ? yellow : green)
return (
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
}}
value={data}
onClick={() => {
setSelectedStatType(data)
// Set local storage for the key
if (data?.value) {
localStorage.setItem("runsOverTimeWidgetStatType", data?.value)
}
}}
>
<span style={{minWidth: 60, maxWidth: 60, color: numbercolor, }}>{formattedamount}</span> {data?.label}
</MenuItem>
)
}}
renderInput={(params) => {
return (
<div style={{ display: "flex", }}>
<TextField
style={theme.palette.textFieldStyle}
{...params}
label="Find your Stat"
variant="outlined"
/>
</div>
)
}}
/>
</FormControl>
<FormControl size="small" variant="outlined" style={{ minWidth: 170 }} sx={{
'& .MuiInputBase-root': {
height: 40,
@@ -438,7 +640,7 @@ function computeTodayValueForOrg(key, orgStats) {
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
height={300}
data={barData}
series={<BarSeries tooltip={tooltip} bar={<Bar rounded={true} />} />}
series={<BarSeries colorScheme={barColorscheme} tooltip={tooltip} bar={<Bar rounded={true} />} />}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
yAxis={
<LinearYAxis
+32 -14
View File
@@ -44,7 +44,7 @@ import {
Send as SendIcon,
} from '@mui/icons-material';
import { DataGrid } from '@mui/x-data-grid'
import { DataGrid, GridValueGetterParams } from '@mui/x-data-grid'
import {
Search as SearchIcon,
} from "@mui/icons-material";
@@ -78,7 +78,7 @@ const RuntimeDebugger = (props) => {
const [ignoreOrg, setIgnoreOrg] = useState(false)
const [searchLoading, setSearchLoading] = useState(false)
const [rowCursor, setCursor] = useState("")
const [rowsPerPage, setRowsPerPage] = useState(10)
const [rowsPerPage, setRowsPerPage] = useState(20)
const [maxExecutionCount, setMaxExecutionCount] = useState(50)
const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
@@ -219,6 +219,10 @@ const RuntimeDebugger = (props) => {
if (data.runs[key].completed_at === 0 || data.runs[key].completed_at === null) {
data.runs[key].endTimestamp = ""
}
if (data.runs[key]?.workflow_id === data.runs[key]?.execution_id && data.runs[key].type === "AGENT") {
data.runs[key].workflow.name = "Agent Execution"
}
}
@@ -359,6 +363,8 @@ const RuntimeDebugger = (props) => {
} else if (source === "single_action" || source == "single_api" || source === "direct_api") {
foundSource = <SendIcon color="secondary" style={{height: imageSize-5, }} />
source = "Single API call"
} else if (params?.row?.type === "AGENT") {
foundSource = <img src={theme.palette.singulBlackWhite} alt="agent" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
} else {
source = "manual"
}
@@ -626,19 +632,28 @@ const RuntimeDebugger = (props) => {
return (
<div style={{display: "flex", }}>
<Tooltip arrow placement="left" title={
<Typography variant="body2" style={{whiteSpace: "pre-line", padding: 10, }}>
Workflow result: {errorReason}<br/><br/>
{params.row.result !== null && params.row.result !== undefined && params.row.result !== "" ?
params.row.result
:
null
}
</Typography>
params.row.type === "AGENT" ?
"See agent result"
:
<Typography variant="body2" style={{whiteSpace: "pre-line", padding: 10, }}>
Workflow result: {errorReason}<br/><br/>
{params.row.result !== null && params.row.result !== undefined && params.row.result !== "" ?
params.row.result
:
null
}
</Typography>
}>
<span style={{backgroundColor: !hasError ? "inherit" : "rgba(244,0,0,0.45)", display: "flex", }}>
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
</Link>
{params.row.type === "AGENT" ?
<Link href={`/agents?execution_id=${params.row.execution_id}&authorization=${params.row.authorization}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
</Link>
:
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
</Link>
}
</span>
</Tooltip>
<Tooltip arrow title={`Force continue workflow. Only workflows for workflows in EXECUTING state. This is NOT a rerun, but way for Shuffle to figure out the next steps automatically. If the execution doesn't finish even after trying this, please contact ${supportEmail}`}>
@@ -1069,7 +1084,10 @@ const RuntimeDebugger = (props) => {
).replaceAll("_", " ");
return newname;
}}
options={workflows}
options={[{
"name": "Agent Runs",
"id": "AGENT",
}].concat(workflows)}
fullWidth
style={{
backgroundColor: theme.palette.backgroundColor,
+9 -6
View File
@@ -73,15 +73,18 @@ const SchedulesTab = memo((props) => {
setWorkflows(responseJson || []);
for (var i = 0; i < responseJson?.length; i++) {
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
if (responseJson[i].background_processing === true && (responseJson[i].name.toLowerCase().includes("ticket") || responseJson[i].name.toLowerCase().includes("ingest")) && responseJson[i].triggers !== undefined) {
for (var triggerkey in responseJson[i].triggers) {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
//console.log("TRIGGER: ", responseJson[i].triggers[triggerkey].trigger_type)
if (responseJson[i].triggers[triggerkey].trigger_type !== "WEBHOOK") {
continue
}
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
}
}
}
File diff suppressed because it is too large Load Diff
+43 -7
View File
@@ -25,6 +25,7 @@ import {
Skeleton,
Switch,
Box,
Chip,
} from "@mui/material";
import {
@@ -158,7 +159,7 @@ const UserManagmentTab = memo((props) => {
toast("Failed to update user");
} else {
//toast("Set the user field " + field + " to " + value);
toast("Successfully updated user field " + field);
toast.success(`Successfully updated user field '${field}'`)
if (field !== "suborgs") {
setSelectedUserModalOpen(false);
@@ -203,7 +204,7 @@ const UserManagmentTab = memo((props) => {
getUsers();
}, 1000);
toast("Invite sent! They will show up in the list when they have accepted the invite.")
toast.success("The user will show up in the list when finished.")
}
})
)
@@ -260,7 +261,6 @@ const UserManagmentTab = memo((props) => {
event.target.value = []
}
console.log("event: ", event.target.value);
setMatchingOrganizations(event.target.value);
// Workaround for empty orgs
if (event.target.value.length === 0) {
@@ -336,6 +336,17 @@ const UserManagmentTab = memo((props) => {
})
.then((responseJson) => {
setUsers(responseJson);
if (responseJson.success !== false) {
for (var i = 0; i < responseJson.length; i++) {
const data = responseJson[i];
if (data?.login_type === "DELETED") {
//toast.info("Found lost/half-deleted users you can recover. Please contact support@shuffler.io to learn more.")
break
}
}
}
setShowLoader(false)
})
.catch((error) => {
@@ -364,13 +375,13 @@ const UserManagmentTab = memo((props) => {
})
.then((responseJson) => {
if (!responseJson.success && responseJson.reason !== undefined) {
toast("Failed to deactivate user: " + responseJson.reason);
toast.error("Failed to deactivate user: " + responseJson.reason);
} else if (responseJson.success === false) {
toast(
toast.error(
`Failed to deactivate user. Please contact ${supportEmail} if this persists.`,
);
} else {
toast("Changed activation for user " + data.id);
toast.success("Changed activation for user " + data.id);
}
})
@@ -1529,6 +1540,7 @@ const UserManagmentTab = memo((props) => {
<ListItemText
primary={
<Select
disabled={data?.login_type === "DELETED"}
SelectDisplayProps={{
style: {
// marginLeft: 10,
@@ -1611,7 +1623,30 @@ const UserManagmentTab = memo((props) => {
data.login_type === undefined ||
data?.login_type === null ||
data?.login_type?.length === 0
? "Normal"
?
"Normal"
:
data?.login_type === "DELETED" ?
<Chip
style={{
marginLeft: 0,
padding: 0,
marginRight: 0,
cursor: 'pointer',
}}
label={"Recover"}
variant="outlined"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toast.warn("Re-adding the account to the org with 'user' role.")
inviteUser({
"Username": data.username,
})
}}
/>
: data.login_type
}
style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }}
@@ -1647,6 +1682,7 @@ const UserManagmentTab = memo((props) => {
) : null}
<ListItemText style={{ display:'table-cell', textAlign: "left", verticalAlign: 'middle', padding: "8px", }}>
<IconButton
disabled={data?.login_type === "DELETED"}
onClick={() => {
setSelectedUserModalOpen(true);
setSelectedUser(data);
+199 -52
View File
@@ -15,6 +15,7 @@ import theme from "../theme.jsx";
import { toast } from "react-toastify";
import { Context } from "../context/ContextApi.jsx";
import { getTheme } from "../theme.jsx";
import { red } from "../views/AngularWorkflow.jsx"
const useStyles = makeStyles({
notchedOutline: {
@@ -25,6 +26,14 @@ const useStyles = makeStyles({
const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{
// Check if user is admin
const isAdmin = userdata?.active_org?.role === "admin" || userdata?.support === true;
// State for tracking user SSO connection status
const [users, setUsers] = React.useState([]);
const [userSSOConnected, setUserSSOConnected] = React.useState(false);
const [checkingSSOStatus, setCheckingSSOStatus] = React.useState(true);
const classes = useStyles();
const [show2faSetup, setShow2faSetup] = React.useState(false);
const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision)
@@ -97,6 +106,53 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
const { themeMode, supportEmail, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
// Function to fetch users and check current user's SSO status
const checkUserSSOStatus = () => {
setCheckingSSOStatus(true);
fetch(globalUrl + "/api/v1/getusers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson && Array.isArray(responseJson)) {
setUsers(responseJson);
// Find current user and check if they have SSO info for this org
const currentUser = responseJson.find(user => user.id === userdata?.id);
if (currentUser && currentUser.sso_infos && Array.isArray(currentUser.sso_infos)) {
const hasSSOForThisOrg = currentUser.sso_infos.some(ssoInfo =>
ssoInfo.org_id === selectedOrganization?.id && ssoInfo.sub
);
setUserSSOConnected(hasSSOForThisOrg);
} else {
setUserSSOConnected(false);
}
}
setCheckingSSOStatus(false);
})
.catch((error) => {
console.log("Error fetching users:", error);
setCheckingSSOStatus(false);
});
};
// Check SSO status on component mount and when organization changes
useEffect(() => {
if (userdata?.id && selectedOrganization?.id) {
checkUserSSOStatus();
}
}, [userdata?.id, selectedOrganization?.id]);
useEffect(()=>{
if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) {
@@ -149,7 +205,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
disabled={
userdata === undefined ||
userdata === null ||
userdata.admin !== "true"
!isAdmin
}
onClick={() =>
handleEditOrg(
@@ -256,7 +312,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
const data = {
org_id: selectedOrganization?.id,
sso_test: true,
sso: true,
};
fetch(url, {
@@ -309,6 +365,59 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
});
};
const HandleDisconnectSSO = () => {
const url = `${globalUrl}/api/v1/disconnect_sso`;
const data = {
org_id: selectedOrganization?.id,
};
fetch(url, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify(data),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status !== 200) {
toast.error(
`Failed to disconnect SSO. Please try again later or contact ${supportEmail} if issue persists.`,
{ duration: 3000 }
);
return null;
}
return response.json();
})
.then((responseJson) => {
if (!responseJson) return;
if (responseJson.success === true) {
toast.success(
"Successfully disconnected from SSO.",
{ duration: 3000 }
);
// Refresh the SSO status after disconnecting
checkUserSSOStatus();
} else {
toast.error(
responseJson.reason || "Failed to disconnect SSO.",
{ duration: 3000 }
);
}
})
.catch((error) => {
console.error("Error disconnecting SSO:", error);
toast.error(
"An error occurred while disconnecting SSO. Please try again.",
{ duration: 3000 }
);
});
};
return (
<div style={{ width: "100%", height: "100%",boxSizing: 'border-box', padding: "27px 10px 19px 27px", backgroundColor: theme.palette.platformColor , borderRadius: '16px', }}>
<div style={{ height: "100%", width: "100%", overflowX: 'hidden', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}} >
@@ -316,6 +425,81 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
<Typography variant="h5" style={{ width: "100%", fontWeight: 500, fontSize: 24}}>
SSO Configuration
</Typography>
{/* SSO Connection section - moved to top */}
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 20,
width: "100%",
paddingBottom: 10,
}}
>
<Typography variant="body2" color="textSecondary" style={{ margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
{checkingSSOStatus
? "Checking your SSO connection status..."
: userSSOConnected
? "Your account is connected with this org's SSO!"
: "Connect your account with this org's SSO!"
}
</Typography>
<Tooltip
title={
checkingSSOStatus
? "Checking SSO connection status..."
: userSSOConnected
? "Your account is already connected to SSO"
: !(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
? "SSO must be configured for this organization before you can connect."
: ""
}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<span style={{ width: 150 }}>
<Button
variant={userSSOConnected ? "contained" : "outlined"}
color={userSSOConnected ? "success" : "primary"}
style={{ width: 150, textTransform: "none", margin: "10px 10px 10px 0px", whiteSpace: "nowrap" }}
disabled={
checkingSSOStatus ||
userSSOConnected ||
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
}
onClick={HandleTestSSO}
>
{checkingSSOStatus ? "Checking..." : userSSOConnected ? "Connected" : "Connect with SSO"}
</Button>
</span>
{userSSOConnected && (
<Tooltip title="Disconnect your account from SSO">
<span style={{ width: 120 }}>
<Button
variant="outlined"
color="error"
style={{ width: 120, textTransform: "none", margin: "10px 0px 10px 0px", whiteSpace: "nowrap" }}
onClick={HandleDisconnectSSO}
>
Disconnect
</Button>
</span>
</Tooltip>
)}
</div>
</Tooltip>
</div>
<div
style={{
display: "flex",
@@ -339,6 +523,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
disabled={!isAdmin}
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
/>
{SSORequired ? "Required" : "Optional"}
@@ -369,7 +554,6 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
title="Disable auto-provisioning of users in SSO"
/>
</div>
@@ -399,6 +583,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
disabled={!isAdmin}
title="Disable auto-provisioning of users in SSO"
/>
</div>
@@ -428,58 +613,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
disabled={!isAdmin}
title="Disable auto-provisioning of users in SSO"
/>
</div>
</div>
}
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 30,
width: "100%",
paddingBottom: 10,
}}
>
<Typography variant="body2" color="textSecondary" style={{ margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
You can test your SSO configuration by clicking the button below.
Before testing, ensure you have set Open ID Connect or SAML SSO
credentials.
</Typography>
<Tooltip
title={
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
? "Please ensure all SSO credentials are set before testing."
: ""
}
>
<span style={{ width: 100 }}>
<Button
variant="outlined"
color="primary"
style={{ width: 100, textTransform: "none", margin: "10px 10px 10px 0px", whiteSpace: "nowrap" }}
disabled={
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
}
onClick={HandleTestSSO}
>
Test SSO
</Button>
</span>
</Tooltip>
</div>
<Grid item xs={12} sx={{marginTop: 2}}>
<span style={{ display: "flex", flexDirection: "column" }}>
<Typography variant="h5" color="textPrimary" style={{ textAlign: "left", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 500, }}>OpenID connect</Typography>
@@ -514,6 +653,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
onChange={(e) => setShowOpenIdCred(e.target.checked)}
name="showOpenIdCred"
color="primary"
disabled={!isAdmin}
/>
</div>
<Grid container style={{ marginTop: 8, }} spacing={2}>
@@ -536,6 +676,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
variant="outlined"
placeholder="The OpenID client ID from the identity provider"
value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""}
disabled={!isAdmin}
onChange={(e) => setOpenidClientId(e.target.value)}
onFocus={(e) => setShowOpenIdCred(true)}
onBlur={(e) => setShowOpenIdCred(false)}
@@ -574,6 +715,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
variant="outlined"
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""}
disabled={!isAdmin}
onChange={(e) => {
setOpenidClientSecret(e.target.value);
}}
@@ -616,6 +758,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
rows={2}
placeholder="The OpenID authorization URL (usually ends with /authorize)"
value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""}
disabled={!isAdmin}
onChange={(e) => {
setOpenidAuthorization(e.target.value)
}}
@@ -656,6 +799,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
rows={2}
placeholder="The OpenID token URL (usually ends with /token)"
value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""}
disabled={!isAdmin}
onChange={(e) => {
setOpenidToken(e.target.value)
}}
@@ -682,8 +826,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
{/*isCloud ? null : */}
<Grid item xs={12} sx={{ marginTop: 3.5 }} >
<Typography variant="h5" color="textPrimary" style={{ textAlign: "left", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 500, }}>SAML SSO (v1.1)</Typography>
<Typography variant="body2" color="textSecondary" style={{ textAlign: "left", marginTop: 8, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, }}>
IdP URL for Shuffle SAML/SSO: <Link to={`${globalUrl}/api/v1/login_sso`} target="_blank" style={{ color: theme.palette.text.secondary, textDecoration: "none" }}>{`${globalUrl}/api/v1/login_sso`}</Link>
<Typography variant="body2" color="textSecondary" style={{ textAlign: "left", marginTop: 4, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, fontStyle: "italic", color: red }}>
Note: Support for SAML SSO was deprecated due to potential security issues. Please consider migrating to OpenID Connect for better compatibility and features.
</Typography>
<div style={{ display: 'flex', marginTop: 10, }}>
<Typography
@@ -702,6 +846,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
onChange={(e) => setShowSamlCred(e.target.checked)}
name="showSamlCred"
color="primary"
disabled={!isAdmin}
/>
</div>
<Grid container style={{ marginTop: 10, }} spacing={2}>
@@ -725,6 +870,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
variant="outlined"
placeholder="The entrypoint URL from your provider"
value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""}
disabled={!isAdmin}
onChange={(e) => {
setSsoEntrypoint(e.target.value);
}}
@@ -765,6 +911,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
rows={2}
placeholder="The X509 certificate to use"
value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""}
disabled={!isAdmin}
onFocus={(e) => setShowSamlCred(true)}
onBlur={(e) => setShowSamlCred(false)}
onChange={(e) => {
+167 -52
View File
@@ -1,4 +1,6 @@
export default function defaultCytoscapeStyle(theme) {
import { red, } from "./views/AngularWorkflow.jsx"
export default function defaultCytoscapeStyle(theme, apps) {
return [
{
selector: "node",
@@ -85,11 +87,12 @@ export default function defaultCytoscapeStyle(theme) {
selector: `node[type="ACTION"]`,
css: {
shape: "roundrectangle",
"background-color": "#213243",
"border-color": "#81c784",
"background-color": theme.palette.cytoscapeBackgroundColor,
"border-width": "2px",
"border-color": "#02CB70",
"background-width": "100%",
"background-height": "100%",
"border-radius": "5px",
"corner-radius": 12,
"z-index": 5001,
},
},
@@ -168,7 +171,9 @@ export default function defaultCytoscapeStyle(theme) {
"background-width": "100%",
"background-height": "100%",
"background-color": "data(iconBackground)",
"background-color": function(element) {
return element.data("iconBackground") || undefined;
},
"background-fill": "data(fillstyle)",
"background-gradient-direction": "to-bottom-right",
"background-gradient-stop-colors": "data(fillGradient)",
@@ -185,7 +190,9 @@ export default function defaultCytoscapeStyle(theme) {
"font-size": "0px",
"background-width": "75%",
"background-height": "75%",
"background-color": "data(iconBackground)",
"background-color": function(element) {
return element.data("iconBackground") || undefined;
},
"background-fill": "data(fillstyle)",
"background-gradient-direction": "to-right",
"background-gradient-stop-colors": "data(fillGradient)",
@@ -212,6 +219,8 @@ export default function defaultCytoscapeStyle(theme) {
css: {
"background-image": "data(large_image)",
"text-halign": "right",
"background-width": "85%",
"background-height": "85%",
},
},
{
@@ -267,7 +276,9 @@ export default function defaultCytoscapeStyle(theme) {
"font-size": "0px",
border: "1px solid rgba(255,255,255,0.9)",
"background-image": "data(icon)",
"background-color": "data(iconBackground)",
"background-color": function(element) {
return element.data("iconBackground") || null;
} || "null",
},
},
{
@@ -327,43 +338,6 @@ export default function defaultCytoscapeStyle(theme) {
"background-height": "100%",
},
},
{
selector: `node[app_id="shuffle_agent"]`,
css: {
"shape": function(element) {
return "roundrectangle"
},
"height": "74px",
"width": "222px",
"background-image": "data(large_image)",
"label": function(element) {
var elementname = element.data("label")
if (elementname === null || elementname === undefined) {
return ""
}
elementname = elementname.replaceAll("_", " ", -1)
if (elementname.length > 15) {
elementname = elementname.substring(0, 15) + ".."
}
return elementname
},
"background-width": "65px",
"background-height": "65px",
"background-position-x": "20px",
"background-repeat": "no-repeat",
"background-opacity": "0.5",
"font-size": "14px",
"text-halign": "center",
"text-valign": "center",
"text-margin-x": "20px",
"text-margin-y": "0px",
},
},
{
selector: "node[!is_valid]",
css: {
@@ -382,9 +356,9 @@ export default function defaultCytoscapeStyle(theme) {
{
selector: ".skipped-highlight",
css: {
"background-color": "grey",
"background-color": "transparent",
"border-color": "grey",
"border-width": "8px",
"border-width": "3px",
"transition-property": "background-color",
"transition-duration": "0.5s",
},
@@ -433,7 +407,7 @@ export default function defaultCytoscapeStyle(theme) {
selector: ".executing-highlight",
css: {
//"background-color": "#ffef47",
"border-color": "#FECC00",
"border-color": "#FFC633",
"border-width": "8px",
"transition-property": "border-width",
"transition-duration": "0.25s",
@@ -475,8 +449,8 @@ export default function defaultCytoscapeStyle(theme) {
selector: "edge.executing-highlight",
css: {
width: "5px",
"target-arrow-color": "#FECC00",
"line-color": "#FECC00",
"target-arrow-color": "#FFC633",
"line-color": "#FFC633",
"transition-property": "line-color, width",
"transition-duration": "0.25s",
},
@@ -533,6 +507,8 @@ export default function defaultCytoscapeStyle(theme) {
{
selector: ".eh-preview, .eh-ghost-edge",
style: {
"curve-style": "unbundled-bezier",
"control-point-step-size": 60,
"background-color": "#337ab7",
"line-color": "#337ab7",
"target-arrow-color": "#337ab7",
@@ -575,7 +551,14 @@ export default function defaultCytoscapeStyle(theme) {
height: "data(height)",
padding: "0px",
margin: "0px",
"background-color": "data(iconBackground)",
"background-color": function(element) {
const value = element.data()
if (value?.backgroundcolor === null || value?.backgroundcolor === undefined) {
return "#888888"
}
return element.data("backgroundcolor")
},
"background-fill": "data(fillstyle)",
"background-gradient-direction": "to-right",
"background-gradient-stop-colors": "data(fillGradient)",
@@ -593,8 +576,26 @@ export default function defaultCytoscapeStyle(theme) {
{
selector: "node:selected",
css: {
"border-color": "#f86a3e",
"border-width": "7px",
"border-color": "#FF8444",
"border-width": "3px",
"background-color": "transparent",
},
},
{
// This is to show the underlay orange effect on action on selection
selector: 'node[type="ACTION"]:selected',
css: {
"underlay-color": "rgba(255, 132, 68, 0.15)",
"underlay-padding": 10,
"underlay-opacity": 0.3,
"underlay-corner-radius": 16,
},
},
{
// For the startNode
selector: 'node[?isStartNode][app_id != "shuffle_agent"]:selected',
css: {
"underlay-shape": "ellipse",
},
},
{
@@ -605,6 +606,120 @@ export default function defaultCytoscapeStyle(theme) {
"background-color": "#f85a3e",
},
},
{
selector: `node[large_image="/images/singul_green.png"]`,
css: {
"background-image": function(element) {
const nodeData = element.data()
if (apps === null || apps === undefined) {
return nodeData?.large_image
}
const params = nodeData["parameters"]
if (params === null || params === undefined) {
return nodeData?.large_image
}
var foundappname = ""
for (var key in params) {
const param = params[key]
if (param.name === "app_name") {
foundappname = param?.value.toLowerCase().replaceAll(" ", "_")
break
}
}
if (foundappname == "") {
return nodeData?.large_image
}
for (var key in apps) {
const app = apps[key]
const newappname = app.name.toLowerCase().replaceAll(" ", "_")
if (newappname == foundappname) {
if (app.large_image !== null && app.large_image !== undefined && app.large_image != "") {
element.data("large_image", app.large_image)
return app.large_image
}
}
}
return nodeData?.large_image
},
},
},
{
selector: 'node[flowOrientation = "horizontal"][?small_image], node[flowOrientation = "horizontal"][?large_image]',
css: {
"text-halign": "center",
"text-valign": "bottom",
"text-margin-x": "0px",
"text-margin-y": "12px",
},
},
{
selector: 'node[flowOrientation = "vertical"][?small_image], node[flowOrientation = "vertical"][?large_image]',
css: {
"text-halign": "right",
"text-valign": "center",
"text-margin-x": "15px",
"text-margin-y": "0px",
},
},
{
selector: 'node[?isStartNode], node[?isStartNode][flowOrientation = "vertical"]',
css: {
"text-halign": "right",
"text-valign": "center",
"text-margin-x": "15px",
"text-margin-y": "0px",
},
},
{
selector: 'node[?isStartNode][flowOrientation = "horizontal"]',
css: {
"text-halign": "center",
"text-valign": "bottom",
"text-margin-x": "0px",
"text-margin-y": "12px",
},
},
{
selector: `node[app_id="shuffle_agent"]`,
css: {
"shape": function(element) {
return "roundrectangle"
},
"height": "74px",
"width": "222px",
"background-image": "data(large_image)",
"label": function(element) {
var elementname = element.data("label")
if (elementname === null || elementname === undefined) {
return ""
}
elementname = elementname.replaceAll("_", " ", -1)
if (elementname.length > 15) {
elementname = elementname.substring(0, 15) + ".."
}
return elementname
},
"background-width": "65px",
"background-height": "65px",
"background-position-x": "20px",
"background-repeat": "no-repeat",
"background-color": "#212121",
"font-size": "14px",
"text-halign": "center",
"text-valign": "center",
"text-margin-x": "20px",
"text-margin-y": "0px",
},
},
{
selector: `node[name="switch"]`,
css: {
+14 -1
View File
@@ -177,6 +177,7 @@ export const getTheme = (themeMode, brandColor) =>
surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF",
platformColor: themeMode === "dark" ? "#212121" : "#ffffff",
backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1",
cytoscapeBackgroundColor: themeMode === "dark" ? "#161616" : "#f5f5f5",
distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080",
cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea",
cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0",
@@ -188,6 +189,8 @@ export const getTheme = (themeMode, brandColor) =>
green: themeMode === "dark" ? "#5cc879" : "#008000",
defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC',
linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor,
slateGrayColor: themeMode === "dark" ? "#494949" : "#CCCCCC",
parsedAppPaperColor: themeMode === "dark" ? "#2f2f2f" : "#CCCCCC",
borderRadius: 10,
loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0",
@@ -214,6 +217,13 @@ export const getTheme = (themeMode, brandColor) =>
drawer: {
backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9"
},
actionSidebarField: {
backgroundColor: themeMode === "dark" ? "#2F2F2F" : "#F1F1F1",
color: themeMode === "dark" ? "#ffffff" : "#000000",
borderRadius: 8,
height: 40,
border: "none",
},
reactJsonStyle: {
padding: 5,
width: "98%",
@@ -258,7 +268,7 @@ export const getTheme = (themeMode, brandColor) =>
defaultImage: "/images/no_image.png",
singulOrange: "/images/singul_orange.png",
singulGreen: "/images/singul_green.png",
singulBlackWhite: "/images/singul_black_white.png",
singulBlackWhite: "/icons/workflow-page/shuffle_agent.png",
scrollbarColor: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1",
scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent",
},
@@ -337,6 +347,7 @@ export const getTheme = (themeMode, brandColor) =>
borderRadius: '4px',
whiteSpace: "nowrap",
textWrap: "normal",
transition: 'background-color 0.2s ease-in-out',
'&:hover': {
fontWeight: 600,
backgroundColor: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F',
@@ -352,6 +363,7 @@ export const getTheme = (themeMode, brandColor) =>
borderRadius: '4px',
boxShadow: 'none',
whiteSpace: "nowrap",
transition: 'all 0.2s ease-in-out',
textWrap: "normal",
'&:hover': {
fontWeight: 600,
@@ -523,3 +535,4 @@ export const getTheme = (themeMode, brandColor) =>
},
},
});
+572 -216
View File
@@ -24,6 +24,9 @@ import {
TextField,
Popover,
Divider,
AvatarGroup,
Avatar,
} from '@mui/material'
import {
@@ -40,6 +43,7 @@ import {
Refresh as RefreshIcon,
Add as AddIcon,
Warning as WarningIcon,
Pause as PauseIcon,
} from '@mui/icons-material'
import {
@@ -68,6 +72,27 @@ const AgentUI = (props) => {
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
const [chosenApps, setChosenApps] = useState([])
const activateApp = (appId) => {
if (appId === undefined || appId === null || appId === "") {
return
}
const url = `${globalUrl}/api/v1/apps/${appId}/activate`
fetch(url, {
method: "GET",
credentials: "include",
})
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson.success !== false) {
getApps()
} else {
toast.warn("Failed to activate app: " + responseJson.reason)
}
})
}
useEffect(() => {
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
@@ -83,9 +108,76 @@ const AgentUI = (props) => {
id: newSelectedApp.objectID,
image: newSelectedApp.image_url,
}]))
var found = false
if (apps?.length > 0) {
for (var key in apps) {
const app = apps[key]
if (app?.id !== newSelectedApp?.objectID) {
continue
}
found = true
break
}
}
if (!found) {
activateApp(newSelectedApp.objectID)
}
}
}, [newSelectedApp])
useEffect(() => {
if (chosenApps?.length > 0) {
return
}
if (apps?.length === 0) {
console.log("No apps loaded yet")
return
}
if (data === undefined || data === null) {
return
}
if (data?.allowed_actions === undefined || data?.allowed_actions === null) {
return
}
console.log("HERE!")
console.log("Data, apps: ", data, apps)
var newChosenApps = []
for (var key in data?.allowed_actions) {
const allowedAction = data?.allowed_actions[key]
if (!allowedAction?.startsWith("app:")) {
newChosenApps.push({
"name": allowedAction,
"large_image": "",
})
continue
}
const appId = allowedAction.split(":")[1]
for (var appKey in apps) {
const app = apps[appKey]
if (app.id !== appId) {
continue
}
newChosenApps.push(app)
break
}
}
if (newChosenApps.length > 0) {
setChosenApps(newChosenApps)
}
}, [data])
const {themeMode} = useContext(Context)
const theme = getTheme(themeMode)
const navigate = useNavigate();
@@ -95,12 +187,13 @@ const AgentUI = (props) => {
}
const agentWrapperStyle = {
width: 1000,
height: 1000,
width: "100%",
maxHeight: "100vh",
margin: "auto",
paddingTop: 100,
paddingBottom: 1000,
backgroundColor: theme.palette.backgroundColor,
paddingBottom: showAgentStarter ? 0 : 1500,
}
if (data.input === undefined || data.input === null) {
@@ -382,7 +475,7 @@ const AgentUI = (props) => {
getAppAuth()
}, [])
const maxTimelineWidth = 380
const maxTimelineWidth = 375
const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
console.log("Submitting questions: ", decisionId, questionAnswers)
@@ -411,10 +504,15 @@ const AgentUI = (props) => {
} else {
for (var key in questionAnswers) {
const answer = questionAnswers[key]
if (isContinuation === true) {
newArgument["question_"+(answer.index)] = answer.value
// Special handler for continuation of normal actions
// Approves/denies continuation
if (key === "approve") {
newArgument[key] = questionAnswers[key]
break
}
const answer = questionAnswers[key]
newArgument["question_"+(answer.index)] = answer.value
}
}
@@ -552,6 +650,7 @@ const AgentUI = (props) => {
item.label = item?.details?.reason || item.label
} else if (item?.category === "ask" || item?.details?.action === "ask") {
console.log("IN HERE?: ", item)
item.type = "question"
item.category = "ask"
@@ -572,6 +671,8 @@ const AgentUI = (props) => {
item.label = item?.details?.reason || item.label
}
console.log("ITEM: ", item, questions)
var parsedCategory = item.category === "singul" ?
<Tooltip title="Singul" placement="top">
<img src="/images/logos/singul.svg" style={categoryStyle} />
@@ -592,7 +693,14 @@ const AgentUI = (props) => {
if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) {
// Find the app and inject the image
const toolName = item.details.tool.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
var toolName = item?.details?.tool?.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
if (toolName.includes("app:")) {
const toolNameSplit = toolName.split(":")
if (toolNameSplit.length >= 2) {
toolName = toolNameSplit[2]
}
}
for (var appKey in apps) {
const app = apps[appKey]
@@ -657,10 +765,17 @@ const AgentUI = (props) => {
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
toast.error("No decision details found to rerun. Cannot proceed. Please go back to your workflow or /agents to start over.")
} else {
if (item?.details?.original_input !== undefined && item?.details?.original_input !== null && item?.details?.original_input !== "") {
setActionInput(item?.details?.original_input)
setDisableButtons(true)
submitInput(item?.details?.original_input)
return
}
//console.log("DETAILS: ", item?.details)
for (var messagekey in item?.details?.input?.messages) {
const message = item?.details?.input?.messages[messagekey]
if (message.role === "user") {
if (message.role === "user" && !message?.role?.includes("USER CONTEXT")) {
setActionInput(message.content)
setDisableButtons(true)
@@ -678,8 +793,8 @@ const AgentUI = (props) => {
</Tooltip>
const rerunButton =
<Tooltip title="Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." placement="right">
const rerunButton = item?.type === "processing" ? null :
<Tooltip title={item?.type === "decision" ? "Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." : ""} placement="right">
<span>
<IconButton
disabled={item.type !== "decision" || disableButtons}
@@ -698,6 +813,11 @@ const AgentUI = (props) => {
</span>
</Tooltip>
var itemLabel = item?.label?.replaceAll("_", " ") || ""
if (item?.details?.reason !== undefined && item?.details?.reason !== null && item?.details?.reason !== "") {
itemLabel = item?.details?.reason
}
return (
<div
style={{
@@ -775,17 +895,33 @@ const AgentUI = (props) => {
<div style={{
minWidth: 300,
maxWidth: 300,
maxHeight: 150,
overflowX: "hidden",
overflowY: "auto",
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}>
{item.label}
{itemLabel}
</div>
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right">
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right"
PopperProps={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 130], // [skid, distance]
},
},
],
}}
style={theme.palette.tooltip}
>
<div style={{
minWidth: maxTimelineWidth,
maxWidth: maxTimelineWidth,
paddingTop: defaultTopPadding*1.5,
marginLeft: 5,
}}>
{currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
<div style={{
@@ -813,40 +949,6 @@ const AgentUI = (props) => {
{item.category === "ask" ?
<span style={{display: "flex", }}>
{rerunButton}
{/*
<Tooltip title="Approve" placement="left">
<span>
<IconButton
disabled={disableButtons}
style={{marginLeft: 20, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toast.info("Approving this step.")
}}
>
<CheckIcon style={{color: green, }} />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Deny" placement="left">
<span>
<IconButton
disabled={item.type !== "decision" || disableButtons}
style={{marginLeft: 0, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
toast.info("Stopping on this step.")
}}
>
<CloseIcon style={{color: red, }} />
</IconButton>
</span>
</Tooltip>
*/}
<Tooltip title="Answer in the Form UI" placement="left">
<span>
@@ -872,9 +974,63 @@ const AgentUI = (props) => {
rerunAgentButton
:
item?.type === "decision" ?
item?.details?.run_details?.status === "WAITING" ?
agentRequestLoading ? null :
<div>
<Button
style={{
maxHeight: 30,
minHeight: 30,
minWidth: 75,
maxWidth: 75,
marginTop: 5,
}}
color="primary"
variant="contained"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
// FIXME: How do we approve it? Hmm
toast.info("Approving this step.")
// Keeping it as string to make it simple
const newAnswers = {
"approve": "true",
}
submitQuestions(item?.details?.run_details?.id, newAnswers)
}}
>
Approve
</Button>
<Button
style={{
maxHeight: 30,
minHeight: 30,
minWidth: 75,
maxWidth: 75,
marginTop: 3,
}}
variant="outlined"
color="secondary"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
// Keeping it as string to make it simple
const newAnswers = {
"approve": "false",
}
submitQuestions(item?.details?.run_details?.id, newAnswers)
}}
>
Deny
</Button>
</div>
:
<div style={{display: "flex", }}>
{rerunButton}
<Tooltip title="Explore/debug execution" placement="left">
<Tooltip title="Explore/debug app run" placement="left">
<span>
<IconButton
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
@@ -895,26 +1051,29 @@ const AgentUI = (props) => {
:
rerunButton
}
<Tooltip title="Explore results" placement="right">
<span>
<IconButton
disabled={item.details === undefined || item.details === null || item.details === ""}
style={{marginLeft: 10, }}
>
{open ?
<ExpandLessIcon />
:
<ExpandMoreIcon />
}
</IconButton>
</span>
</Tooltip>
{item?.type === "processing" ? null :
<Tooltip title="Explore results" placement="right">
<span>
<IconButton
disabled={item.details === undefined || item.details === null || item.details === ""}
style={{marginLeft: 10, }}
>
{open ?
<ExpandLessIcon />
:
<ExpandMoreIcon />
}
</IconButton>
</span>
</Tooltip>
}
</div>
</div>
{showAuthentication && selectedApp.id !== undefined ?
<div style={{minWidth: 300, maxWidth: 300, margin: "auto", marginTop: 25, }}>
<div style={{minWidth: 300, maxWidth: 300, margin: "auto", marginTop: 0, marginBottom: 25, }}>
<AuthenticationModal
globalUrl={globalUrl}
userdata={userdata}
@@ -925,7 +1084,7 @@ const AgentUI = (props) => {
</div>
: null}
{questions?.length > 0 && item?.status === "RUNNING" ?
{questions?.length > 0 && item?.status === "RUNNING" || item?.status === "WAITING" ?
<div>
{questions.map((q, questionIndex) => {
return (
@@ -976,13 +1135,17 @@ const AgentUI = (props) => {
<Button
variant="contained"
style={{marginTop: 16, }}
disabled={questionSubmitDisabled}
disabled={questionSubmitDisabled || agentRequestLoading}
onClick={() => {
submitQuestions(item?.details?.run_details?.id, questionAnswers)
}}
>
Submit
{agentRequestLoading ?
<CircularProgress size={20} style={{marginRight: 3, }} />
:
"Submit"
}
</Button>
</div>
: null}
@@ -1078,6 +1241,7 @@ const AgentUI = (props) => {
}
var finishDecisionId = ""
var finishAnswer = ""
var sortedTimelineItems = []
for (var key in agent_data?.decisions) {
const item = agent_data.decisions[key]
@@ -1105,17 +1269,73 @@ const AgentUI = (props) => {
if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
finishDecisionId = item?.run_details?.id
}
finishAnswer = item?.reason || ""
if (item?.fields !== undefined && item?.fields !== null) {
// Just choosing the first one
for (var fieldKey in item?.fields) {
const field = item?.fields[fieldKey]
finishAnswer = field?.value || finishAnswer
break
}
}
}
}
timelineItems.sort((a, b) => {
if (a.start_time < b.start_time) {
return 1;
// Fill blank space between items in timeline by adding "empty" decisions:
// 1. Sort
// 2. Fill blank space
// 3. Sort again
timelineItems.sort((a, b) => a.start_time - b.start_time)
for (var i = 0; i < timelineItems.length - 1; i++) {
if (timelineItems?.length <= 2) {
break
}
return 0;
})
if (i === timelineItems.length - 1) {
break
}
const currentItem = timelineItems[i]
if (currentItem?.type === "processing") {
continue
}
const nextItem = timelineItems[i + 1]
if (nextItem?.type === "processing") {
continue
}
if (nextItem.start_time - currentItem.end_time > 1) {
/*
timelineItems?.push({
label: "",
type: "processing",
category: "processing",
status: "FINISHED",
start_time: currentItem.end_time,
end_time: nextItem.start_time,
})
*/
}
}
timelineItems.sort((a, b) => a.start_time - b.start_time)
const handleKeyDownCont = (e) => {
if (finishDecisionId === "") {
return
}
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
if (isCmdEnter || isCtrlEnter) {
e.preventDefault()
submitQuestions(finishDecisionId, {
"continue": continuationText,
}, true)
}
}
return (
<div style={{marginTop: 20, }}>
@@ -1151,6 +1371,7 @@ const AgentUI = (props) => {
<Box
component="form"
style={{width: "100%", textAlign: "center",}}
onKeyDown={handleKeyDownCont}
onSubmit={(e) => {
e.preventDefault();
@@ -1161,15 +1382,35 @@ const AgentUI = (props) => {
}, true)
}}
>
<div style={{display: "flex", maxWidth: 550, minWidth: 550, margin: "auto", marginTop: 50, }}>
{finishAnswer !== "" ?
<div style={{marginTop: 50, overflowX: "hidden", textAlign: "left", }}>
<Markdown
components={markdownComponents}
id="markdown_wrapper"
className={"style.reactMarkdown"}
escapeHtml={false}
skipHtml={false}
remarkPlugins={[remarkGfm]}
style={{
maxWidth: "100%",
minWidth: "100%",
}}
>
{finishAnswer}
</Markdown>
</div>
: null}
<div style={{display: "flex", maxWidth: 600, minWidth: 600, margin: "auto", marginTop: 50, }}>
<div>
<TextField
label="Add more details to the current task"
label="Add more details to continue the current task"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 400, margin: "auto", }}
style={{width: 450, margin: "auto", }}
multiline
minRows={1}
minRows={2}
value={continuationText}
onChange={(e) => {
console.log("Value: ", e.target.value)
//setActionInput(e.target.value)
@@ -1195,7 +1436,7 @@ const AgentUI = (props) => {
}}
/>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
Any failed tasks will be set to ignored (TBD). <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
Any failed tasks will be set to ignored. <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
</Typography>
</div>
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
@@ -1242,7 +1483,7 @@ const AgentUI = (props) => {
// 1. Run the execution. Can this be a single-action run?
// 2. Get the execution ID and node ID from the response.
const uuid = uuidv4()
var parsedAction = "list_tickets,API" // Default action for now
var parsedAction = "API" // Default action for now
if (chosenApps.length > 0) {
parsedAction = ""
for (var appKey in chosenApps) {
@@ -1319,155 +1560,270 @@ const AgentUI = (props) => {
cursor: "pointer",
}
const typeButtonGroup =
<ButtonGroup style={{}}>
<Button
variant={showAgentStarter ? "contained" : "outlined"}
color="secondary"
onClick={() => {
if (actionInput === "" || actionInput === undefined || actionInput === null) {
setActionInput(data?.original_input || "")
}
//setChosenApps(chosenApps.concat([{
setTimeout(() => {
setShowAgentStarter(true)
}, 200)
}}
>
Restart
</Button>
<Button
variant={buttonState === "default" ? "contained" : "outlined"}
color="secondary"
disabled
onClick={() => {
setButtonState("default");
setShowAgentStarter(false)
}}
>
Default
</Button>
<Button
variant={buttonState === "timeline" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("timeline");
setShowAgentStarter(false)
}}
>
Timeline
</Button>
</ButtonGroup>
return (
<div style={agentWrapperStyle}>
<TextField
id="copy_element_shuffle"
style={{ display: "none" }}
/>
{showAgentStarter ?
<Box
component="form"
style={{textAlign: "center", }}
onKeyDown={handleKeyDownRoot}
onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
}}
>
<img src="/images/logos/agent.svg" style={{
width: 200,
height: 200,
borderRadius: theme.palette.borderRadius,
}} />
<div />
<Typography variant="h5" style={{marginTop: 30, }}>
Shuffle AI Agents
</Typography>
<div>
<div style={{height: showAgentStarter ? "25vh" : "5vh", }}>
</div>
<div style={agentWrapperStyle}>
<div style={{
minWidth: 1000, maxWidth: 1000, margin: "auto",
}}>
<TextField
label="What do you want to do?"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline
minRows={1}
defaultValue={actionInput || ""}
onChange={(e) => {
setActionInput(e.target.value)
}}
InputProps={{
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit">
<SendIcon
color="primary"
/>
</IconButton>
</Tooltip>
),
}}
id="copy_element_shuffle"
style={{ display: "none" }}
/>
<div style={{display: "flex", margin: "auto", paddingTop: 10, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
<div>
<Chip
id="add_app_chip"
icon={<AddIcon />} label="Select Apps"
style={chipStyle}
onClick={() => {
setAppPickerAnchor(document.getElementById("add_app_chip"))
{showAgentStarter ?
<Box
component="form"
style={{textAlign: "center", }}
onKeyDown={handleKeyDownRoot}
onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
}}
>
{/*
<img src="/images/logos/agent.svg" style={{
width: 200,
height: 200,
borderRadius: theme.palette.borderRadius,
}} />
*/}
<div />
<img
src={theme.palette.singulBlackWhite}
style={{
width: 100,
height: 100,
borderRadius: theme.palette.borderRadius,
}}
/>
<Popover
open={appPickerAnchor !== null}
anchorEl={appPickerAnchor}
onClose={() => {
setAppPickerAnchor(null)
<Typography variant="h2" style={{marginTop: 30, }}>
What do you want to do?
</Typography>
<TextField
label="Get my emails for today and summarise them"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 650, marginTop: 30, }}
multiline
minRows={1}
maxRows={20}
defaultValue={actionInput || ""}
onChange={(e) => {
setActionInput(e.target.value)
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
InputProps={{
style: {
borderRadius: 30,
},
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit">
<SendIcon
color="primary"
/>
</IconButton>
</Tooltip>
),
}}
>
<AppSearch
userdata={userdata}
defaultSearch={""}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
inputHeight={200}
/>
<div style={{display: "flex", margin: "auto", marginTop: 30, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
<div style={{}}>
<Chip
id="add_app_chip"
icon={<AddIcon />} label="Select Apps / MCPs"
style={chipStyle}
onClick={() => {
setAppPickerAnchor(document.getElementById("add_app_chip"))
}}
/>
</Popover>
<Popover
open={appPickerAnchor !== null}
anchorEl={appPickerAnchor}
onClose={() => {
setAppPickerAnchor(null)
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<AppSearch
userdata={userdata}
defaultSearch={""}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
inputHeight={250}
apps={apps}
/>
</Popover>
</div>
{chosenApps.map((app, index) => {
var chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
if (chosenName === "API") {
chosenName = "default"
}
const chosenImagePath = app?.image === undefined || app?.image === null || app?.image === "" ? app?.large_image === undefined || app?.large_image === null || app?.large_image === "" ? theme.palette.singulBlackWhite : app.large_image : app.image
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
return (
<Chip icon={chosenImage} label={chosenName} variant="outlined"
style={chipStyle}
onDelete={() => {
const newChosenApps = chosenApps.filter((a, i) => i !== index)
setChosenApps(newChosenApps)
}}
/>
)
})}
</div>
<div style={{marginTop: 20, }}>
{execution === undefined || execution === null || execution?.execution_id === undefined || execution?.execution_id === null ? null : typeButtonGroup}
</div>
{/*
<div style={{
height: 300,
width: "100%",
background: showAgentStarter ? `linear-gradient(to bottom, ${theme.palette.backgroundColor}, #C9591D)` : "inherit",
backgroundSize: "100% 100%",
backgroundRepeat: "no-repeat",
backgroundPosition: "0px 100px",
}} />
*/}
</Box>
:
<div>
<div style={{display: "flex", }}>
<div style={{flex: 2, }}>
{typeButtonGroup}
<ButtonGroup style={{marginLeft: 25, }}>
<Tooltip title="Reload the agent data" placement="top">
<span>
<Button
disabled={execution === null || Object.keys(execution).length === 0}
style={{}}
variant={"outlined"}
color="secondary"
onClick={() => {
GetExecution(execution?.execution_id, agentActionResult?.action?.id, execution?.authorization)
}}
>
<RefreshIcon />
</Button>
</span>
</Tooltip>
{execution?.status === "EXECUTING" ?
<Tooltip title="Stop the agent" placement="top">
<span>
<Button
disabled={execution === null || true}
style={{}}
variant={"outlined"}
color="secondary"
onClick={() => {
toast.warn("Not implemented. Should run abort.")
}}
>
<PauseIcon />
</Button>
</span>
</Tooltip>
: null}
</ButtonGroup>
</div>
<div style={{flex: 1, }}>
<AvatarGroup>
{chosenApps?.map((app, index) => {
return(
<Tooltip title={`Constraint: ${app?.name}`} key={index} placement="top">
<Avatar
key={index}
alt={app?.name}
src={app?.large_image ? app.large_image : app?.image ? app.image : theme.palette.singulBlackWhite}
onClick={() => {
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
}}
style={{
cursor: "pointer",
width: 30,
height: 30,
}}
/>
</Tooltip>
)
})}
</AvatarGroup>
</div>
</div>
{buttonState === "timeline" ?
<TimelineRender agent_data={data} />
:
null
}
</div>
{chosenApps.map((app, index) => {
const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
const chosenImagePath = app?.image
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
return (
<Chip icon={chosenImage} label={chosenName} variant="outlined"
style={chipStyle}
onDelete={() => {
const newChosenApps = chosenApps.filter((a, i) => i !== index)
setChosenApps(newChosenApps)
}}
/>
)
})}
</div>
</Box>
:
<div>
<ButtonGroup style={{marginTop: 50, }}>
<Button
variant={buttonState === "default" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("default");
}}
>
Default
</Button>
<Button
variant={buttonState === "timeline" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("timeline");
}}
>
Timeline
</Button>
</ButtonGroup>
<Tooltip title="Reload the agent data" placement="top">
<span>
<Button
disabled={execution === null || Object.keys(execution).length === 0}
style={{marginLeft: 25, }}
variant={"outlined"}
color="secondary"
onClick={() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}}
>
<RefreshIcon />
</Button>
</span>
</Tooltip>
{buttonState === "timeline" ?
<TimelineRender agent_data={data} />
:
null
}
</div>
}
</div>
</div>
)
}
File diff suppressed because one or more lines are too long
+104 -54
View File
@@ -563,7 +563,7 @@ const AppCreator = (defaultprops) => {
};
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true");
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
useEffect(() => {
if (window.location.pathname.includes("apps/edit")) {
@@ -699,20 +699,20 @@ const AppCreator = (defaultprops) => {
// This is the data FROM the database, not what's being saved
const parseIncomingOpenapiData = (data) => {
var parsedDecoded = ""
try {
const decoded = base64_decode(data.openapi)
parsedDecoded = decoded
var parsedDecoded = ""
try {
const decoded = base64_decode(data.openapi)
parsedDecoded = decoded
} catch (e) {
console.log("Failed JSON parsing: ", e)
parsedDecoded = data
}
console.log("Failed JSON parsing: ", e)
parsedDecoded = data
}
if (data.openapi === null) {
toast("Failed to load OpenAPI for app. Please contact support if this persists.")
setIsAppLoaded(true);
return
}
if (data.openapi === null) {
toast("Failed to load OpenAPI for app. Please contact support if this persists.")
setIsAppLoaded(true);
return
}
//console.log("Decoded: ", parsedDecoded)
const parsedapp =
@@ -791,6 +791,9 @@ const AppCreator = (defaultprops) => {
}
if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) {
if (typeof data.info["x-categories"] === "array") {
} else {
}
setNewWorkflowCategories(data.info["x-categories"]);
}
}
@@ -913,36 +916,41 @@ const AppCreator = (defaultprops) => {
body: "",
errors: [],
example_response: "",
action_label: "No Label",
action_label: ["No Label"],
required_bodyfields: [],
};
if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) {
console.log("LABEL: ", methodvalue["x-label"])
const labels = typeof methodvalue["x-label"] === 'object' || Array.isArray(methodvalue["x-label"]) ? methodvalue["x-label"] : (typeof methodvalue["x-label"] === 'string' ? methodvalue["x-label"].split(",") : [])
var correctlabel = ""
const labels = methodvalue["x-label"].split(",")
var correctlabels = []
console.log("LABEL1: ", methodvalue["x-label"])
for (let labelkey in labels) {
var label = labels[labelkey].trim()
if (label.toLowerCase() === "no label") {
var label = labels[labelkey]?.trim()
if (label?.toLowerCase() === "no label") {
continue
}
// Remove quotes and escapes
label = label.replace(/['"]+/g, '')
label = label.replace(/\\/g, '')
label = label?.replace(/['"]+/g, '')
label = label?.replace(/\\/g, '')
//label = label.replace("_", " ", -1)
//label = label.charAt(0).toUpperCase() + label.slice(1)
// Uppercase first
if (label === undefined || label === null || label?.length === 0 || label?.toLowerCase().replaceAll(" ", "_") === "no_label") {
label = "No Label"
}
correctlabel = label
break
label = label?.replace("_", " ")
label = label?.charAt(0).toUpperCase() + label?.slice(1)
correctlabels?.push(label)
}
console.log("LABEL: ", correctlabel)
console.log("LABEL: ", correctlabels)
// FIX: Map labels only if they're actually in the category list
//newaction.action_label = methodvalue["x-label"]
newaction.action_label = correctlabel
newaction.action_label = correctlabels
}
if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) {
@@ -1678,7 +1686,7 @@ const AppCreator = (defaultprops) => {
}
}
//newaction.action_label = "No Label"
//newaction.action_label = ["No Label"]
newActions.push(newaction);
}
}
@@ -1961,9 +1969,9 @@ const AppCreator = (defaultprops) => {
setProjectCategories(all_categories);
// Rearrange them by which has action_label
const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label[0] !== "No Label")
console.log("First actions: ", firstActions)
const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label[0] === "No Label")
newActions = firstActions.concat(secondActions)
setActions(newActions);
//data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label
@@ -2114,17 +2122,16 @@ const AppCreator = (defaultprops) => {
},
};
if (item.action_label !== undefined && item.action_label !== "" && item.action_label !== "No Label") {
//console.log("Action label: ", item.action_label)
data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label
}
if (item.action_label !== undefined && item.action_label !== "" && item.action_label[0] !== "No Label") {
data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label
}
if (item.required_bodyfields !== undefined && item.required_bodyfields !== null && item.required_bodyfields.length > 0) {
console.log("Required bodyfields: ", item.required_bodyfields)
data.paths[item.url][item.method.toLowerCase()]["x-required-fields"] = item.required_bodyfields
}
if (item.required_bodyfields !== undefined && item.required_bodyfields !== null && item.required_bodyfields.length > 0) {
console.log("Required bodyfields: ", item.required_bodyfields)
data.paths[item.url][item.method.toLowerCase()]["x-required-fields"] = item.required_bodyfields
}
//console.log("ACTION: ", item)
//console.log("ACTION: ", item)
if (item.example_response !== undefined && item.example_response !== null && item.example_response.length > 0) {
@@ -3448,7 +3455,7 @@ const AppCreator = (defaultprops) => {
errors: [],
example_response: "",
method: actionNonBodyRequest[0],
action_label: "No Label",
action_label: ["No Label"],
required_bodyfields: [],
});
@@ -4450,6 +4457,31 @@ const AppCreator = (defaultprops) => {
const url = data.url;
const hasFile = (data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download"
if (data?.action_label[0] !== 'No Label') {
console.log("Action label: ", data?.name, data?.action_label)
}
var currentActionLabels = JSON.parse(JSON.stringify(actionLabels))
for (var labelKey in data?.action_label) {
const label = data?.action_label[labelKey]
if (label.toLowerCase().replaceAll(" ", "_") === "no_label") {
continue
}
var found = false
for (var acatKey in currentActionLabels) {
const acat = currentActionLabels[acatKey]
if (acat === label) {
found = true
break
}
}
if (!found) {
console.log(`Not found: '${label}' in ${currentActionLabels}`)
currentActionLabels.push(data?.action_label[labelKey])
}
}
return (
<Paper key={index} style={actionListStyle}>
@@ -4532,23 +4564,38 @@ const AppCreator = (defaultprops) => {
*/}
{/* From 2023: Example of handling action labels */}
{actionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ?
{currentActionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ?
<Select
multiple={true}
fullWidth
onChange={(e) => {
console.log("Should change: ", e.target.value, " Index: ", index)
if (e.target.value === undefined || e.target.value === null) {
return
}
if (e.target.value.length === 0) {
e.target.value = ["No Label"]
}
const foundIndex = actions.findIndex((action) => action.name === data.name)
console.log("Found index: ", foundIndex)
if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
if (e.target.value.includes("no_label") || e.target.value.includes("No_Label") || e.target.value.includes("No Label")) {
actions[foundIndex].action_label = ["No Label"]
setActions(actions)
setUpdate(Math.random())
return
}
console.log("Should change: ", e.target.value, " Index: ", index)
actions[foundIndex].action_label = e.target.value
setActions(actions)
setUpdate(Math.random())
}
}}
value={data?.action_label?.replace(" ", "_").toLowerCase()}
value={data?.action_label}
style={{
border: data.action_label === undefined || data.action_label === "No Label" ? "" : `2px solid ${bgColor}`,
border: data.action_label === undefined || data.action_label[0] === "No Label" ? "" : `2px solid ${bgColor}`,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.backgroundColor,
paddingLeft: 10,
@@ -4563,12 +4610,15 @@ const AppCreator = (defaultprops) => {
name: "Method",
id: "method-option",
}}
onClick={(e) => {
console.log("Current labels: ", data?.action_label)
}}
>
{actionLabels.map((label, labelindex) => {
{currentActionLabels.map((label, labelindex) => {
return (
<MenuItem
key={labelindex}
value={label.replace(" ", "_").toLowerCase()}
value={label}
style={{
}}
>
@@ -4633,7 +4683,7 @@ const AppCreator = (defaultprops) => {
body: "",
errors: [],
method: actionNonBodyRequest[0],
action_label: "No Label",
action_label: ["No Label"],
required_bodyfields: [],
});
setActionsModalOpen(true);
@@ -5385,15 +5435,15 @@ const AppCreator = (defaultprops) => {
body: "",
errors: [],
method: actionNonBodyRequest[0],
action_label: "No Label",
required_bodyfields: [],
action_label: ["No Label"],
required_bodyfields: [],
});
console.log("Added: ", actions)
console.log("Added: ", actions)
setActions(actions)
setFilteredActions(actions)
setFilteredActions(actions)
setActionAmount(50);
setUpdate(Math.random());
setUpdate(Math.random());
}}
>
New action
@@ -5417,7 +5467,7 @@ const AppCreator = (defaultprops) => {
// body: "",
// errors: [],
// method: actionNonBodyRequest[0],
// action_label: "No Label",
// action_label: ["No Label"],
// required_bodyfields: [],
//})
//setActions(actions)
+293 -216
View File
@@ -65,6 +65,7 @@ import { Context } from "../context/ContextApi.jsx";
import {
SearchBox,
StaticRefinementList,
RefinementList,
InstantSearch,
connectSearchBox,
@@ -72,14 +73,14 @@ import {
Index,
} from "react-instantsearch-dom";
import AppStats from "../components/AppStats.jsx";
import ParsedAction from "../components/ParsedAction.jsx";
import ParsedAction from "../components/ParsedActionNew.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
import { base64_decode, appCategories } from "../views/AppCreator.jsx";
import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
import AuthenticationWindow from "../components/AuthenticationWindow.jsx";
import { CodeHandler, Img, OuterLink, CopyToClipboard, } from "../views/Docs.jsx";
import { useStyles, } from "../components/ParsedAction.jsx";
import { useStyles, } from "../components/ParsedActionNew.jsx";
import { sortByKey } from "../views/AngularWorkflow.jsx";
import { v4 as uuidv4 } from "uuid";
@@ -240,13 +241,14 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const [triggers, setTriggers] = useState([])
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined)
const [selectedValidationAction, setSelectedValidationAction] = React.useState({})
const [settingValidationAction, setSettingValidationAction] = React.useState(false)
const [selectedMeta, setSelectedMeta] = React.useState({
link: "https://github.com/Shuffle/openapi-apps/new/master/docs",
read_time: 1,
})
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true");
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
// FIXME: This is used, as useEffect() creates an issue with apps not loading at all
@@ -453,6 +455,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
rel="noopener noreferrer"
target="_blank"
href={data.url}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<Tooltip title={data.url} placement="bottom">
@@ -1163,8 +1166,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) {
// Check if there are commas in it then loop and find the correct one
// Should ignore 'No Label' and 'No label'
var correctlabel = ""
const labels = methodvalue["x-label"].split(",")
const labels = typeof methodvalue["x-label"] === 'object' || Array.isArray(methodvalue["x-label"]) ? methodvalue["x-label"] : methodvalue["x-label"].split(",")
var correctlabels = []
for (let labelkey in labels) {
var label = labels[labelkey].trim()
if (label.toLowerCase() === "no label") {
@@ -1178,12 +1181,14 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
//label = label.replace("_", " ", -1)
//label = label.charAt(0).toUpperCase() + label.slice(1)
correctlabel = label
break
correctlabels.push(label)
if (label === "app_validation") {
setSelectedValidationAction(newaction)
}
}
// FIX: Map labels only if they're actually in the category list
newaction.action_label = correctlabel
newaction.action_label = correctlabels
}
if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) {
@@ -2931,7 +2936,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}
const ActionSelectOption = (actionprops) => {
const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops;
const { option, newActionname, newActiondescription, useIcon, extraDescription, selected, } = actionprops;
const [hover, setHover] = React.useState(false);
return (
@@ -2945,14 +2950,18 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
padding: 8,
paddingLeft: 14,
paddingBottom: 4,
backgroundColor: hover ? theme.palette.surfaceColor : theme.palette.inputColor,
backgroundColor: selected === true ? theme.palette.platformColor : hover ? theme.palette.surfaceColor : theme.palette.inputColor,
border: selected === true ? `2px solid ${theme.palette.platformColor}` : `2px solid ${theme.palette.inputColor}`,
}} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
onClick={(event) => {
console.log("Clicked on action: ", option)
if (option !== undefined && option !== null) {
// FIXME: Add removal of app_validation from others
setSelectedValidationAction(option)
toast.info(`Selecting validation action...`)
setSettingValidationAction(true)
const labelData = {
"app_id": app.id,
@@ -2973,12 +2982,20 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason)
setSettingValidationAction(false)
if (responseJson.success === true) {
toast.success("Validation action selected")
} else if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast.error(responseJson.reason)
} else {
toast.error("Failed selecting validation action")
}
})
.catch((error) => {
console.log("Error: ", error)
setSettingValidationAction(false)
toast.error("Failed selecting validation action")
})
// Update the app itself?
@@ -3104,13 +3121,15 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
<Paper
style={{
flex: 6,
margin: "auto",
margin: 10,
padding: 30,
backgroundColor: boxStyle.backgroundColor,
color: theme.palette.text.primary,
textAlign: "left",
paddingBottom: 50,
overflow: "hidden",
margin: "auto",
}}
>
<Tabs
@@ -3133,7 +3152,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
<Tab style={{color: theme.palette.text.primary, textTransform: "none", }} icon={appType === 0 || appType === 2 ? <OpenInNewIcon /> : <AppsIcon />} label={appType === 0 || appType === 2 ? "Try the API" : "Try it out"} />
<Tab icon={<ShowChartIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} label="Stats & Downloads" />
<Tab icon={<PolylineIcon />} style={{color: theme.palette.text.secondary, textTransform: "none", }} disabled label="Integrations" />
<Tab icon={<PolylineIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} disabled style={{color: theme.palette.text.secondary}} label="Integrations" />
<Tab icon={<PersonIcon />} disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary, textTransform: "none", }} label="Creator" value={4} />
</Tabs>
<div style={{ marginTop: 25 }}>
@@ -3481,222 +3500,280 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
The validation action is the action that is used to validate the app. This is used both when a user wants to validate their auth, as well as when Shuffle runs automatic tests of the app. It is recommended that the action should be a GET request. Validation is decided based on whether the action is ran successfully in a workflow.
</Typography>
<Autocomplete
id="action_search"
value={selectedValidationAction}
classes={{ inputRoot: classes.inputRoot }}
groupBy={(option) => {
// Most popular
// Is categorized
// Uncategorized
return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions";
}}
renderGroup={(params) => {
<div style={{display: "flex", }}>
<Autocomplete
id="action_search"
value={selectedValidationAction}
classes={{ inputRoot: classes.inputRoot }}
groupBy={(option) => {
// Most popular
// Is categorized
// Uncategorized
return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions";
}}
renderGroup={(params) => {
return (
<li key={params.key}>
<Typography variant="body1" style={{textAlign: "center", marginLeft: 10, marginTop: 25, marginBottom: 10, }}>{params.group}</Typography>
<Typography variant="body2">{params.children}</Typography>
</li>
)
}}
options={renderedActionOptions}
ListboxProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
},
}}
filterOptions={(options, { inputValue }) => {
const lowercaseValue = inputValue === null ? "" : inputValue.toLowerCase()
options = options.filter((x) => {
if (x.name === undefined || x.name === null) {
x.name = ""
}
if (x.description === undefined || x.description === null) {
x.description = ""
}
if (x.method !== "GET") {
return null
}
return x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)
})
return options
}}
getOptionLabel={(option) => {
if (option === undefined || option === null || option.name === undefined || option.name === null ) {
return null;
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
fullWidth
style={{
backgroundColor: theme.palette.inputColor,
height: 50,
borderRadius: theme.palette?.borderRadius,
}}
onChange={(event, newValue) => {
// Workaround with event lol
console.log("Changed to: ", event, newValue)
toast("Changed validation action")
if (newValue !== undefined && newValue !== null) {
/*
setNewSelectedAction({
target: {
value: newValue.name
}
})
*/
}
}}
renderOption={(props, option, state) => {
var newActionname = option.name;
if (option.label !== undefined && option.label !== null && option.label.length > 0) {
newActionname = option.label;
}
var newActiondescription = option.description;
//console.log("DESC: ", newActiondescription)
if (option.description === undefined || option.description === null) {
newActiondescription = "Description: No description defined for this action"
} else {
newActiondescription = "Description: "+newActiondescription
}
const iconInfo = GetIconInfo({ name: option.name });
const useIcon = iconInfo.originalIcon;
if (newActionname === undefined || newActionname === null) {
newActionname = "No name"
option.name = "No name"
option.label = "No name"
}
newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " ");
var method = ""
var extraDescription = ""
if (option.name.includes("get_")) {
method = "GET"
} else if (option.name.includes("post_")) {
method = "POST"
} else if (option.name.includes("put_")) {
method = "PUT"
} else if (option.name.includes("patch_")) {
method = "PATCH"
} else if (option.name.includes("delete_")) {
method = "DELETE"
} else if (option.name.includes("options_")) {
method = "OPTIONS"
} else if (option.name.includes("connect_")) {
method = "CONNECT"
}
// FIXME: Should it require a base URL?
if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) {
var extraUrl = ""
const descSplit = option.description.split("\n")
// Last line of descSplit
if (descSplit.length > 0) {
extraUrl = descSplit[descSplit.length-1]
}
if (extraUrl.length > 0) {
if (extraUrl.includes(" ")) {
extraUrl = extraUrl.split(" ")[0]
return (
<li key={params.key}>
<Typography variant="body1" style={{textAlign: "center", marginLeft: 10, marginTop: 25, marginBottom: 10, }}>{params.group}</Typography>
<Typography variant="body2">{params.children}</Typography>
</li>
)
}}
options={renderedActionOptions}
ListboxProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
},
}}
filterOptions={(options, { inputValue }) => {
const lowercaseValue = inputValue === null ? "" : inputValue.toLowerCase()
options = options.filter((x) => {
if (x.name === undefined || x.name === null) {
x.name = ""
}
if (extraUrl.includes("#")) {
extraUrl = extraUrl.split("#")[0]
if (x.description === undefined || x.description === null) {
x.description = ""
}
extraDescription = `${method} ${extraUrl}`
} else {
//console.log("No url found. Check again :)")
}
}
if (x.method !== "GET") {
return null
}
return (
<ActionSelectOption
{...props}
option={option}
newActiondescription={newActiondescription}
useIcon={useIcon}
newActionname={newActionname}
extraDescription={extraDescription}
/>
);
}}
renderInput={(params) => {
if (params.inputProps?.value) {
const prefixes = ["Post", "Put", "Patch"];
for (let prefix of prefixes) {
if (params.inputProps.value.startsWith(prefix)) {
let newValue = params.inputProps.value.replace(prefix + " ", "");
if (newValue.length > 1) {
newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1);
return x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)
})
return options
}}
getOptionLabel={(option) => {
if (option === undefined || option === null || option.name === undefined || option.name === null ) {
return null;
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
fullWidth
style={{
backgroundColor: theme.palette.inputColor,
height: 50,
borderRadius: theme.palette?.borderRadius,
}}
onChange={(event, newValue) => {
// Workaround with event lol
console.log("Changed to: ", event, newValue)
toast("Changed validation action")
if (newValue !== undefined && newValue !== null) {
/*
setNewSelectedAction({
target: {
value: newValue.name
}
})
*/
}
}}
renderOption={(props, option, state) => {
var newActionname = option.name;
if (option.label !== undefined && option.label !== null && option.label.length > 0) {
newActionname = option.label;
}
var newActiondescription = option.description;
//console.log("DESC: ", newActiondescription)
if (option.description === undefined || option.description === null) {
newActiondescription = "Description: No description defined for this action"
} else {
newActiondescription = "Description: "+newActiondescription
}
const iconInfo = GetIconInfo({ name: option.name });
const useIcon = iconInfo.originalIcon;
if (newActionname === undefined || newActionname === null) {
newActionname = "No name"
option.name = "No name"
option.label = "No name"
}
newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " ");
var method = ""
var extraDescription = ""
if (option.name.includes("get_")) {
method = "GET"
} else if (option.name.includes("post_")) {
method = "POST"
} else if (option.name.includes("put_")) {
method = "PUT"
} else if (option.name.includes("patch_")) {
method = "PATCH"
} else if (option.name.includes("delete_")) {
method = "DELETE"
} else if (option.name.includes("options_")) {
method = "OPTIONS"
} else if (option.name.includes("connect_")) {
method = "CONNECT"
}
// FIXME: Should it require a base URL?
if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) {
var extraUrl = ""
const descSplit = option.description.split("\n")
// Last line of descSplit
if (descSplit.length > 0) {
extraUrl = descSplit[descSplit.length-1]
}
if (extraUrl.length > 0) {
if (extraUrl.includes(" ")) {
extraUrl = extraUrl.split(" ")[0]
}
// Set the new value without mutating inputProps
params = { ...params, inputProps: { ...params.inputProps, value: newValue } };
break;
if (extraUrl.includes("#")) {
extraUrl = extraUrl.split("#")[0]
}
extraDescription = `${method} ${extraUrl}`
} else {
//console.log("No url found. Check again :)")
}
}
// Check if it starts with "Get List" and method is "Get"
if (params.inputProps.value.startsWith("Get List")) {
console.log("Get List")
}
}
const actionDescription = ""
const isIntegration = false
const isSelected = selectedValidationAction !== undefined && selectedValidationAction !== null && selectedValidationAction?.name === option.name
return (
<Tooltip title={actionDescription}
placement="right"
open={false}
PopperProps={{
sx: {
'& .MuiTooltip-tooltip': {
backgroundColor: 'transparent',
boxShadow: 'none',
},
'& .MuiTooltip-arrow': {
color: 'transparent',
},
},
}}
>
<TextField
{...params}
<ActionSelectOption
{...props}
option={option}
newActiondescription={newActiondescription}
useIcon={useIcon}
newActionname={newActionname}
extraDescription={extraDescription}
data-lpignore="true"
autocomplete="off"
dataLPIgnore="true"
autoComplete="off"
color="primary"
id="checkbox-search"
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
}}
label={"Select Validation Action"}
variant="outlined"
name={`disable_autocomplete_${Math.random()}`}
/>
</Tooltip>
)
selected={isSelected}
/>
);
}}
/>
renderInput={(params) => {
if (params.inputProps?.value) {
const prefixes = ["Post", "Put", "Patch"];
for (let prefix of prefixes) {
if (params.inputProps.value.startsWith(prefix)) {
let newValue = params.inputProps.value.replace(prefix + " ", "");
if (newValue.length > 1) {
newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1);
}
// Set the new value without mutating inputProps
params = { ...params, inputProps: { ...params.inputProps, value: newValue } };
break;
}
}
// Check if it starts with "Get List" and method is "Get"
if (params.inputProps.value.startsWith("Get List")) {
console.log("Get List")
}
}
const actionDescription = ""
const isIntegration = false
return (
<Tooltip title={actionDescription}
placement="right"
open={false}
PopperProps={{
sx: {
'& .MuiTooltip-tooltip': {
backgroundColor: 'transparent',
boxShadow: 'none',
},
'& .MuiTooltip-arrow': {
color: 'transparent',
},
},
}}
>
<TextField
{...params}
data-lpignore="true"
autocomplete="off"
dataLPIgnore="true"
autoComplete="off"
color="primary"
id="checkbox-search"
variant="body1"
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
}}
label={"Select Validation Action"}
variant="outlined"
name={`disable_autocomplete_${Math.random()}`}
/>
</Tooltip>
)
}}
/>
<Button
disabled={selectedValidationAction === null || selectedValidationAction === undefined || executing || settingValidationAction}
onClick={() => {
if (app === null || app === undefined || app.id === undefined || app.name === undefined) {
toast.error("App not found");
return;
}
// Check if validation action is selected
if (selectedValidationAction === null || selectedValidationAction === undefined) {
toast.error("Please select a validation action first");
return;
}
setExecuting(true);
fetch(`${globalUrl}/api/v1/apps/${app.id}/test`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
toast.success("Test executed successfully");
} else {
if (responseJson.reason && responseJson.reason.includes("No validation action found")) {
toast.error("No validation action found. Please select a validation action from the dropdown above and try again.");
} else {
toast.error(responseJson.reason || "Test failed");
}
}
setExecuting(false);
})
.catch((error) => {
console.log("Error:", error);
toast.error("Request failed");
setExecuting(false);
});
}}
variant="outlined"
style={{
marginLeft: 20,
}}
>
{executing ? "Testing..." : "Run Test"}
</Button>
</div>
<Divider style={{marginTop: 30, marginBottom: 10, }}/>
<Typography variant="h6" style={{marginTop: 20, marginBottom: 10, }}>
+15
View File
@@ -1360,6 +1360,13 @@ const Apps2 = (props) => {
console.log("Failed to set apps in localstorage: ", e)
}
}
const userId = userdata?.id;
if (userId !== undefined && userId !== null) {
const filteredUserApps = privateapps.filter((app) => app.owner === userId);
setUserApps(filteredUserApps);
}
})
.catch((error) => {
console.log("Failed to get apps: ", error.toString());
@@ -1368,6 +1375,14 @@ const Apps2 = (props) => {
});
};
useEffect(() => {
const userId = userdata?.id;
if (userId !== undefined && userId !== null && userId.length > 0 && apps.length > 0) {
const filteredUserApps = apps.filter((app) => app.owner === userId);
setUserApps(filteredUserApps);
}
}, [userdata, apps]);
// Locally hotloads app from folder
const hotloadApps = () => {
toast("Hotloading apps from location in .env");
+2 -2
View File
@@ -972,7 +972,7 @@ const GettingStarted = (props) => {
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
var boxColor = "#FECC00";
var boxColor = "#FFC633";
if (data.is_valid) {
boxColor = "#86c142";
}
@@ -1562,7 +1562,7 @@ const GettingStarted = (props) => {
renderCell: (params) => {
const data = params.row.record;
var boxColor = "#FECC00";
var boxColor = "#FFC633";
if (data.is_valid) {
boxColor = "#86c142";
}
+340 -167
View File
@@ -103,6 +103,9 @@ import {
Publish as PublishIcon,
GetApp as GetAppIcon,
Image as ImageIcon,
Shield as ShieldIcon,
Warning as WarningIcon,
ChevronRight as ChevronRightIcon,
} from "@mui/icons-material";
// Additional Components
@@ -120,6 +123,7 @@ import { debounce } from "lodash";
import { removeQuery } from "../components/ScrollToTop.jsx";
import {green, yellow, red, grey, triggers as wfTriggers, } from "../views/AngularWorkflow.jsx"
import Licensed from "../components/Licensed.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e");
@@ -255,6 +259,18 @@ export const handleReactJsonClipboard = (copy) => {
// Takes an action in Shuffle and
// Returns information about the icon, the color etc to be used
// This can be used for actions of all types
// PARSED ICON PROPERTIES:
// icon - SVG <path> d attribute string
// iconColor - path fill/stroke color (used in cytoscape node SVGs; "white" on colored bg)
// iconBackgroundColor - solid bg color for the node
// fillGradient - [color1, color2] for gradient bg; if set, fillstyle = "linear-gradient"
// originalIcon - React <svg> element (22x22, currentColor) used in ParsedActionNew dropdown
// svgViewBox - viewBox for the SVG; "0 0 48 48" for 48-coord paths, defaults to "0 0 24 24"
// dropdownViewBox - tighter crop viewBox for dropdown rendering (auto: "10 10 28 28" for 48x48)
// useStroke - true render path with stroke (no fill); for outline-style icons
// useFill - true + useStroke render with BOTH fill and stroke
// fillRule - "evenodd" for complex filled shapes (e.g. filter); default "nonzero"
//
export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs.
const iconList = [
@@ -263,6 +279,7 @@ export const GetIconInfo = (action) => {
{ key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter"] },
{ key: "merge", values: ["join", "merge", "route", "router"] },
{key: "get_file_value", values: ["get_file_value"]},
{
key: "search",
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate", "siem", ],
@@ -343,7 +360,7 @@ export const GetIconInfo = (action) => {
"protect",
],
},
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", ] },
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", "sha", "md5",] },
];
var selectedKey = ""
@@ -392,57 +409,132 @@ export const GetIconInfo = (action) => {
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
cache_add: {
icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
// cache_add: {
// icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
// iconColor: "white",
// iconBackgroundColor: "#8acc3f",
// originalIcon: "",
// fillGradient: ["#8acc3f", "#459622"],
// },
get_file_value: {
icon: "M25.8332 14.8335H18.4998C18.0136 14.8335 17.5473 15.0267 17.2035 15.3705C16.8597 15.7143 16.6665 16.1806 16.6665 16.6668V31.3335C16.6665 31.8197 16.8597 32.286 17.2035 32.6299C17.5473 32.9737 18.0136 33.1668 18.4998 33.1668H29.4998C29.9861 33.1668 30.4524 32.9737 30.7962 32.6299C31.14 32.286 31.3332 31.8197 31.3332 31.3335V20.3335L25.8332 14.8335Z M25.8335 14.8335V20.3335H31.3335 M24 29.5V24 M21.25 26.75H26.75",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
iconBackgroundColor: "rgba(43, 189, 160, 1)",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
useStroke: true,
svgViewBox: "0 0 48 48",
fillGradient: ["rgba(43, 189, 160, 1)", "rgba(43, 189, 160, 1)"],
},
cache_get: {
icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
cache_add: {
icon: "M24 20.6667C28.1421 20.6667 31.5 19.5475 31.5 18.1667C31.5 16.786 28.1421 15.6667 24 15.6667C19.8579 15.6667 16.5 16.786 16.5 18.1667C16.5 19.5475 19.8579 20.6667 24 20.6667Z M31.5 24C31.5 25.3833 28.1667 26.5 24 26.5C19.8333 26.5 16.5 25.3833 16.5 24 M16.5 18.1667V29.8334C16.5 31.2167 19.8333 32.3334 24 32.3334C28.1667 32.3334 31.5 31.2167 31.5 29.8334V18.1667 M32 35C34.2091 35 36 33.2091 36 31C36 28.7909 34.2091 27 32 27C29.7909 27 28 28.7909 28 31C28 33.2091 29.7909 35 32 35Z M32 29V33 M30 31H34",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
iconBackgroundColor: "rgba(43, 189, 160, 1)",
fillGradient: ["rgba(43, 189, 160, 1)", "rgba(43, 189, 160, 1)"],
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
useStroke: true,
svgViewBox: "0 0 48 48",
},
// cache_get: {
// icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
// iconColor: "white",
// iconBackgroundColor: "#8acc3f",
// originalIcon: "",
// fillGradient: ["#8acc3f", "#459622"],
// },
cache_get: {
icon: "M22.25 14C24.379 14 26.3377 14.2865 27.79 14.7705C28.5123 15.0113 29.1516 15.3144 29.625 15.6875C30.09 16.054 30.5 16.5766 30.5 17.25V25.0889C30.4172 25.0848 30.3338 25.083 30.25 25.083C29.8184 25.083 29.3995 25.1374 29 25.2402V25.0537C28.6427 25.2475 28.2369 25.4175 27.7998 25.5635C26.3499 26.0475 24.3907 26.333 22.25 26.333C20.1093 26.333 18.1501 26.0475 16.7002 25.5635C16.2631 25.4175 15.8573 25.2475 15.5 25.0537V28.917C15.5002 28.9389 15.5137 29.0744 15.8018 29.3027C16.085 29.5272 16.542 29.7623 17.1748 29.9736C18.4332 30.3937 20.2243 30.667 22.25 30.667C23.3382 30.667 24.3581 30.5844 25.2646 30.4463C25.3016 30.9605 25.4159 31.4527 25.5967 31.9121C24.5747 32.0741 23.4408 32.167 22.25 32.167C20.1092 32.167 18.1502 31.8805 16.7002 31.3965C15.9791 31.1558 15.342 30.8524 14.8701 30.4785C14.4059 30.1106 14.0002 29.5883 14 28.917V17.25C14 16.5766 14.41 16.054 14.875 15.6875C15.3484 15.3144 15.9877 15.0113 16.71 14.7705C18.1623 14.2865 20.121 14 22.25 14ZM29 19.2168C28.64 19.4118 28.2306 19.5826 27.79 19.7295C26.3377 20.2135 24.379 20.5 22.25 20.5C20.121 20.5 18.1623 20.2135 16.71 19.7295C16.2694 19.5826 15.86 19.4118 15.5 19.2168V23.083C15.5001 23.1039 15.5122 23.2403 15.8018 23.4697C16.0851 23.6941 16.5421 23.9294 17.1748 24.1406C18.4332 24.5607 20.2243 24.833 22.25 24.833C24.2757 24.833 26.0668 24.5607 27.3252 24.1406C27.9579 23.9294 28.4149 23.6941 28.6982 23.4697C28.9878 23.2403 28.9999 23.1039 29 23.083V19.2168ZM22.25 15.5C20.2371 15.5 18.4456 15.7727 17.1836 16.1934C16.5488 16.405 16.0893 16.6412 15.8037 16.8662C15.5099 17.0978 15.5 17.2331 15.5 17.25C15.5001 17.2679 15.5111 17.4032 15.8037 17.6338C16.0893 17.8588 16.5488 18.095 17.1836 18.3066C18.4456 18.7273 20.2371 19 22.25 19C24.2628 19 26.0544 18.7273 27.3164 18.3066C27.9512 18.095 28.4107 17.8588 28.6963 17.6338C28.9889 17.4032 28.9999 17.2679 29 17.25C29 17.2331 28.9901 17.0978 28.6963 16.8662C28.4107 16.6412 27.9512 16.405 27.3164 16.1934C26.0544 15.7727 24.2629 15.5 22.25 15.5Z M29.9166 32.4164C31.3893 32.4164 32.5831 31.2225 32.5831 29.7497C32.5831 28.2769 31.3893 27.083 29.9166 27.083C28.4439 27.083 27.25 28.2769 27.25 29.7497C27.25 31.2225 28.4439 32.4164 29.9166 32.4164Z M33.2505 33.0828L31.8005 31.6328",
iconColor: "white",
iconBackgroundColor: "rgba(43, 189, 160, 1)",
fillGradient: ["rgba(43, 189, 160, 1)", "rgba(43, 189, 160, 1)"],
originalIcon: "",
useStroke: true,
svgViewBox: "0 0 48 48",
},
repeat: {
icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
icon: "M7.417 8.667V11.167H9.917 M16.583 15.333V12.833H14.083 M15.538 10.75C15.326 10.153 14.967 9.619 14.494 9.198C14.020 8.777 13.448 8.483 12.830 8.343C12.212 8.204 11.569 8.223 10.960 8.399C10.352 8.575 9.798 8.902 9.350 9.35L7.417 11.167M16.583 12.833L14.650 14.65C14.202 15.098 13.648 15.425 13.040 15.601C12.431 15.777 11.788 15.796 11.170 15.657C10.552 15.517 9.980 15.223 9.507 14.802C9.033 14.381 8.674 13.847 8.463 13.25",
iconColor: "white",
iconBackgroundColor: defaultColor,
iconBackgroundColor: "rgba(242, 101, 59, 1)",
fillGradient: ["rgba(242, 101, 59, 1)", "rgba(242, 101, 59, 1)"],
originalIcon: <CachedIcon />,
useStroke: true,
dropdownViewBox: "5 7 14 10",
},
// repeat: {
// icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
// iconColor: "white",
// iconBackgroundColor: defaultColor,
// originalIcon: <CachedIcon />,
// },
add: {
icon: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <AddIcon />,
},
// edit: {
// icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
// iconColor: "white",
// iconBackgroundColor: defaultColor,
// originalIcon: <EditIcon />,
// },
edit: {
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
icon: "M27.75 17.25C27.947 17.053 28.1808 16.8967 28.4382 16.7901C28.6956 16.6835 28.9714 16.6287 29.25 16.6287C29.5286 16.6287 29.8044 16.6835 30.0618 16.7901C30.3192 16.8967 30.553 17.053 30.75 17.25C30.947 17.447 31.1032 17.6808 31.2098 17.9382C31.3165 18.1956 31.3713 18.4714 31.3713 18.75C31.3713 19.0286 31.3165 19.3044 31.2098 19.5618C31.1032 19.8191 30.947 20.053 30.75 20.25L20.625 30.375L16.5 31.5L17.625 27.375L27.75 17.25Z",
iconColor: "white",
iconBackgroundColor: defaultColor,
iconBackgroundColor: "rgba(242, 101, 59, 1)",
fillGradient: ["rgba(242, 101, 59, 1)", "rgba(242, 101, 59, 1)"],
originalIcon: <EditIcon />,
useStroke: true,
svgViewBox: "0 0 48 48",
},
filter: {
icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
// filter: {
// icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
// iconColor: "white",
// iconBackgroundColor: "#f5515f",
// originalIcon: "",
// fillGradient: ["#f5515f", "#a1051d"],
// },
filter: {
icon: "M31.25 17C31.25 16.8619 31.1381 16.75 31 16.75H17C16.8619 16.75 16.75 16.8619 16.75 17V18.5859C16.75 18.6522 16.7764 18.7158 16.8232 18.7627L21.2373 23.1768C21.5655 23.5049 21.75 23.95 21.75 24.4141V30.958L26.25 29.459V24.4141C26.25 23.95 26.4345 23.5049 26.7627 23.1768L31.1768 18.7627C31.2236 18.7158 31.25 18.6522 31.25 18.5859V17ZM32.75 18.5859C32.75 19.05 32.5655 19.4951 32.2373 19.8232L27.8232 24.2373C27.7764 24.2842 27.75 24.3478 27.75 24.4141V29.6396C27.75 30.1769 27.4066 30.6558 26.8955 30.8262L21.8955 32.4922C21.0874 32.7615 20.2509 32.1611 20.25 31.3076V24.4141C20.25 24.3478 20.2236 24.2842 20.1768 24.2373L15.7627 19.8232C15.4345 19.4951 15.25 19.05 15.25 18.5859V17C15.25 16.0335 16.0335 15.25 17 15.25H31C31.9665 15.25 32.75 16.0335 32.75 17V18.5859Z",
iconColor: "white",
iconBackgroundColor: "#f5515f",
iconBackgroundColor: "rgba(217, 56, 136, 1)",
originalIcon: "",
fillGradient: ["#f5515f", "#a1051d"],
},
fillGradient: ["rgba(217, 56, 136, 1)", "rgba(217, 56, 136, 1)"],
fillRule: "evenodd",
svgViewBox: "0 0 48 48",
},
// merge: {
// icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
// iconColor: "white",
// iconBackgroundColor: "#f5515f",
// originalIcon: "",
// fillGradient: ["#f5515f", "#a1051d"],
// },
merge: {
icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
icon: "M23.4692 16.2696C23.7621 15.9768 24.2369 15.9769 24.5298 16.2696L29.3032 21.0421C29.5961 21.335 29.5961 21.8107 29.3032 22.1036C29.0103 22.3963 28.5345 22.3964 28.2417 22.1036L24.7495 18.6104V23.0802C24.7497 23.0867 24.7505 23.0932 24.7505 23.0997V24.0001C24.7507 28.0592 28.0409 31.3497 32.1001 31.3497C32.5141 31.3498 32.85 31.6857 32.8501 32.0997C32.8501 32.5138 32.5142 32.8496 32.1001 32.8497C28.4809 32.8497 25.3708 30.6766 23.9995 27.5645C22.6281 30.6765 19.519 32.8497 15.8999 32.8497C15.4857 32.8497 15.1499 32.5139 15.1499 32.0997C15.15 31.6856 15.4858 31.3497 15.8999 31.3497C19.9591 31.3497 23.2493 28.0592 23.2495 24.0001V18.6104L19.7573 22.1036C19.4644 22.3963 18.9886 22.3964 18.6958 22.1036C18.4032 21.8108 18.4032 21.3349 18.6958 21.0421L23.4692 16.2696Z",
iconColor: "white",
iconBackgroundColor: "#f5515f",
iconBackgroundColor: "rgba(217, 56, 136, 1)",
originalIcon: "",
fillGradient: ["#f5515f", "#a1051d"],
fillGradient: ["rgba(217, 56, 136, 1)", "rgba(217, 56, 136, 1)"],
useStroke: false,
useFill: true,
svgViewBox: "0 0 48 48",
},
// compare: {
// icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
// iconColor: "white",
// iconBackgroundColor: defaultColor,
// originalIcon: <CompareIcon />,
// },
compare: {
icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
icon: "M19 15.75H23.7832V32.25H19C17.7574 32.25 16.75 31.2426 16.75 30V18C16.75 16.8352 17.6352 15.8771 18.7695 15.7617L19 15.75Z M26.667 16.125H29.0003C30.6572 16.125 32.0003 17.4681 32.0003 19.125V28.875C32.0003 30.5319 30.6572 31.875 29.0003 31.875H26.667",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <CompareIcon />,
iconBackgroundColor: "rgba(242, 101, 59, 1)",
originalIcon: "",
fillGradient: ["rgba(242, 101, 59, 1)", "rgba(242, 101, 59, 1)"],
useStroke: true,
useFill: true,
svgViewBox: "0 0 48 48",
},
extract: {
icon: "M3 3h18v2H3z",
@@ -462,18 +554,36 @@ export const GetIconInfo = (action) => {
iconBackgroundColor: defaultColor,
originalIcon: <TocIcon />,
},
// execute: {
// icon: "M8 5v14l11-7z",
// iconColor: "white",
// iconBackgroundColor: defaultColor,
// originalIcon: <PlayArrowIcon />,
// },
execute: {
icon: "M8 5v14l11-7z",
icon: "M17.3335 28.1667L22.3335 23.1667L17.3335 18.1667 M24 29.8333H30.6667",
iconColor: "white",
iconBackgroundColor: defaultColor,
iconBackgroundColor: "rgba(242, 101, 59, 1)",
fillGradient: ["rgba(242, 101, 59, 1)", "rgba(242, 101, 59, 1)"],
originalIcon: <PlayArrowIcon />,
useStroke: true,
svgViewBox: "0 0 48 48",
},
// delete: {
// icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
// iconColor: "white",
// iconBackgroundColor: "#03030e",
// originalIcon: <DeleteIcon />,
// fillGradient: ["#03030e", "#205d66"],
// },
delete: {
icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
icon: "M15 18.5H33C31.5955 18.5 30.8933 18.5 30.3889 18.8371C30.1705 18.983 29.983 19.1705 29.8371 19.3889C29.5 19.8933 29.5 20.5955 29.5 22V27.5C29.5 29.3856 29.5 30.3284 28.9142 30.9142C28.3284 31.5 27.3856 31.5 25.5 31.5H22.5C20.6144 31.5 19.6716 31.5 19.0858 30.9142C18.5 30.3284 18.5 29.3856 18.5 27.5V22C18.5 20.5955 18.5 19.8933 18.1629 19.3889C18.017 19.1705 17.8295 18.983 17.6111 18.8371C17.1067 18.5 16.4045 18.5 15 18.5Z M21.5 15.5002C21.5 15.5002 22 14.5 24 14.5C26 14.5 26.5 15.5 26.5 15.5",
iconColor: "white",
iconBackgroundColor: "#03030e",
originalIcon: <DeleteIcon />,
fillGradient: ["#03030e", "#205d66"],
iconBackgroundColor: "rgba(98, 98, 98, 1)",
originalIcon: "",
fillGradient: ["rgba(98, 98, 98, 1)", "rgba(98, 98, 98, 1)"],
useStroke: true,
svgViewBox: "0 0 48 48",
},
close: {
icon: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
@@ -482,19 +592,37 @@ export const GetIconInfo = (action) => {
originalIcon: <CloseIcon />,
fillGradient: ["#03030e", "#205d66"],
},
// send: {
// icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
// iconColor: "white",
// iconBackgroundColor: "#0373da",
// originalIcon: <SendIcon />,
// fillGradient: ["#0bc8bf", "#0373da"],
// },
send: {
icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
icon: "M31.3332 16.6667L22.1665 25.8334 M31.3332 16.6667L25.4998 33.3334L22.1665 25.8334L14.6665 22.5001L31.3332 16.6667Z",
iconColor: "white",
iconBackgroundColor: "#0373da",
originalIcon: <SendIcon />,
fillGradient: ["#0bc8bf", "#0373da"],
iconBackgroundColor: "rgba(128, 107, 255, 1)",
originalIcon: "",
fillGradient: ["rgba(128, 107, 255, 1)", "rgba(128, 107, 255 , 1)"],
useStroke: true,
svgViewBox: "0 0 48 48",
},
// download: {
// icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
// iconColor: "white",
// iconBackgroundColor: "#0373da",
// originalIcon: <GetAppIcon />,
// fillGradient: ["#0bc8bf", "#0373da"],
// },
download: {
icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
icon: "M18 22L24 28M24 28L30 22M24 28L24 16 M17 33H31",
iconColor: "white",
iconBackgroundColor: "#0373da",
originalIcon: <GetAppIcon />,
fillGradient: ["#0bc8bf", "#0373da"],
iconBackgroundColor: "rgba(128, 107, 255, 1)",
originalIcon: "",
fillGradient: ["rgba(128, 107, 255, 1)", "rgba(128, 107, 255, 1)"],
useStroke: true,
svgViewBox: "0 0 48 48",
},
search: {
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
@@ -537,7 +665,7 @@ export const GetIconInfo = (action) => {
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <LockIcon />,
}
},
}
/*
@@ -568,28 +696,59 @@ export const GetIconInfo = (action) => {
}
if (
(selectedItem.originalIcon === undefined ||
selectedItem.originalIcon === "") &&
selectedItem.icon !== "" &&
selectedItem.icon !== undefined
) {
const svg_pin = (
<svg
width={svgSize}
height={svgSize}
viewBox={`0 0 ${svgSize} ${svgSize}`}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<path d={selectedItem.icon} fill={selectedItem.iconColor}></path>
</svg>
);
const iconViewBox = selectedItem.svgViewBox || `0 0 ${svgSize} ${svgSize}`;
// Use a tighter viewBox for dropdown icons so 48x48 centered content appears larger
const dropdownViewBox = selectedItem.dropdownViewBox || (iconViewBox === "0 0 48 48" ? "10 10 28 28" : iconViewBox);
const strokeW = iconViewBox === "0 0 48 48" ? "1.5" : "1";
// Build dropdown React SVG using the same stroke/fill logic as buildIconSvgPath
var svg_pin;
if (selectedItem.useStroke && selectedItem.useFill) {
svg_pin = (
<svg width={22} height={22} viewBox={dropdownViewBox} version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d={selectedItem.icon} fill="currentColor" stroke="currentColor" strokeWidth={strokeW} strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
);
} else if (selectedItem.useStroke) {
svg_pin = (
<svg width={22} height={22} viewBox={dropdownViewBox} version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d={selectedItem.icon} fill="none" stroke="currentColor" strokeWidth={strokeW} strokeLinecap="round" strokeLinejoin="round"></path>
</svg>
);
} else {
svg_pin = (
<svg width={22} height={22} viewBox={dropdownViewBox} version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d={selectedItem.icon} fill="currentColor" fillRule={selectedItem.fillRule || "nonzero"}></path>
</svg>
);
}
selectedItem.originalIcon = svg_pin;
}
return selectedItem;
};
// Builds the SVG <path .../> string for an icon with the proper fill/stroke attributes.
// color: the fill/stroke color (e.g. "white" for cytoscape nodes, "currentColor" for React)
// Returns an SVG path element string ready to embed inside an <svg> tag.
export const buildIconSvgPath = (iconInfo, color) => {
const iconViewBox = iconInfo.svgViewBox || "0 0 24 24";
// For stroke icons: use proportional stroke width (0.75 for 24x24, 1.5 for 48x48)
// This ensures consistent visual appearance across different viewBox sizes
const strokeW = iconInfo.useStroke
? (iconViewBox === "0 0 48 48" ? "1.5" : "0.75")
: (iconInfo.useFill ? "0" : "1.5");
if (iconInfo.useStroke && iconInfo.useFill) {
return `<path d="${iconInfo.icon}" fill="${color}" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-linejoin="round"/>`;
} else if (iconInfo.useStroke) {
return `<path d="${iconInfo.icon}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-linejoin="round"/>`;
} else {
return `<path d="${iconInfo.icon}" fill="${color}"${iconInfo.fillRule ? ` fill-rule="${iconInfo.fillRule}"` : ""}/>`;
}
};
export const collapseField = (field, inputdata) => {
if (field === undefined || field === null) {
return true
@@ -625,7 +784,7 @@ export const validateJson = (showResult) => {
if (showResult === undefined || showResult === null) {
return {
valid: false,
result: "",
result: showResult,
}
}
@@ -638,7 +797,10 @@ export const validateJson = (showResult) => {
}
if (typeof showResult === "object" || typeof showResult === "array") {
return {
//console.log("Valid!: ", showResult)
// FIXME: Not returning as we want to recurse.
return {
valid: true,
result: showResult,
}
@@ -720,64 +882,85 @@ export const validateJson = (showResult) => {
}
// This is where we start recursing
// Problem: Loops aren't recursed properly.
if (jsonvalid) {
// Check fields if they can be parsed too
try {
for (const [key, value] of Object.entries(result)) {
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
//console.log("CHECKING STRING: ", value)
if (value === null || value === undefined) {
continue
}
const inside_result = validateJson(value)
if (inside_result.valid) {
//console.log("INSIDE RESULT: ", inside_result.result)
try {
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
//console.log("CHECKING STRING: ", value)
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
const inside_result = validateJson(value)
if (inside_result.valid) {
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key] = newres
} else {
result[key] = inside_result.result
}
}
} else {
result[key] = newres
} else {
result[key] = inside_result.result
}
}
} else {
// Usually only reaches here if raw array > dict > value
if (typeof showResult !== "array") {
for (const [subkey, subvalue] of Object.entries(value)) {
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
const inside_result = validateJson(subvalue)
if (inside_result.valid) {
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key][subkey] = newres
} else {
result[key][subkey] = inside_result.result
}
}
}
// Usually only reaches here if raw array > dict > value
if (typeof showResult !== "array") {
for (const [subkey, subvalue] of Object.entries(value)) {
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
const inside_result = validateJson(subvalue)
if (inside_result.valid) {
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key][subkey] = newres
} else {
result[key][subkey] = inside_result.result
}
}
}
}
}
}
}
} else {
console.log("Skip array: ", value)
}
}
} catch (e) {
console.log(`Failed to parse '${key}': '${value}': `, e, JSON.stringify(result))
}
}
} catch (e) {
//console.log("Failed parsing inside json subvalues: ", e)
console.log("Failed parsing inside json subvalues: ", e, result)
}
}
return {
valid: jsonvalid,
//console.log("Result: ", result)
try {
const stringified = JSON.stringify(result)
if ((stringified.startsWith("{") && stringified.endsWith("}")) || (stringified.startsWith("[") && stringified.endsWith("]"))) {
result = JSON.parse(stringified)
jsonvalid = true
}
} catch(e) {
jsonvalid = false
}
const toReturn = {
result: result,
};
valid: jsonvalid,
}
return toReturn
};
//Custom hook for handling styling of the dropzone
const useDropzoneStyles = () => {
const { leftSideBarOpenByClick } = useContext(Context);
const { themeMode, brandColor } = useContext(Context);
const { leftSideBarOpenByClick, themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
return {
return React.useMemo(() => ({
paddingTop: 70,
// minHeight: 1000,
backgroundColor: theme.palette.backgroundColor,
@@ -785,7 +968,7 @@ const useDropzoneStyles = () => {
// maxWidth: window.innerWidth > 1366 ? 1366 : isMobile ? "100%" : 1200,
paddingLeft: leftSideBarOpenByClick ? 200 : 0,
transition: "padding-left 0.3s ease",
};
}), [leftSideBarOpenByClick, theme]);
};
//Wrapper for the dropzone component
@@ -798,8 +981,6 @@ const DropzoneWrapper = memo(({ onDrop, WorkflowView }) => {
);
});
const Workflows2 = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
const location = useLocation();
@@ -891,7 +1072,7 @@ const Workflows2 = (props) => {
const [field1, setField1] = React.useState("");
const [field2, setField2] = React.useState("");
const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/shuffle/workflows")
const [downloadBranch, setDownloadBranch] = React.useState("master");
const [downloadBranch, setDownloadBranch] = React.useState("main");
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] =
React.useState(false);
const [exportModalOpen, setExportModalOpen] = React.useState(false);
@@ -2903,7 +3084,7 @@ const Workflows2 = (props) => {
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
var boxColor = "#FECC00";
var boxColor = "#FFC633";
if (data.is_valid) {
boxColor = "#86c142";
}
@@ -3346,7 +3527,7 @@ const Workflows2 = (props) => {
}
style={{
textDecoration: "none",
color: "inherit",
color: theme.palette.text.primary,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
@@ -3891,7 +4072,7 @@ const Workflows2 = (props) => {
renderCell: (params) => {
const data = params.row.record;
var boxColor = "#FECC00";
var boxColor = "#FFC633";
if (data.is_valid) {
boxColor = "#86c142";
}
@@ -5097,7 +5278,7 @@ const Workflows2 = (props) => {
margin: "auto",
}}>
<Typography variant="h4" color="textPrimary" style={{ marginBottom: 20, paddingLeft: 15, textTransform: 'none', fontFamily: theme.typography?.fontFamily }}>
{currTab === 0 ? "Org" : currTab === 1 ? "Your" : "Discover"} Workflows
{currTab === 0 ? "Org" : currTab === 1 ? "Your" : currTab === 2 ? "Discover" : "Security Bundle" } Workflows
</Typography>
<div style={{ borderBottom: themeMode === "dark" ? "1px solid #808080" : theme.palette.defaultBorder, marginBottom: 30 }}>
@@ -5149,7 +5330,7 @@ const Workflows2 = (props) => {
{backgroundWorkflows.length > 0 &&
<Tab
label={`Background Processes`}
label={`Security Bundle`}
value={4}
style={{
...tabStyle,
@@ -5491,13 +5672,22 @@ const Workflows2 = (props) => {
<Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top">
<IconButton
style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
disabled={isCloud || currTab === 2}
style={(currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
disabled={currTab === 2}
onClick={() => exportAllWorkflows(workflows)}
>
<GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
</IconButton>
</Tooltip>
<Tooltip title="Import workflows from URL" placement="top">
<IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => setLoadWorkflowsModalOpen(true)}
disabled={currTab === 2}
>
<CloudDownloadIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
</IconButton>
</Tooltip>
</div>
<Button
variant="contained"
@@ -5520,48 +5710,18 @@ const Workflows2 = (props) => {
</div>
}
{!isCloud && currentOrg?.old_org ? (
{!isCloud && (currentOrg?.old_org || isProdStatusOn) ? (
<div
style={{
display: "flex",
alignItems: "center",
gap: 20,
padding: "4px 10px",
marginLeft: "5px",
marginRight: "5px",
borderRadius: 20,
marginBottom: "14px",
background: isProdStatusOn
? "rgba(43, 192, 126, 0.1)"
: "rgba(255, 82, 82, 0.1)",
cursor: "pointer",
style={{
position: "absolute",
top: 20,
right: 20,
}}
onClick={() => {
navigate("/admin?admin_tab=billingstats")
}}
>
<span
style={{
width: 8,
height: 8,
marginLeft: 10,
background: isProdStatusOn ? "#2BC07E" : "#FD4C62",
borderRadius: 999,
display: "inline",
}}
<Licensed
licensed={isProdStatusOn}
showBadge={true}
/>
<Typography
style={{
fontFamily: "12px",
opacity: 0.9,
color: isProdStatusOn ? "#2BC07E" : "#FD4C62",
}}
>
{isProdStatusOn ? "Production" : "NOT Production"}
</Typography>
</div>
) : null}
@@ -5812,15 +5972,15 @@ const Workflows2 = (props) => {
const parsedData = {
url: url,
field_3: downloadBranch || "master",
branch: downloadBranch || "master",
};
if (field1.length > 0) {
parsedData["field_1"] = field1;
parsedData["username"] = field1;
}
if (field2.length > 0) {
parsedData["field_2"] = field2;
parsedData["password"] = field2;
}
toast("Getting specific workflows from your URL.");
@@ -5867,17 +6027,28 @@ const Workflows2 = (props) => {
open={loadWorkflowsModalOpen}
onClose={() => { }}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: '440px',
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}
>
<DialogTitle>
<div style={{ color: "rgba(255,255,255,0.9)" }}>
Load workflows from github repo
<div style={{ color: theme.palette.text.primary }}>
Load workflows from a remote repository
<div style={{ float: "right" }}>
<Tooltip color="primary" title={"Import manually"} placement="top">
<Button
@@ -5892,17 +6063,17 @@ const Workflows2 = (props) => {
</div>
</div>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
Repository (supported: github, gitlab, bitbucket)
<DialogContent style={{ color: theme.palette.text.primary }}>
Repository (supported: github, gitlab, bitbucket, azure devops):
<TextField
style={{ backgroundColor: theme.palette.inputColor }}
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.textFieldStyle.color, borderRadius: theme.palette.textFieldStyle.borderRadius, height: theme.palette.textFieldStyle.height, border: theme.palette.textFieldStyle.border, }}
variant="outlined"
margin="normal"
defaultValue={downloadUrl}
InputProps={{
style: {
color: "white",
height: "50px",
color: theme.palette.textFieldStyle.color,
height: theme.palette.textFieldStyle.height,
fontSize: "1em",
},
}}
@@ -5915,34 +6086,36 @@ const Workflows2 = (props) => {
</span>
<div style={{ display: "flex" }}>
<TextField
style={{ backgroundColor: theme.palette.inputColor }}
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.textFieldStyle.color, borderRadius: theme.palette.textFieldStyle.borderRadius, height: theme.palette.textFieldStyle.height, border: theme.palette.textFieldStyle.border, }}
variant="outlined"
margin="normal"
defaultValue={downloadBranch}
InputProps={{
style: {
color: "white",
height: "50px",
color: theme.palette.textFieldStyle.color,
height: theme.palette.textFieldStyle.height,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
fontSize: "1em",
},
}}
onChange={(e) => setDownloadBranch(e.target.value)}
placeholder="master"
placeholder="main"
fullWidth
/>
</div>
<span style={{ marginTop: 10 }}>
Authentication (optional - private repos etc):
Authentication for private repositories (optional):
</span>
<div style={{ display: "flex" }}>
<TextField
style={{ flex: 1, backgroundColor: theme.palette.inputColor }}
style={{ flex: 1, backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.textFieldStyle.color, borderRadius: theme.palette.textFieldStyle.borderRadius, height: theme.palette.textFieldStyle.height, border: theme.palette.textFieldStyle.border, }}
variant="outlined"
margin="normal"
InputProps={{
style: {
color: "white",
height: "50px",
color: theme.palette.textFieldStyle.color,
height: theme.palette.textFieldStyle.height,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
fontSize: "1em",
},
}}
@@ -5952,13 +6125,13 @@ const Workflows2 = (props) => {
fullWidth
/>
<TextField
style={{ flex: 1, backgroundColor: theme.palette.inputColor }}
style={{ flex: 1, backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.textFieldStyle.color, borderRadius: theme.palette.textFieldStyle.borderRadius, height: theme.palette.textFieldStyle.height, border: theme.palette.textFieldStyle.border, }}
variant="outlined"
margin="normal"
InputProps={{
style: {
color: "white",
height: "50px",
color: theme.palette.textFieldStyle.color ,
height: theme.palette.textFieldStyle.height,
fontSize: "1em",
},
}}
@@ -5978,12 +6151,12 @@ const Workflows2 = (props) => {
Cancel
</Button>
<Button
style={{ borderRadius: "0px" }}
variant="contained"
color="primary"
disabled={downloadUrl.length === 0 || !downloadUrl.includes("http")}
onClick={() => {
handleGithubValidation();
}}
color="primary"
>
Submit
</Button>