Schedule Page - New Feature! (#1333)

- Schedule page functionality: Create (modal), delete (modal), view schedule, advanced options
- Replaces Packs tab with Schedules tab
- Updates e2e tests, mocks, stubs, etc
- Defaults logging type to snapshot for packs
- Adds conversion helpers and tests helper functions
- Adds global_scheduled_queries to redux
This commit is contained in:
RachelElysia 2021-07-26 14:41:36 -04:00 committed by GitHub
parent fd23d42300
commit 05691d49ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1813 additions and 82 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 602 B

View File

@ -0,0 +1,71 @@
<svg width="150" height="157" viewBox="0 0 150 157" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M135.923 58.1916C150.177 94.9929 132.469 136.16 96.3717 150.142C60.274 164.123 19.456 145.624 5.20211 108.823C-9.05181 72.0213 8.65602 30.8538 44.7537 16.8724C80.8513 2.89104 121.669 21.3902 135.923 58.1916Z" fill="#F1F0FF"/>
<path d="M44.7096 57.75C32.9735 57.75 23.4596 48.236 23.4596 36.5C23.4596 24.7639 32.9735 15.25 44.7096 15.25C56.4456 15.25 65.9596 24.7639 65.9596 36.5C65.9596 48.236 56.4456 57.75 44.7096 57.75Z" fill="white" stroke="#D66C7B" strokeWidth="1.5" strokeMiterlimit="10"/>
<path opacity="0.2" d="M31.1645 21C27.7058 24.9273 25.8733 30.0256 26.0399 35.257C26.2065 40.4884 28.3597 45.4596 32.0612 49.1587C35.7628 52.8578 40.7343 55.0066 45.9638 55.1677C51.1932 55.3289 56.2875 53.4902 60.2096 50.026C58.3524 52.1349 56.083 53.8407 53.5415 55.0384C50.9999 56.236 48.24 56.9001 45.432 56.9896C42.6241 57.0791 39.8275 56.5921 37.2149 55.5587C34.6023 54.5254 32.229 52.9675 30.2414 50.9812C28.2538 48.9949 26.694 46.6223 25.6583 44.0097C24.6226 41.3972 24.1329 38.6 24.2193 35.7908C24.3058 32.9816 24.9667 30.2199 26.1612 27.6761C27.3557 25.1322 29.0584 22.8602 31.1645 21Z" fill="#D66C7B"/>
<path d="M41.1871 34.427L40.5195 31.9355C40.3728 31.3881 40.0173 30.9207 39.5312 30.6361C39.0452 30.3514 38.4683 30.2729 37.9276 30.4178C37.3869 30.5627 36.9265 30.9191 36.6479 31.4086C36.3693 31.8982 36.2951 32.4807 36.4418 33.0281L36.7527 34.1884L35.5463 34.5117L35.2354 33.3514C35.0452 32.4973 35.1881 31.604 35.6344 30.8578C36.0807 30.1116 36.7959 29.5702 37.6308 29.3465C38.4657 29.1228 39.3558 29.2341 40.1154 29.6571C40.875 30.0802 41.4455 30.7824 41.7078 31.6171L42.3754 34.1086L41.1871 34.427ZM41.0423 34.4658L39.9626 34.7551C39.4826 34.8837 39.0741 35.2001 38.8268 35.6346C38.5794 36.0691 38.5136 36.5861 38.6438 37.072L40.2948 43.2336C40.425 43.7195 40.7405 44.1343 41.1719 44.387C41.6034 44.6396 42.1154 44.7093 42.5953 44.5807L51.215 42.271C51.695 42.1424 52.1035 41.8261 52.3509 41.3916C52.5982 40.9571 52.664 40.44 52.5338 39.9542L50.8779 33.7743C50.7477 33.2884 50.4322 32.8735 50.0007 32.6209C49.5693 32.3683 49.0573 32.2986 48.5774 32.4272L42.2438 34.1243L41.0423 34.4658ZM51.3334 40.2758C51.3768 40.4378 51.3549 40.6101 51.2724 40.7549C51.19 40.8998 51.0538 41.0052 50.8938 41.0481L42.2801 43.3561C42.1201 43.399 41.9495 43.3758 41.8057 43.2916C41.6619 43.2073 41.5567 43.0691 41.5133 42.9071L39.8574 36.7272C39.814 36.5652 39.8359 36.3929 39.9184 36.2481C40.0008 36.1032 40.137 35.9978 40.297 35.9549L48.9167 33.6453C49.0767 33.6024 49.2474 33.6256 49.3912 33.7098C49.535 33.794 49.6401 33.9323 49.6835 34.0943L51.3334 40.2758Z" fill="#D66C7B"/>
<path d="M45.083 36.564C44.9374 36.6021 44.8007 36.6691 44.6811 36.7609C44.5614 36.8528 44.461 36.9678 44.3857 37.0993C44.3104 37.2308 44.2616 37.3762 44.2422 37.5271C44.2228 37.6781 44.2331 37.8316 44.2726 37.9789C44.3235 38.1666 44.4196 38.3389 44.5522 38.4804C44.6849 38.6219 44.8501 38.7282 45.0329 38.7896L45.388 40.1147C45.4314 40.2767 45.5365 40.415 45.6803 40.4992C45.8241 40.5834 45.9948 40.6066 46.1548 40.5638C46.3148 40.5209 46.451 40.4155 46.5334 40.2706C46.6158 40.1258 46.6378 39.9535 46.5944 39.7915L46.2475 38.4969C46.4118 38.3111 46.5122 38.0764 46.5337 37.8275C46.5552 37.5786 46.4968 37.3288 46.367 37.1153C46.2372 36.9017 46.0429 36.7357 45.8131 36.6419C45.5833 36.5481 45.3302 36.5314 45.0912 36.5945L45.083 36.564Z" fill="#D66C7B"/>
<path d="M81.2136 102.973C67.9609 102.973 57.2176 92.2339 57.2176 78.9865C57.2176 65.7391 67.9609 55 81.2136 55C94.4662 55 105.21 65.7391 105.21 78.9865C105.21 92.2339 94.4662 102.973 81.2136 102.973Z" fill="white"/>
<path opacity="0.2" d="M65.319 61.02C61.2863 65.5997 59.1495 71.5449 59.3438 77.6453C59.5381 83.7457 62.0487 89.5427 66.3647 93.8563C70.6806 98.1698 76.4774 100.676 82.575 100.864C88.6726 101.051 94.6126 98.9073 99.1857 94.8677C97.0202 97.3269 94.3741 99.3161 91.4106 100.713C88.4472 102.109 85.2292 102.884 81.9551 102.988C78.6809 103.092 75.4202 102.525 72.3739 101.319C69.3275 100.114 66.5602 98.2978 64.2427 95.9816C61.9252 93.6653 60.1065 90.8986 58.8988 87.852C57.6912 84.8055 57.1202 81.5437 57.2211 78.2678C57.3219 74.992 58.0924 71.7715 59.4852 68.8051C60.878 65.8387 62.8634 63.1892 65.319 61.02Z" fill="#63C740"/>
<path d="M81.2136 102.973C67.9609 102.973 57.2176 92.2339 57.2176 78.9865C57.2176 65.7391 67.9609 55 81.2136 55C94.4662 55 105.21 65.7391 105.21 78.9865C105.21 92.2339 94.4662 102.973 81.2136 102.973Z" stroke="#63C740" strokeWidth="1.4" strokeMiterlimit="10"/>
<path d="M88.892 69.6914H86.0268C85.8922 69.6922 85.7634 69.7463 85.6686 69.8419L79.4296 76.084C78.2471 75.7045 76.9713 75.7338 75.8074 76.1672C74.6435 76.6006 73.659 77.4129 73.0121 78.4736C72.3652 79.5342 72.0935 80.7817 72.2407 82.0154C72.3878 83.2492 72.9452 84.3977 73.8234 85.2763C74.7016 86.1549 75.8495 86.7126 77.0826 86.8598C78.3158 87.007 79.5626 86.7352 80.6227 86.088C81.6829 85.4408 82.4948 84.4558 82.9279 83.2913C83.3611 82.1268 83.3904 80.8504 83.0111 79.6673L83.4409 79.2373L85.0741 78.9076C85.1709 78.8874 85.2598 78.8393 85.3298 78.7693C85.3998 78.6993 85.4478 78.6104 85.468 78.5134L85.7546 77.0801L87.1872 76.7935C87.284 76.7732 87.3729 76.7252 87.4429 76.6552C87.5129 76.5852 87.5609 76.4962 87.5811 76.3993L87.8605 75.009H88.8704C88.9363 75.009 89.0015 74.996 89.0623 74.9708C89.1232 74.9456 89.1785 74.9086 89.225 74.8621C89.2716 74.8155 89.3085 74.7602 89.3337 74.6993C89.3589 74.6385 89.3719 74.5732 89.3719 74.5073V70.2074C89.3758 70.0755 89.3276 69.9474 89.2377 69.8509C89.1479 69.7543 89.0237 69.697 88.892 69.6914ZM87.4594 73.9913C87.3429 73.9893 87.2295 74.0284 87.1391 74.1018C87.0486 74.1752 86.987 74.2782 86.9651 74.3927L86.6714 75.826L85.2388 76.1198C85.1402 76.1379 85.0493 76.1851 84.9778 76.2554C84.9063 76.3256 84.8575 76.4157 84.8377 76.514L84.5512 77.9473L83.1186 78.2411C83.0216 78.2615 82.9322 78.3087 82.8607 78.3773L82.1014 79.1441C82.0321 79.2128 81.9844 79.3003 81.964 79.3958C81.9436 79.4912 81.9516 79.5906 81.9868 79.6816C82.3674 80.6518 82.4024 81.7235 82.0861 82.7165C81.7697 83.7096 81.1212 84.5633 80.2497 85.1342C79.3781 85.7051 78.3367 85.9583 77.3005 85.8513C76.2643 85.7443 75.2965 85.2836 74.5599 84.5466C73.8233 83.8096 73.3628 82.8414 73.2558 81.8046C73.1489 80.7679 73.402 79.7259 73.9726 78.854C74.5432 77.982 75.3965 77.3332 76.389 77.0167C77.3816 76.7002 78.4527 76.7352 79.4225 77.116C79.5133 77.1522 79.6128 77.1606 79.7085 77.1402C79.8041 77.1198 79.8915 77.0714 79.9597 77.0013L86.2774 70.6876H87.71L81.4424 76.9511C81.3496 77.0473 81.2978 77.1758 81.2978 77.3095C81.2978 77.4431 81.3496 77.5716 81.4424 77.6678C81.5386 77.7606 81.667 77.8125 81.8006 77.8125C81.9342 77.8125 82.0626 77.7606 82.1587 77.6678L88.4192 71.397V73.9483L87.4594 73.9913Z" fill="#63C740"/>
<path d="M75.2464 81.7158C75.0367 81.9279 74.8943 82.1973 74.8373 82.4902C74.7803 82.7831 74.8112 83.0863 74.926 83.3617C75.0409 83.637 75.2346 83.8722 75.4828 84.0377C75.731 84.2031 76.0226 84.2914 76.3209 84.2914C76.6191 84.2914 76.9106 84.2031 77.1588 84.0377C77.407 83.8722 77.6008 83.637 77.7156 83.3617C77.8305 83.0863 77.8613 82.7831 77.8043 82.4902C77.7473 82.1973 77.605 81.9279 77.3953 81.7158C77.2548 81.5736 77.0874 81.4608 76.903 81.3838C76.7185 81.3067 76.5207 81.2671 76.3209 81.2671C76.121 81.2671 75.9231 81.3067 75.7387 81.3838C75.5543 81.4608 75.387 81.5736 75.2464 81.7158ZM76.679 83.1491C76.6085 83.2182 76.5191 83.265 76.4221 83.2835C76.3251 83.302 76.2248 83.2915 76.1338 83.2532C76.0428 83.215 75.9651 83.1507 75.9105 83.0684C75.8558 82.9861 75.8266 82.8895 75.8266 82.7907C75.8266 82.692 75.8558 82.5954 75.9105 82.5131C75.9651 82.4308 76.0428 82.3665 76.1338 82.3283C76.2248 82.29 76.3251 82.2795 76.4221 82.298C76.5191 82.3165 76.6085 82.3633 76.679 82.4324C76.7708 82.5295 76.8214 82.6585 76.82 82.7922C76.8187 82.9259 76.7656 83.0538 76.6718 83.1491H76.679Z" fill="#63C740"/>
<g clipPath="url(#clip0)">
<path d="M130.532 95.886H30.238V154.239H130.532V95.886Z" fill="#F79568"/>
<path d="M73.494 95.9587H28.8752V155.261H73.494V95.9587Z" fill="#D1585D"/>
<path d="M132.371 90.0005H26.818L27.7298 95.9591H132.371V90.0005Z" fill="#F79568"/>
<path d="M73.8486 90.0005H26.818L27.7298 95.9591H73.8486V90.0005Z" fill="#D1585D"/>
<path d="M130.055 155.049L130.304 95.8169H131.296L131.155 155.049H130.055Z" fill="#A53942"/>
<path d="M117.545 103.194C130.74 101.137 143.65 112.416 146.345 128.377C149.041 144.337 138.472 152.991 125.277 155.049C99.6695 155.049 98.4636 156.822 98.4636 135.825C98.4636 119.651 104.351 105.251 117.545 103.194Z" fill="#A53942"/>
<rect x="75.5984" y="131.373" width="28.4926" height="18.2353" transform="rotate(-8.13259 75.5984 131.373)" fill="#A53942"/>
<rect x="74.4587" y="131.597" width="28.4926" height="18.2353" transform="rotate(-10.9133 74.4587 131.597)" fill="#192147"/>
<path d="M85.6013 135.502C86.4076 135.363 86.948 134.596 86.8083 133.789C86.6687 132.983 85.9019 132.443 85.0956 132.582C84.2894 132.722 83.7489 133.489 83.8886 134.295C84.0282 135.101 84.795 135.642 85.6013 135.502Z" fill="#63C740"/>
<path d="M89.9809 134.744C90.7872 134.604 91.3276 133.837 91.188 133.031C91.0483 132.225 90.2815 131.684 89.4753 131.824C88.669 131.963 88.1286 132.73 88.2682 133.537C88.4078 134.343 89.1747 134.883 89.9809 134.744Z" fill="#5CABDF"/>
<path d="M94.3606 133.985C95.1668 133.845 95.7073 133.079 95.5676 132.272C95.428 131.466 94.6612 130.926 93.8549 131.065C93.0486 131.205 92.5082 131.972 92.6479 132.778C92.7875 133.584 93.5543 134.125 94.3606 133.985Z" fill="#D66C7B"/>
<path d="M86.3598 139.882C87.1661 139.742 87.7065 138.975 87.5669 138.169C87.4273 137.363 86.6604 136.822 85.8542 136.962C85.0479 137.102 84.5075 137.868 84.6471 138.675C84.7868 139.481 85.5536 140.021 86.3598 139.882Z" fill="#C98DEF"/>
<path d="M90.7395 139.123C91.5457 138.984 92.0862 138.217 91.9465 137.411C91.8069 136.604 91.0401 136.064 90.2338 136.203C89.4275 136.343 88.8871 137.11 89.0268 137.916C89.1664 138.722 89.9332 139.263 90.7395 139.123Z" fill="#FAA669"/>
<path d="M87.1183 144.261C87.9245 144.122 88.4649 143.355 88.3253 142.549C88.1857 141.742 87.4189 141.202 86.6126 141.342C85.8063 141.481 85.2659 142.248 85.4055 143.054C85.5452 143.861 86.312 144.401 87.1183 144.261Z" fill="#3AEFC4"/>
<path fillRule="evenodd" clipRule="evenodd" d="M98.7522 145.484C98.507 142.972 98.4636 139.808 98.4636 135.825C98.4636 132.742 98.6775 129.724 99.1152 126.843L102.436 126.202L105.888 144.108L98.7522 145.484Z" fill="#141A36"/>
<path d="M119.815 102.201C131.52 100.357 144.359 110.359 148.403 124.546C152.446 138.733 146.274 151.786 134.57 153.559C122.866 155.474 110.026 145.472 105.983 131.214C101.868 117.027 108.111 104.046 119.815 102.201Z" fill="#A3D0EF"/>
<path d="M131.378 150.367C122.085 150.367 112.651 141.854 109.388 130.647C107.402 123.766 108.04 117.027 111.161 112.061C113.431 108.444 116.765 106.174 120.738 105.535C121.447 105.393 122.227 105.393 123.008 105.393C132.3 105.393 141.735 113.906 144.998 125.114C146.984 131.994 146.345 138.733 143.224 143.699C140.954 147.317 137.62 149.586 133.648 150.225C132.868 150.296 132.158 150.367 131.378 150.367Z" fill="#6FAADA"/>
<path d="M130.527 150.367C131.094 150.367 131.662 150.296 132.229 150.225C136.273 149.586 139.536 147.317 141.806 143.699C144.927 138.733 145.565 131.994 143.579 125.114C140.458 114.189 131.449 105.89 122.44 105.393C121.873 105.393 121.305 105.464 120.738 105.535C116.694 106.174 113.431 108.444 111.161 112.061C108.04 117.027 107.402 123.766 109.388 130.647C112.509 141.571 121.518 149.87 130.527 150.367Z" fill="white"/>
<path d="M138.401 152.566L135.138 153.913C133.932 154.41 132.655 154.765 131.307 154.978C119.603 156.822 106.763 146.82 102.72 132.562C99.102 120.006 103.571 108.373 112.722 104.613L115.985 103.265C106.834 107.096 102.365 118.658 105.983 131.214C110.026 145.401 122.866 155.403 134.57 153.63C135.918 153.417 137.195 153.062 138.401 152.566Z" fill="#6FAADA"/>
<path d="M138.401 152.566L135.138 153.913C133.932 154.41 132.655 154.765 131.307 154.978C119.603 156.822 106.763 146.82 102.72 132.562C99.102 120.006 103.571 108.373 112.722 104.613L115.985 103.265C106.834 107.096 102.365 118.658 105.983 131.214C110.026 145.401 122.866 155.403 134.57 153.63C135.918 153.417 137.195 153.062 138.401 152.566Z" fill="#6FAADA"/>
<path d="M36.7943 155.848C74.0215 155.149 111.249 154.831 148.476 155.213C150.128 155.213 150.128 154.958 148.476 154.958C111.249 154.006 73.958 153.497 36.7308 153.815C24.0253 153.879 24.0253 156.102 36.7943 155.848Z" fill="#A53942"/>
<path d="M73.1393 154.268C72.9265 154.268 72.7846 153.488 72.7137 152.211C72.7137 151.857 72.6427 151.502 72.6427 151.076C72.6427 150.651 72.6427 150.225 72.6427 149.729C72.6427 149.232 72.7137 148.806 72.7137 148.31C72.7137 147.813 72.7137 147.317 72.7137 146.749C72.7137 146.182 72.7137 145.543 72.7137 144.905C72.7137 144.266 72.7137 143.699 72.7137 142.99C72.7137 142.351 72.7137 141.642 72.7137 140.932C72.7137 140.223 72.7846 139.514 72.7846 138.804C72.7846 138.095 72.7846 137.457 72.7137 136.676C72.7137 136.322 72.6427 135.896 72.6427 135.47C72.6427 135.045 72.6427 134.69 72.6427 134.264C72.7136 133.484 72.7137 132.704 72.7137 131.995C72.7137 131.214 72.6427 130.434 72.6427 129.654C72.5718 126.745 72.5718 123.908 72.5718 121.212C72.5718 120.503 72.5718 119.864 72.5718 119.155C72.5718 118.446 72.5718 117.807 72.6427 117.169C72.6427 116.53 72.6427 115.963 72.6427 115.325C72.6427 114.686 72.6427 114.119 72.5718 113.551C72.5718 112.984 72.5718 112.345 72.5718 111.778C72.5718 111.21 72.6427 110.714 72.6427 110.217C72.6427 110.004 72.6427 109.721 72.6427 109.508C72.6427 109.295 72.6427 109.082 72.6427 108.869C72.6427 108.444 72.6427 108.018 72.6427 107.593C72.6427 106.812 72.6427 106.103 72.7137 105.535C72.7137 104.968 72.7846 104.542 72.7846 104.259C72.7846 103.62 72.8555 103.053 72.8555 102.485C72.8555 101.989 72.9265 101.492 72.9265 100.925C72.9265 100.428 72.9974 99.9314 72.9974 99.4349C73.0683 98.9383 73.1393 98.5127 73.2102 98.158C73.3521 97.4487 73.4939 96.8812 73.6358 96.5265C73.9196 96.1009 73.9905 95.8881 74.0614 95.959C74.1324 95.959 74.2033 96.2427 74.2033 96.6684C74.2033 97.094 74.2033 97.7324 74.1324 98.4418C74.1324 98.7964 74.0614 99.1511 74.0614 99.5767C74.0614 100.002 74.0614 100.428 74.0614 100.925C74.0614 101.421 74.0614 101.989 74.0614 102.627C74.0614 103.194 73.9905 103.762 73.9905 104.4C73.9905 104.684 73.9905 105.11 73.9196 105.677C73.9196 106.245 73.8486 106.883 73.8486 107.734C73.8486 108.16 73.8486 108.515 73.7777 109.011C73.7777 109.153 73.7777 109.224 73.7777 109.366C73.7777 109.508 73.7777 109.579 73.7777 109.721C73.7777 110.004 73.7777 110.217 73.7068 110.501C73.7068 110.997 73.6358 111.494 73.6358 111.991C73.6358 112.274 73.6358 112.487 73.6358 112.771C73.6358 113.055 73.6358 113.338 73.6358 113.622C73.6358 114.19 73.6358 114.757 73.6358 115.395C73.6358 116.034 73.6358 116.672 73.5649 117.311C73.5649 117.949 73.494 118.588 73.494 119.226C73.494 119.864 73.494 120.574 73.494 121.212C73.494 123.908 73.423 126.816 73.3521 129.654C73.3521 130.434 73.3521 131.214 73.3521 131.995C73.3521 132.846 73.2811 133.626 73.2811 134.406C73.2811 134.761 73.2811 135.187 73.2811 135.541C73.2811 135.896 73.2811 136.251 73.3521 136.605C73.423 137.315 73.494 138.095 73.494 138.875C73.494 139.656 73.423 140.365 73.423 141.074C73.423 141.713 73.423 142.351 73.423 143.061C73.423 143.699 73.494 144.408 73.494 145.047C73.494 145.614 73.5649 146.253 73.5649 146.82C73.5649 147.388 73.6358 147.955 73.6358 148.523C73.6358 149.09 73.5649 149.587 73.5649 150.083C73.5649 150.509 73.494 150.934 73.494 151.289C73.494 151.715 73.494 152.069 73.494 152.424C73.494 153.488 73.3521 154.268 73.1393 154.268Z" fill="#A53942"/>
<path d="M56.8523 100.296L54.0149 103.417C53.944 103.488 53.944 103.63 53.944 103.701C54.0149 103.771 54.0858 103.842 54.1568 103.842H56.0011V114.057C56.0011 114.128 56.0011 114.199 56.0721 114.27C56.143 114.341 56.2139 114.341 56.2849 114.341H57.8455C57.9164 114.341 57.9873 114.341 58.0583 114.27C58.1292 114.199 58.1292 114.128 58.1292 114.057V103.913H59.9026C60.0445 103.913 60.1154 103.842 60.1154 103.771C60.1863 103.701 60.1154 103.559 60.0445 103.488L57.1361 100.367C57.0652 100.296 56.9942 100.296 56.9233 100.296C56.9233 100.225 56.9233 100.225 56.8523 100.296Z" fill="#A53942"/>
<path d="M64.6553 99.5864L61.8179 102.708C61.7469 102.779 61.7469 102.92 61.7469 102.991C61.8179 103.062 61.8888 103.133 61.9598 103.133H63.8041V114.057C63.8041 114.128 63.8041 114.199 63.875 114.27C63.946 114.341 64.0169 114.341 64.0878 114.341H65.6484C65.7194 114.341 65.7903 114.341 65.8612 114.27C65.9322 114.199 65.9322 114.128 65.9322 114.057V103.204H67.7056C67.8474 103.204 67.9184 103.133 67.9184 103.062C67.9893 102.991 67.9184 102.849 67.8474 102.779L64.9391 99.6573C64.8681 99.5864 64.7972 99.5864 64.7263 99.5864C64.7263 99.5155 64.7263 99.5155 64.6553 99.5864Z" fill="#A53942"/>
<path d="M29.6205 150.869C29.4786 141.931 29.3367 133.064 29.3367 124.126C29.2658 115.188 29.0123 105.209 29.0123 96.342C29.0123 95.5618 28.8704 95.5618 28.8704 96.342C28.7286 105.28 28.7692 115.188 28.6274 124.126C28.4855 133.064 28.4146 142.002 28.4146 150.94C28.4855 157.04 29.7623 157.04 29.6205 150.869Z" fill="#A53942"/>
<path d="M27.8111 95.4623C27.6692 94.5401 27.3146 93.0505 27.3146 92.1283C27.2436 91.2061 27.0308 90.9224 27.0308 90.0002C27.0308 89.9293 26.8889 89.9293 26.8889 90.0002C26.7471 90.9224 26.818 91.2061 26.7471 92.1283C26.6052 93.0505 26.818 94.5401 26.8889 95.4623C26.818 96.1007 27.882 96.1007 27.8111 95.4623Z" fill="#A53942"/>
<path d="M131.52 90.4258C131.662 91.348 132.016 92.8377 132.016 93.7598C132.087 94.682 132.3 94.9657 132.3 95.8879C132.3 95.9589 132.442 95.9589 132.442 95.8879C132.584 94.9657 132.513 94.682 132.584 93.7598C132.726 92.8377 132.513 91.348 132.442 90.4258C132.513 89.7874 131.449 89.7874 131.52 90.4258Z" fill="#A53942"/>
<path d="M35.0466 90.4968C67.1097 90.1421 99.2437 90.0002 131.307 90.213C132.726 90.213 132.726 90.1421 131.307 90.0712C99.2437 89.6455 67.1097 89.4327 35.0466 89.5037C24.0515 89.5746 24.0515 90.6387 35.0466 90.4968Z" fill="#A53942"/>
<path d="M131.236 95.6043C122.511 95.3915 113.573 95.5334 104.635 95.6043C94.9166 95.6753 85.1984 95.7462 75.4801 95.8171C59.3067 95.8881 44.0555 96.1718 27.8821 95.8881C26.9599 95.8881 26.6761 96.03 27.3855 96.1009C40.2249 97.0231 58.5974 96.4556 72.4299 96.3846C91.7954 96.3137 111.445 96.3137 130.739 96.03C132.513 95.8881 133.08 95.6043 131.236 95.6043Z" fill="#A53942"/>
<path d="M123.291 113.977L127.192 129.015L140.103 132.704" stroke="#B0B0EA" strokeWidth="2.09375" strokeMiterlimit="10"/>
</g>
<ellipse cx="37.5765" cy="138.926" rx="16.2492" ry="18.3954" transform="rotate(-70.2681 37.5765 138.926)" fill="#A53942"/>
<path d="M91.6363 30.8443C83.3698 30.8443 76.6684 24.1567 76.6684 15.9071C76.6684 7.65757 83.3698 0.969971 91.6363 0.969971C99.9028 0.969971 106.604 7.65757 106.604 15.9071C106.604 24.1567 99.9028 30.8443 91.6363 30.8443Z" fill="white"/>
<path opacity="0.2" d="M81.7257 4.71729C79.2568 7.58365 77.9636 11.2812 78.1066 15.0651C78.2495 18.849 79.8181 22.438 82.4961 25.1091C85.1742 27.7801 88.7628 29.3347 92.539 29.4597C96.3151 29.5846 99.9983 28.2707 102.846 25.7825C101.506 27.3381 99.8612 28.6006 98.0133 29.4913C96.1654 30.3819 94.1543 30.8817 92.1052 30.9594C90.0562 31.0372 88.0132 30.6913 86.1033 29.9432C84.1935 29.1952 82.4578 28.061 81.0043 26.6114C79.5509 25.1617 78.4109 23.4277 77.6553 21.5173C76.8996 19.6069 76.5446 17.561 76.6123 15.507C76.6799 13.4531 77.1689 11.4351 78.0486 9.57884C78.9283 7.72254 80.18 6.06765 81.7257 4.71729Z" fill="#C98DEF"/>
<path d="M91.6363 30.8443C83.3698 30.8443 76.6684 24.1567 76.6684 15.9071C76.6684 7.65757 83.3698 0.969971 91.6363 0.969971C99.9028 0.969971 106.604 7.65757 106.604 15.9071C106.604 24.1567 99.9028 30.8443 91.6363 30.8443Z" stroke="#C98DEF" strokeWidth="1.3" strokeMiterlimit="10"/>
<path d="M97.5081 19.1686L95.6738 17.4132C96.162 16.6246 96.4119 15.7111 96.3935 14.7832C96.3935 13.6785 96.0666 12.5986 95.4542 11.6801C94.8418 10.7616 93.9714 10.0457 92.9531 9.62293C91.9347 9.20017 90.8141 9.08956 89.7331 9.30508C88.652 9.5206 87.659 10.0526 86.8795 10.8337C86.1001 11.6148 85.5693 12.6101 85.3543 13.6936C85.1392 14.777 85.2496 15.9001 85.6714 16.9207C86.0932 17.9413 86.8076 18.8137 87.7241 19.4274C88.6406 20.0411 89.7181 20.3687 90.8204 20.3687C91.7096 20.3735 92.5857 20.154 93.3681 19.7304L95.1961 21.4858C95.5075 21.7682 95.9153 21.9199 96.3352 21.9094C96.755 21.8988 97.1548 21.727 97.4518 21.4294C97.7488 21.1317 97.9202 20.7311 97.9307 20.3103C97.9412 19.8895 97.7899 19.4808 97.5081 19.1686ZM86.2026 14.7832C86.2026 13.8679 86.4734 12.9731 86.9808 12.2121C87.4882 11.451 88.2094 10.8578 89.0532 10.5075C89.897 10.1573 90.8255 10.0656 91.7212 10.2442C92.617 10.4228 93.4398 10.8635 94.0856 11.5108C94.7314 12.158 95.1712 12.9826 95.3493 13.8804C95.5275 14.7781 95.4361 15.7086 95.0866 16.5543C94.7371 17.3999 94.1452 18.1227 93.3858 18.6312C92.6264 19.1398 91.7337 19.4112 90.8204 19.4112C89.5962 19.4095 88.4226 18.9214 87.557 18.0538C86.6914 17.1863 86.2043 16.0101 86.2026 14.7832ZM96.8712 20.8475C96.7383 20.9621 96.5688 21.0251 96.3935 21.0251C96.2182 21.0251 96.0487 20.9621 95.9158 20.8475L94.2407 19.2516C94.5963 18.972 94.9171 18.6506 95.1961 18.2941L96.8712 19.89C96.9855 20.0231 97.0484 20.193 97.0484 20.3687C97.0484 20.5444 96.9855 20.7143 96.8712 20.8475Z" fill="#AE6DDF"/>
<path d="M92.9156 12.626L90.3679 15.7411L88.6928 14.541C88.5973 14.4674 88.4766 14.4347 88.3572 14.4503C88.2377 14.4659 88.1293 14.5283 88.0558 14.624C87.9824 14.7196 87.9498 14.8406 87.9653 14.9603C87.9809 15.0801 88.0432 15.1887 88.1386 15.2623L90.5271 17.0178L93.629 13.1877C93.6716 13.1434 93.7038 13.0902 93.7234 13.0318C93.7429 12.9735 93.7492 12.9115 93.7419 12.8504C93.7346 12.7893 93.7138 12.7306 93.6811 12.6785C93.6483 12.6264 93.6044 12.5823 93.5525 12.5494C93.5078 12.5077 93.4546 12.4764 93.3965 12.4574C93.3384 12.4385 93.277 12.4325 93.2164 12.4398C93.1557 12.4471 93.0974 12.4675 93.0455 12.4997C92.9936 12.5318 92.9492 12.5749 92.9156 12.626Z" fill="#AE6DDF"/>
<circle cx="33.7749" cy="135.665" r="20.2849" fill="#F1F0FF" stroke="white" strokeWidth="0.569853"/>
<path fillRule="evenodd" clipRule="evenodd" d="M21.6316 119.772C24.9984 117.196 29.2079 115.665 33.7749 115.665C44.8206 115.665 53.7749 124.619 53.7749 135.665C53.7749 140.232 52.2443 144.441 49.6681 147.808C46.3012 150.384 42.0918 151.915 37.5248 151.915C26.4791 151.915 17.5248 142.96 17.5248 131.915C17.5248 127.348 19.0554 123.139 21.6316 119.772Z" fill="white"/>
<path fillRule="evenodd" clipRule="evenodd" d="M21.6316 120.272C24.9984 117.696 29.2079 116.165 33.7749 116.165C44.8206 116.165 53.7749 125.119 53.7749 136.165C53.7749 140.732 52.2443 144.941 49.6681 148.308C46.3012 150.884 42.0918 152.415 37.5248 152.415C26.4791 152.415 17.5248 143.46 17.5248 132.415C17.5248 127.848 19.0554 123.639 21.6316 120.272Z" fill="white"/>
<g clipPath="url(#clip1)">
<path d="M43.745 125.727V130.69L38.7561 135.653V130.678L43.745 125.714" fill="#A299F8"/>
<path d="M33.7749 125.727V130.69L38.7638 135.653V130.678L33.7749 125.714" fill="#03155B"/>
<path d="M43.7527 145.585H38.7638L33.7749 140.621H38.7638L43.7527 145.585Z" fill="#A299F8"/>
<path d="M43.7527 135.658H38.7638L33.7749 140.622H38.7638L43.7527 135.658Z" fill="#03155B"/>
<path d="M23.8088 145.593V140.63L28.7977 135.666V140.65L23.8088 145.614" fill="#A299F8"/>
<path d="M33.775 145.593V140.63L28.7861 135.666V140.65L33.775 145.614" fill="#03155B"/>
<path d="M23.7974 125.736H28.7863L33.7752 130.7H28.7863L23.7974 125.736Z" fill="#A299F8"/>
<path d="M23.7971 135.658H28.786L33.7749 130.695H28.786L23.7971 135.658Z" fill="#03155B"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="136.765" height="66.538" fill="white" transform="translate(12.9146 89.5037)"/>
</clipPath>
<clipPath id="clip1">
<rect width="20" height="20" fill="white" transform="translate(23.775 125.665)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1 @@
* New Schedule page feature allows users to create and remove queries from a global schedule applying to all hosts

View File

@ -46,6 +46,55 @@ describe("Query flow", () => {
cy.findByText(/query updated/i).should("be.visible");
// Test Schedules
cy.visit("/schedule/manage");
cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting
cy.findByRole("button", { name: /schedule a query/i }).click();
cy.findByText(/select query/i).click();
cy.findByText(/query all window crashes/i).click();
cy.get(
".schedule-editor-modal__form-field--frequency > .dropdown__select"
).click();
cy.findByText(/every week/i).click();
cy.findByText(/show advanced options/i).click();
cy.get(
".schedule-editor-modal__form-field--logging > .dropdown__select"
).click();
cy.findByText(/ignore removals/i).click();
cy.get(".schedule-editor-modal__form-field--shard > .input-field")
.click()
.type("50");
cy.get(".schedule-editor-modal__btn-wrap")
.contains("button", /schedule/i)
.click();
cy.visit("/schedule/manage");
cy.findByText(/query all window crashes/i).should("exist");
// Checkbox won't check so can't test remove schedule
// cy.get("tbody").get(".table-checkbox__input").click();
// cy.findByRole("button", { name: /remove query/i }).click();
// cy.get(".remove-scheduled-query-modal__btn-wrap")
// .contains("button", /remove/i)
// .click();
// cy.findByText(/query all window crashes/i).should("not.exist");
// End Test Schedules
cy.visit("/queries/manage");
// This element has no label, text, or role

View File

@ -66,6 +66,9 @@ describe("Basic tier - Admin user", () => {
// On the Packs pages (manage, new, and edit), they should…
// ^^General admin functionality for packs page is being tested in app/packflow.spec.ts
// On the Schedule pages (manage, new, and edit), they should…
// ^^General admin functionality for packs page is being tested in app/queryflow.spec.ts
// On the Settings pages, they should…
// See the “Teams” navigation item and access the Settings - Teams page
cy.visit("/settings/organization");

View File

@ -70,5 +70,9 @@ describe("Basic tier - Maintainer user", () => {
cy.findByText(/Label name, host name, IP address, etc./i).click();
cy.findByText(/teams/i).should("exist");
});
// On the Packs pages (manage, new, and edit), they should…
// On the Schedule pages (manage, new, and edit), they should…
// ^^General maintainer functionality for packs page is being tested in core/maintainer.spec.ts
});
});

View File

@ -57,8 +57,15 @@ describe("Basic tier - Observer user", () => {
cy.findByText("All hosts which have enrolled in Fleet").should("exist");
cy.findByText("Packs").should("not.exist");
cy.findByText("Settings").should("not.exist");
// Nav restrictions
cy.findByText(/settings/i).should("not.exist");
cy.findByText(/schedule/i).should("not.exist");
cy.visit("/settings/organization");
cy.findByText(/you do not have permissions/i).should("exist");
cy.visit("/packs/manage");
cy.findByText(/you do not have permissions/i).should("exist");
cy.visit("/schedule/manage");
cy.findByText(/you do not have permissions/i).should("exist");
cy.contains(".table-container .data-table__table th", "Team").should(
"be.visible"

View File

@ -27,9 +27,15 @@ describe("Basic tier - Team observer/maintainer user", () => {
// See the “Teams” column in the Hosts table
// cy.get("thead").contains(/team/i).should("exist");
// NOT see the “Packs” and “Settings” navigation items
cy.findByText(/packs/i).should("not.exist");
// Nav restrictions
cy.findByText(/settings/i).should("not.exist");
cy.findByText(/schedule/i).should("not.exist");
cy.visit("/settings/organization");
cy.findByText(/you do not have permissions/i).should("exist");
cy.visit("/packs/manage");
cy.findByText(/you do not have permissions/i).should("exist");
cy.visit("/schedule/manage");
cy.findByText(/you do not have permissions/i).should("exist");
// NOT see and select "add new label"
cy.findByRole("button", { name: /new label/i }).should("not.exist");
@ -103,6 +109,15 @@ describe("Basic tier - Team observer/maintainer user", () => {
cy.login("marco@organization.com", "user123#");
cy.visit("/");
// Ensure page is loaded and appropriate nav links are displayed
cy.contains("All hosts");
cy.get("nav").within(() => {
cy.findByText(/hosts/i).should("exist");
cy.findByText(/queries/i).should("exist");
cy.findByText(/schedule/i).should("not.exist");
cy.findByText(/settings/i).should("not.exist");
});
// Ensure page is loaded and appropriate nav links are displayed
cy.contains("All hosts");
cy.get("nav").within(() => {

View File

@ -28,7 +28,7 @@ describe("Core tier - Admin user", () => {
cy.get("nav").within(() => {
cy.findByText(/hosts/i).should("exist");
cy.findByText(/queries/i).should("exist");
cy.findByText(/packs/i).should("exist");
cy.findByText(/schedule/i).should("exist");
cy.findByText(/settings/i).should("exist");
});

View File

@ -123,5 +123,8 @@ describe("Core tier - Maintainer user", () => {
cy.findByText(/successfully deleted/i).should("be.visible");
cy.findByText(/server errors/i).should("not.exist");
// Schedule page: Can create, edit, remove a schedule
// TODO: Copy flow from queryflow.spec.ts here to ensure maintainers have access
});
});

View File

@ -21,11 +21,13 @@ describe("Core tier - Observer user", () => {
// Nav restrictions
cy.findByText(/settings/i).should("not.exist");
cy.findByText(/packs/i).should("not.exist");
cy.findByText(/schedule/i).should("not.exist");
cy.visit("/settings/organization");
cy.findByText(/you do not have permissions/i).should("exist");
cy.visit("/packs/manage");
cy.findByText(/you do not have permissions/i).should("exist");
cy.visit("/schedule/manage");
cy.findByText(/you do not have permissions/i).should("exist");
// Host manage page: No team UI, cannot add host or label
cy.visit("/hosts/manage");

View File

@ -55,11 +55,8 @@
.active-selection {
position: absolute;
top: -1px;
top: 0px;
width: 100%;
max-width: calc(
100vw - 2rem - 2rem - 0.125rem - 300px
); // same as wrapper page for flex layout
border: 0;
&__container {

View File

@ -8,6 +8,12 @@ import Form from "components/forms/Form";
import formFieldInterface from "interfaces/form_field";
import InputField from "components/forms/fields/InputField";
import validate from "components/forms/ConfigurePackQueryForm/validate";
import {
FREQUENCY_DROPDOWN_OPTIONS,
PLATFORM_OPTIONS,
LOGGING_TYPE_OPTIONS,
MIN_OSQUERY_VERSION_OPTIONS,
} from "utilities/constants";
const baseClass = "configure-pack-query-form";
const fieldNames = [
@ -18,47 +24,6 @@ const fieldNames = [
"shard",
"version",
];
const platformOptions = [
{ label: "All", value: "" },
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
];
const loggingTypeOptions = [
{ label: "Differential", value: "differential" },
{
label: "Differential (Ignore Removals)",
value: "differential_ignore_removals",
},
{ label: "Snapshot", value: "snapshot" },
];
const minOsqueryVersionOptions = [
{ label: "All", value: "" },
{ label: "4.7.0 +", value: "4.7.0" },
{ label: "4.6.0 +", value: "4.6.0" },
{ label: "4.5.1 +", value: "4.5.1" },
{ label: "4.5.0 +", value: "4.5.0" },
{ label: "4.4.0 +", value: "4.4.0" },
{ label: "4.3.0 +", value: "4.3.0" },
{ label: "4.2.0 +", value: "4.2.0" },
{ label: "4.1.2 +", value: "4.1.2" },
{ label: "4.1.1 +", value: "4.1.1" },
{ label: "4.1.0 +", value: "4.1.0" },
{ label: "4.0.2 +", value: "4.0.2" },
{ label: "4.0.1 +", value: "4.0.1" },
{ label: "4.0.0 +", value: "4.0.0" },
{ label: "3.4.0 +", value: "3.4.0" },
{ label: "3.3.2 +", value: "3.3.2" },
{ label: "3.3.1 +", value: "3.3.1" },
{ label: "3.2.6 +", value: "3.2.6" },
{ label: "2.2.1 +", value: "2.2.1" },
{ label: "2.2.0 +", value: "2.2.0" },
{ label: "2.1.2 +", value: "2.1.2" },
{ label: "2.1.1 +", value: "2.1.1" },
{ label: "2.0.0 +", value: "2.0.0" },
{ label: "1.8.2 +", value: "1.8.2" },
{ label: "1.8.1 +", value: "1.8.1" },
];
export class ConfigurePackQueryForm extends Component {
static propTypes = {
@ -134,15 +99,19 @@ export class ConfigurePackQueryForm extends Component {
const { fields, handleSubmit } = this.props;
const { handlePlatformChoice, renderCancelButton } = this;
// Uncontrolled form field defaults to snapshot if !fields.logging_type
const loggingType = fields.logging_type.value || "snapshot";
return (
<form className={baseClass} onSubmit={handleSubmit}>
<h2 className={`${baseClass}__title`}>Configuration</h2>
<div className={`${baseClass}__fields`}>
<Dropdown
{...fields.logging_type}
options={loggingTypeOptions}
options={LOGGING_TYPE_OPTIONS}
placeholder="- - -"
label="Logging"
value={loggingType}
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
/>
<InputField
@ -155,7 +124,7 @@ export class ConfigurePackQueryForm extends Component {
/>
<Dropdown
{...fields.platform}
options={platformOptions}
options={PLATFORM_OPTIONS}
placeholder="- - -"
label="Platform"
onChange={handlePlatformChoice}
@ -164,7 +133,7 @@ export class ConfigurePackQueryForm extends Component {
/>
<Dropdown
{...fields.version}
options={minOsqueryVersionOptions}
options={MIN_OSQUERY_VERSION_OPTIONS}
placeholder="- - -"
label="Minimum osquery version"
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}

View File

@ -75,7 +75,7 @@ describe("ConfigurePackQueryForm - component", () => {
expect(spy).toHaveBeenCalledWith({
interval: 123,
logging_type: "differential",
logging_type: "snapshot",
platform: "",
query_id: 1,
version: "",

View File

@ -17,9 +17,7 @@ const validate = (formData) => {
errors.interval = "Frequency must be a number";
}
if (!formData.logging_type) {
errors.logging_type = "A Logging Type must be selected";
}
// logging_type does not need to be validated because it is defaulted "snapshot" if unspecified.
if (formData.shard) {
if (formData.shard < 0 || formData.shard > 100) {

View File

@ -0,0 +1,16 @@
import React from "react";
import PropTypes from "prop-types";
class SchedulePageWrapper extends React.Component {
static propTypes = {
children: PropTypes.node,
};
render() {
const { children } = this.props;
return children || null;
}
}
export default SchedulePageWrapper;

View File

@ -0,0 +1 @@
export { default } from "./SchedulePageWrapper";

View File

@ -23,7 +23,7 @@ const ScheduleQuerySidePanel = ({
return false;
}
const formData = selectedScheduledQuery || {};
const formData = selectedScheduledQuery || { logging_type: "snapshot" };
formData.query_id = selectedQuery.id;

View File

@ -48,11 +48,11 @@ export default (currentUser) => {
const globalMaintainerNavItems = [
{
icon: "packs",
name: "Packs",
name: "Schedule",
iconName: "packs",
location: {
regex: new RegExp(`^${URL_PREFIX}/packs/`),
pathname: PATHS.MANAGE_PACKS,
regex: new RegExp(`^${URL_PREFIX}/(schedule|packs)/`),
pathname: PATHS.MANAGE_SCHEDULE,
},
},
];

View File

@ -1,4 +1,6 @@
import { IPack } from "interfaces/pack";
import { IScheduledQuery } from "interfaces/scheduled_query";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
export default {
CHANGE_PASSWORD: "/v1/fleet/change_password",
@ -12,6 +14,7 @@ export default {
return `/v1/fleet/users/${id}/enable`;
},
FORGOT_PASSWORD: "/v1/fleet/forgot_password",
GLOBAL_SCHEDULE: "/v1/fleet/global/schedule",
HOSTS: "/v1/fleet/hosts",
HOSTS_TRANSFER: "/v1/fleet/hosts/transfer",
HOSTS_TRANSFER_BY_FILTER: "/v1/fleet/hosts/transfer/filter",

View File

@ -0,0 +1,65 @@
import endpoints from "fleet/endpoints";
import helpers from "fleet/helpers";
export default (client) => {
return {
create: (formData) => {
const { GLOBAL_SCHEDULE } = endpoints;
const {
interval,
logging_type: loggingType,
platform,
query_id: queryID,
shard,
version,
} = formData;
const removed = loggingType === "differential";
const snapshot = loggingType === "snapshot";
const params = {
interval: Number(interval),
platform,
query_id: Number(queryID),
removed,
snapshot,
shard: Number(shard),
version,
};
return client
.authenticatedPost(
client._endpoint(GLOBAL_SCHEDULE),
JSON.stringify(params)
)
.then((response) => response.scheduled);
},
destroy: ({ id }) => {
const { GLOBAL_SCHEDULE } = endpoints;
const endpoint = `${client._endpoint(GLOBAL_SCHEDULE)}/${id}`;
return client.authenticatedDelete(endpoint);
},
loadAll: () => {
const { GLOBAL_SCHEDULE } = endpoints;
const globalScheduledQueryPath = GLOBAL_SCHEDULE;
return client
.authenticatedGet(client._endpoint(globalScheduledQueryPath))
.then((response) => response.global_schedule);
},
update: (globalScheduledQuery, updatedAttributes) => {
const { GLOBAL_SCHEDULE } = endpoints;
const endpoint = client._endpoint(
`${GLOBAL_SCHEDULE}/${globalScheduledQuery.id}`
);
const params = helpers.formatGlobalScheduledQueryForServer(
updatedAttributes
);
return client
.authenticatedPatch(endpoint, JSON.stringify(params))
.then((response) => response.scheduled);
},
};
};

View File

@ -0,0 +1,80 @@
import nock from "nock";
import Fleet from "fleet";
import mocks from "test/mocks";
import { globalScheduledQueryStub } from "test/stubs";
const { globalScheduledQueries: globalScheduledQueryMocks } = mocks;
describe("Fleet - API client (global scheduled queries)", () => {
afterEach(() => {
nock.cleanAll();
Fleet.setBearerToken(null);
});
const bearerToken = "valid-bearer-token";
describe("#create", () => {
it("calls the appropriate endpoint with the correct parameters", () => {
const formData = {
interval: 60,
logging_type: "differential",
platform: "darwin",
query_id: 2,
shard: 12,
};
const request = globalScheduledQueryMocks.create.valid(
bearerToken,
formData
);
Fleet.setBearerToken(bearerToken);
return Fleet.globalScheduledQueries.create(formData).then(() => {
expect(request.isDone()).toEqual(true);
});
});
});
describe("#destroy", () => {
it("calls the appropriate endpoint with the correct parameters", () => {
const scheduledQuery = { id: 1 };
const request = globalScheduledQueryMocks.destroy.valid(
bearerToken,
scheduledQuery
);
Fleet.setBearerToken(bearerToken);
return Fleet.globalScheduledQueries.destroy(scheduledQuery).then(() => {
expect(request.isDone()).toEqual(true);
});
});
});
describe("#loadAll", () => {
it("calls the appropriate endpoint with the correct parameters", () => {
const request = globalScheduledQueryMocks.loadAll.valid(bearerToken);
Fleet.setBearerToken(bearerToken);
return Fleet.globalScheduledQueries.loadAll().then(() => {
expect(request.isDone()).toEqual(true);
});
});
});
describe("#update", () => {
it("calls the appropriate endpoint with the correct parameters", () => {
const updatedAttrs = { interval: 200 };
const request = globalScheduledQueryMocks.update.valid(
bearerToken,
globalScheduledQueryStub,
updatedAttrs
);
Fleet.setBearerToken(bearerToken);
return Fleet.globalScheduledQueries
.update(globalScheduledQueryStub, updatedAttrs)
.then(() => {
expect(request.isDone()).toEqual(true);
});
});
});
});

View File

@ -8,7 +8,7 @@ const label2 = { id: 2, target_type: "labels" };
const host1 = { id: 6, target_type: "hosts" };
const host2 = { id: 5, target_type: "hosts" };
describe("Kolide API - helpers", () => {
describe("Fleet API - helpers", () => {
describe("#labelSlug", () => {
it("creates a slug for the label", () => {
expect(helpers.labelSlug({ name: "All Hosts" })).toEqual("all-hosts");
@ -221,3 +221,19 @@ describe("redux app node - helpers", () => {
});
});
});
describe("conversion - helpers", () => {
describe("#secondsConversions", () => {
const { secondsToHms, secondsToDhms } = helpers;
it("creates correct conversion to hour-minute-second", () => {
expect(secondsToHms(10861)).toEqual("3 hrs 1 min 1 sec");
expect(secondsToHms(3723)).toEqual("1 hr 2 mins 3 secs");
});
it("creates correct conversion to day-hour-minute-second", () => {
expect(secondsToDhms(270061)).toEqual("3 days 3 hours 1 minute 1 second");
expect(secondsToDhms(90123)).toEqual("1 day 1 hour 2 minutes 3 seconds");
});
});
});

View File

@ -274,6 +274,66 @@ export const formatScheduledQueryForClient = (scheduledQuery: any): any => {
return scheduledQuery;
};
export const formatGlobalScheduledQueryForServer = (scheduledQuery: any) => {
const {
interval,
logging_type: loggingType,
platform,
query_id: queryID,
shard,
} = scheduledQuery;
const result = omit(scheduledQuery, ["logging_type"]);
if (platform === "all") {
result.platform = "";
}
if (interval) {
result.interval = Number(interval);
}
if (loggingType) {
result.removed = loggingType === "differential";
result.snapshot = loggingType === "snapshot";
}
if (queryID) {
result.query_id = Number(queryID);
}
if (shard) {
result.shard = Number(shard);
}
return result;
};
export const formatGlobalScheduledQueryForClient = (
scheduledQuery: any
): any => {
if (scheduledQuery.platform === "") {
scheduledQuery.platform = "all";
}
if (scheduledQuery.snapshot) {
scheduledQuery.logging_type = "snapshot";
} else {
scheduledQuery.snapshot = false;
if (scheduledQuery.removed === false) {
scheduledQuery.logging_type = "differential_ignore_removals";
} else {
// If both are unset, we should default to differential (like osquery does)
scheduledQuery.logging_type = "differential";
}
}
if (scheduledQuery.shard === null) {
scheduledQuery.shard = undefined;
}
return scheduledQuery;
};
export const formatTeamForClient = (team: any): any => {
if (team.display_text === undefined) {
team.display_text = team.name;
@ -369,10 +429,26 @@ export const secondsToHms = (d: number): string => {
const hDisplay = h > 0 ? h + (h === 1 ? " hr " : " hrs ") : "";
const mDisplay = m > 0 ? m + (m === 1 ? " min " : " mins ") : "";
const sDisplay = s > 0 ? s + (s === 1 ? " sec " : " secs ") : "";
const sDisplay = s > 0 ? s + (s === 1 ? " sec" : " secs") : "";
return hDisplay + mDisplay + sDisplay;
};
export const secondsToDhms = (d: number): string => {
if (d === 604800) {
return "1 week";
}
const day = Math.floor(d / (3600 * 24));
const h = Math.floor((d % (3600 * 24)) / 3600);
const m = Math.floor((d % 3600) / 60);
const s = Math.floor(d % 60);
const dDisplay = day > 0 ? day + (day === 1 ? " day " : " days ") : "";
const hDisplay = h > 0 ? h + (h === 1 ? " hour " : " hours ") : "";
const mDisplay = m > 0 ? m + (m === 1 ? " minute " : " minutes ") : "";
const sDisplay = s > 0 ? s + (s === 1 ? " second" : " seconds") : "";
return dDisplay + hDisplay + mDisplay + sDisplay;
};
export const syntaxHighlight = (json: JSON): string => {
let jsonStr: string = JSON.stringify(json, undefined, 2);
jsonStr = jsonStr
@ -407,6 +483,8 @@ export default {
formatLabelResponse,
formatScheduledQueryForClient,
formatScheduledQueryForServer,
formatGlobalScheduledQueryForClient,
formatGlobalScheduledQueryForServer,
formatSelectedTargetsForApi,
humanHostUptime,
humanHostLastSeen,
@ -416,6 +494,7 @@ export default {
hostTeamName,
humanQueryLastRun,
secondsToHms,
secondsToDhms,
labelSlug,
setupData,
frontendFormattedConfig,

View File

@ -10,6 +10,7 @@ import labelMethods from "fleet/entities/labels";
import packMethods from "fleet/entities/packs";
import queryMethods from "fleet/entities/queries";
import scheduledQueryMethods from "fleet/entities/scheduled_queries";
import globalScheduledQueryMethods from "fleet/entities/global_scheduled_queries";
import sessionMethods from "fleet/entities/sessions";
import statusLabelMethods from "fleet/entities/status_labels";
import targetMethods from "fleet/entities/targets";
@ -34,6 +35,7 @@ class Fleet extends Base {
this.packs = packMethods(this);
this.queries = queryMethods(this);
this.scheduledQueries = scheduledQueryMethods(this);
this.globalScheduledQueries = globalScheduledQueryMethods(this);
this.sessions = sessionMethods(this);
this.statusLabels = statusLabelMethods(this);
this.targets = targetMethods(this);

View File

@ -0,0 +1,23 @@
import PropTypes from "prop-types";
export default PropTypes.shape({
id: PropTypes.number.isRequired,
interval: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
shard: PropTypes.number,
query: PropTypes.string.isRequired,
query_id: PropTypes.number.isRequired,
removed: PropTypes.bool,
snapshot: PropTypes.bool,
});
export interface IGlobalScheduledQuery {
id: number;
interval: number;
name: string;
shard?: number;
query: string;
query_id: number;
removed: boolean;
snapshot: boolean;
}

View File

@ -136,7 +136,11 @@ const MembersPage = (props: IMembersPageProps): JSX.Element => {
renderFlash("success", `Successfully removed ${userEditing?.name}`)
);
})
.catch(() => dispatch(renderFlash("error", "Remove failed")));
.catch(() =>
dispatch(
renderFlash("error", "Unable to remove members. Please try again.")
)
);
toggleRemoveMemberModal();
}, [
dispatch,

View File

@ -5,7 +5,7 @@ import classnames from "classnames";
import { Link } from "react-router";
import ReactTooltip from "react-tooltip";
import { isEmpty, noop, pick, reduce, filter, includes } from "lodash";
import { isEmpty, noop, pick, reduce } from "lodash";
import simpleSearch from "utilities/simple_search";
import Spinner from "components/loaders/Spinner";
import Button from "components/buttons/Button";

View File

@ -11,6 +11,13 @@
margin-left: 6px;
}
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
&__modal {
@include position(absolute, 22px null null null);
background-color: $core-white;

View File

@ -7,14 +7,12 @@ import Button from "components/buttons/Button";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import { ITeam } from "interfaces/team";
import { IHost } from "interfaces/host";
interface ITransferHostModal {
isGlobalAdmin: boolean;
teams: ITeam[];
onSubmit: (team: ITeam) => void;
onCancel: () => void;
host: IHost;
}
interface INoTeamOption {
@ -33,7 +31,6 @@ const TransferHostModal = ({
onSubmit,
teams,
isGlobalAdmin,
host,
}: ITransferHostModal): JSX.Element => {
const [selectedTeam, setSelectedTeam] = useState<ITeam | INoTeamOption>();

View File

@ -18,6 +18,7 @@ import ScheduleQuerySidePanel from "components/side_panels/ScheduleQuerySidePane
import packInterface from "interfaces/pack";
import queryActions from "redux/nodes/entities/queries/actions";
import queryInterface from "interfaces/query";
import scheduledQueryInterface from "interfaces/scheduled_query";
import ScheduledQueriesListWrapper from "components/queries/ScheduledQueriesListWrapper";
import { renderFlash } from "redux/nodes/notifications/actions";
import scheduledQueryActions from "redux/nodes/entities/scheduled_queries/actions";
@ -25,7 +26,6 @@ import stateEntityGetter from "redux/utilities/entityGetter";
import PATHS from "router/paths";
const baseClass = "edit-pack-page";
export class EditPackPage extends Component {
static propTypes = {
allQueries: PropTypes.arrayOf(queryInterface),
@ -38,7 +38,7 @@ export class EditPackPage extends Component {
packID: PropTypes.string,
packLabels: PropTypes.arrayOf(labelInterface),
packTeams: PropTypes.arrayOf(teamInterface),
scheduledQueries: PropTypes.arrayOf(queryInterface),
scheduledQueries: PropTypes.arrayOf(scheduledQueryInterface),
isBasicTier: PropTypes.bool,
};
@ -232,6 +232,7 @@ export class EditPackPage extends Component {
handleConfigurePackQuerySubmit = (formData) => {
const { create } = scheduledQueryActions;
const { dispatch, packID } = this.props;
const scheduledQueryData = {
...formData,
pack_id: packID,

View File

@ -0,0 +1,220 @@
import React, { useState, useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { push } from "react-router-redux";
import { IQuery } from "interfaces/query";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
// @ts-ignore
import globalScheduledQueryActions from "redux/nodes/entities/global_scheduled_queries/actions";
// @ts-ignore
import queryActions from "redux/nodes/entities/queries/actions";
// @ts-ignore
import { renderFlash } from "redux/nodes/notifications/actions";
import paths from "router/paths";
import Button from "components/buttons/Button";
import ScheduleError from "./components/ScheduleError";
import ScheduleListWrapper from "./components/ScheduleListWrapper";
import ScheduleEditorModal from "./components/ScheduleEditorModal";
import RemoveScheduledQueryModal from "./components/RemoveScheduledQueryModal";
const baseClass = "manage-schedule-page";
const renderTable = (
onRemoveScheduledQueryClick: React.MouseEventHandler<HTMLButtonElement>,
allGlobalScheduledQueriesList: IGlobalScheduledQuery[],
allGlobalScheduledQueriesError: any,
toggleScheduleEditorModal: () => void
): JSX.Element => {
if (Object.keys(allGlobalScheduledQueriesError).length !== 0) {
return <ScheduleError />;
}
return (
<ScheduleListWrapper
onRemoveScheduledQueryClick={onRemoveScheduledQueryClick}
allGlobalScheduledQueriesList={allGlobalScheduledQueriesList}
toggleScheduleEditorModal={toggleScheduleEditorModal}
/>
);
};
interface IRootState {
entities: {
global_scheduled_queries: {
isLoading: boolean;
data: IGlobalScheduledQuery[];
errors: any;
};
queries: {
isLoading: boolean;
data: IQuery[];
};
};
}
interface IFormData {
interval: number;
name?: string;
shard: number;
query?: string;
query_id?: number;
logging_type: string;
platform: string;
version: string;
}
const ManageSchedulePage = (): JSX.Element => {
const dispatch = useDispatch();
const { MANAGE_PACKS } = paths;
const handleAdvanced = () => dispatch(push(MANAGE_PACKS));
useEffect(() => {
dispatch(globalScheduledQueryActions.loadAll());
dispatch(queryActions.loadAll());
}, [dispatch]);
const allQueries = useSelector((state: IRootState) => state.entities.queries);
const allQueriesList = Object.values(allQueries.data);
const allGlobalScheduledQueries = useSelector(
(state: IRootState) => state.entities.global_scheduled_queries
);
const allGlobalScheduledQueriesList = Object.values(
allGlobalScheduledQueries.data
);
const allGlobalScheduledQueriesError = allGlobalScheduledQueries.errors;
const [showScheduleEditorModal, setShowScheduleEditorModal] = useState(false);
const [
showRemoveScheduledQueryModal,
setShowRemoveScheduledQueryModal,
] = useState(false);
const [selectedQueryIds, setSelectedQueryIds] = useState([]);
const toggleScheduleEditorModal = useCallback(() => {
setShowScheduleEditorModal(!showScheduleEditorModal);
}, [showScheduleEditorModal, setShowScheduleEditorModal]);
const toggleRemoveScheduledQueryModal = useCallback(() => {
setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal);
}, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]);
const onRemoveScheduledQueryClick = (selectedTableQueryIds: any) => {
toggleRemoveScheduledQueryModal();
setSelectedQueryIds(selectedTableQueryIds);
};
const onRemoveScheduledQuerySubmit = useCallback(() => {
const promises = selectedQueryIds.map((id: number) => {
return dispatch(globalScheduledQueryActions.destroy({ id }));
});
const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries";
return Promise.all(promises)
.then(() => {
dispatch(
renderFlash(
"success",
`Successfully removed scheduled ${queryOrQueries}.`
)
);
toggleRemoveScheduledQueryModal();
dispatch(globalScheduledQueryActions.loadAll());
})
.catch(() => {
dispatch(
renderFlash(
"error",
`Unable to remove scheduled ${queryOrQueries}. Please try again.`
)
);
toggleRemoveScheduledQueryModal();
});
}, [dispatch, selectedQueryIds, toggleRemoveScheduledQueryModal]);
const onAddScheduledQuerySubmit = useCallback(
(formData: IFormData) => {
dispatch(globalScheduledQueryActions.create({ ...formData }))
.then(() => {
dispatch(
renderFlash(
"success",
`Successfully added ${formData.name} to the schedule.`
)
);
dispatch(globalScheduledQueryActions.loadAll());
})
.catch(() => {
dispatch(
renderFlash("error", "Could not schedule query. Please try again.")
);
});
toggleScheduleEditorModal();
},
[dispatch, toggleScheduleEditorModal]
);
return (
<div className={baseClass}>
<div className={`${baseClass}__wrapper body-wrap`}>
<div className={`${baseClass}__header-wrap`}>
<div className={`${baseClass}__header`}>
<div className={`${baseClass}__text`}>
<h1 className={`${baseClass}__title`}>
<span>Schedule</span>
</h1>
<div className={`${baseClass}__description`}>
<p>
Schedule recurring queries for your hosts. Fleets query
schedule lets you add queries which are executed at regular
intervals.
</p>
</div>
</div>
</div>
{/* Hide CTA Buttons if no schedule or schedule error */}
{allGlobalScheduledQueriesList.length !== 0 &&
allGlobalScheduledQueriesError.length !== 0 && (
<div className={`${baseClass}__action-button-container`}>
<Button
variant="inverse"
onClick={handleAdvanced}
className={`${baseClass}__advanced-button`}
>
Advanced
</Button>
<Button
variant="brand"
className={`${baseClass}__schedule-button`}
onClick={toggleScheduleEditorModal}
>
Schedule a query
</Button>
</div>
)}
</div>
<div>
{renderTable(
onRemoveScheduledQueryClick,
allGlobalScheduledQueriesList,
allGlobalScheduledQueriesError,
toggleScheduleEditorModal
)}
</div>
{showScheduleEditorModal && (
<ScheduleEditorModal
onCancel={toggleScheduleEditorModal}
onScheduleSubmit={onAddScheduledQuerySubmit}
allQueries={allQueriesList}
/>
)}
{showRemoveScheduledQueryModal && (
<RemoveScheduledQueryModal
onCancel={toggleRemoveScheduledQueryModal}
onSubmit={onRemoveScheduledQuerySubmit}
/>
)}
</div>
</div>
);
};
export default ManageSchedulePage;

View File

@ -0,0 +1,79 @@
.manage-schedule-page {
&__header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $pad-xxlarge;
}
&__header {
display: flex;
align-items: center;
}
&__text {
margin-right: $pad-large;
}
&__title {
font-size: $large;
.fleeticon {
color: $core-fleet-blue;
margin-right: 15px;
}
.fleeticon-success-check {
color: $ui-success;
}
.fleeticon-offline {
color: $ui-error;
}
.fleeticon-mia {
color: $core-fleet-black;
}
}
&__description {
margin: 0 0 $pad-medium;
h2 {
text-transform: uppercase;
color: $core-fleet-black;
font-weight: $regular;
font-size: $small;
}
p {
color: $core-fleet-blue;
margin: 0;
font-size: $x-small;
font-style: italic;
}
}
&__action-button-container {
display: flex;
align-items: flex-start;
}
.button {
font-size: $x-small;
}
&__advanced-button {
margin-right: $pad-small;
}
&__modal-buttons {
width: 100%;
display: flex;
justify-content: flex-end;
.button:first-child {
margin-right: $pad-medium;
}
}
}

View File

@ -0,0 +1,48 @@
import React from "react";
import Modal from "components/modals/Modal";
import Button from "components/buttons/Button";
const baseClass = "remove-scheduled-query-modal";
interface IRemoveScheduledQueryModalProps {
onCancel: () => void;
onSubmit: () => void;
}
const RemoveScheduledQueryModal = (
props: IRemoveScheduledQueryModalProps
): JSX.Element => {
const { onCancel, onSubmit } = props;
return (
<Modal
title={"Remove scheduled query"}
onExit={onCancel}
className={baseClass}
>
<div className={baseClass}>
Are you sure you want to remove the selected queries from the schedule?
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
onClick={onCancel}
variant="inverse"
>
Cancel
</Button>
<Button
className={`${baseClass}__btn`}
type="button"
variant="alert"
onClick={onSubmit}
>
Remove
</Button>
</div>
</div>
</Modal>
);
};
export default RemoveScheduledQueryModal;

View File

@ -0,0 +1,13 @@
.remove-scheduled-query-modal {
font-size: $x-small;
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
margin-top: $pad-xxlarge;
}
&__btn {
margin-left: 12px;
}
}

View File

@ -0,0 +1 @@
export { default } from "./RemoveScheduledQueryModal";

View File

@ -0,0 +1,264 @@
import React, { useState, useCallback } from "react";
import { pull } from "lodash";
// @ts-ignore
import FleetIcon from "components/icons/FleetIcon";
import Modal from "components/modals/Modal";
import Button from "components/buttons/Button";
import InfoBanner from "components/InfoBanner/InfoBanner";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import { IQuery } from "interfaces/query";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
import {
FREQUENCY_DROPDOWN_OPTIONS,
PLATFORM_OPTIONS,
LOGGING_TYPE_OPTIONS,
MIN_OSQUERY_VERSION_OPTIONS,
} from "utilities/constants";
const baseClass = "schedule-editor-modal";
interface IFormData {
interval: number;
name?: string;
shard: number;
query?: string;
query_id?: number;
logging_type: string;
platform: string;
version: string;
}
interface IScheduleEditorModalProps {
allQueries: IQuery[];
onCancel: () => void;
onScheduleSubmit: (formData: IFormData) => void;
}
interface INoQueryOption {
id: number;
name: string;
}
const ScheduleEditorModal = ({
onCancel,
onScheduleSubmit,
allQueries,
}: IScheduleEditorModalProps): JSX.Element => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(
false
);
const [selectedQuery, setSelectedQuery] = useState<
IGlobalScheduledQuery | INoQueryOption
>();
const [selectedFrequency, setSelectedFrequency] = useState<number>(86400);
const [
selectedPlatformOptions,
setSelectedPlatformOptions,
] = useState<string>("");
const [selectedLoggingType, setSelectedLoggingType] = useState<string>(
"snapshot"
);
const [
selectedMinOsqueryVersionOptions,
setSelectedMinOsqueryVersionOptions,
] = useState<string>("");
const [selectedShard, setSelectedShard] = useState<string>("");
const createQueryDropdownOptions = () => {
const queryOptions = allQueries.map((q) => {
return {
value: String(q.id),
label: q.name,
};
});
return queryOptions;
};
const toggleAdvancedOptions = () => {
setShowAdvancedOptions(!showAdvancedOptions);
};
const onChangeSelectQuery = useCallback(
(queryId: string) => {
const queryWithId: IQuery | undefined = allQueries.find(
(query: IQuery) => query.id === parseInt(queryId, 10)
);
setSelectedQuery(queryWithId);
},
[allQueries, setSelectedQuery]
);
const onChangeSelectFrequency = useCallback(
(value: number) => {
setSelectedFrequency(value);
},
[setSelectedFrequency]
);
const onChangeSelectPlatformOptions = useCallback(
(values: string) => {
const valArray = values.split(",");
// Remove All if another OS is chosen
// else if Remove OS if All is chosen
if (valArray.indexOf("") === 0 && valArray.length > 1) {
setSelectedPlatformOptions(pull(valArray, "").join(","));
} else if (valArray.length > 1 && valArray.indexOf("") > -1) {
setSelectedPlatformOptions("");
} else {
setSelectedPlatformOptions(values);
}
},
[setSelectedPlatformOptions]
);
const onChangeSelectLoggingType = useCallback(
(value: string) => {
setSelectedLoggingType(value);
},
[setSelectedLoggingType]
);
const onChangeMinOsqueryVersionOptions = useCallback(
(value: string) => {
setSelectedMinOsqueryVersionOptions(value);
},
[setSelectedMinOsqueryVersionOptions]
);
const onChangeShard = useCallback(
(value: string) => {
setSelectedShard(value);
},
[setSelectedShard]
);
const onFormSubmit = () => {
onScheduleSubmit({
shard: parseInt(selectedShard, 10),
interval: selectedFrequency,
query_id: selectedQuery?.id,
name: selectedQuery?.name,
logging_type: selectedLoggingType,
platform: selectedPlatformOptions,
version: selectedMinOsqueryVersionOptions,
});
};
return (
<Modal title={"Schedule editor"} onExit={onCancel} className={baseClass}>
<form className={`${baseClass}__form`}>
<Dropdown
searchable
options={createQueryDropdownOptions()}
onChange={onChangeSelectQuery}
placeholder={"Select query"}
value={selectedQuery?.id}
wrapperClassName={`${baseClass}__select-query-dropdown-wrapper`}
/>
<Dropdown
searchable={false}
options={FREQUENCY_DROPDOWN_OPTIONS}
onChange={onChangeSelectFrequency}
placeholder={"Every day"}
value={selectedFrequency}
label={"Choose a frequency and then run this query on a schedule"}
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--frequency`}
/>
{/* <InfoBanner className={`${baseClass}__sandbox-info`}>
<p>
Your configured log destination is <b>filesystem</b>.
</p>
<p>
This means that when this query is run on your hosts, the data will
be sent to the filesystem.
</p>
<p>
Check out the Fleet documentation on&nbsp;
<a
href="https://github.com/fleetdm/fleet/blob/6649d08a05799811f6fb0566947946edbfebf63e/docs/2-Deploying/2-Configuration.md#osquery_result_log_plugin"
target="_blank"
rel="noopener noreferrer"
>
how configure a different log destination.&nbsp;
<FleetIcon name="external-link" />
</a>
</p>
</InfoBanner> */}
<div>
<Button
variant="unstyled"
className={`${showAdvancedOptions ? "upcarat" : "downcarat"}
${baseClass}__advanced-options-button`}
onClick={toggleAdvancedOptions}
>
{showAdvancedOptions
? "Hide advanced options"
: "Show advanced options"}
</Button>
{showAdvancedOptions && (
<div>
<Dropdown
options={LOGGING_TYPE_OPTIONS}
onChange={onChangeSelectLoggingType}
placeholder="Select"
value={selectedLoggingType}
label="Logging"
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--logging`}
/>
<Dropdown
options={PLATFORM_OPTIONS}
placeholder="Select"
label="Platform"
onChange={onChangeSelectPlatformOptions}
value={selectedPlatformOptions}
multi
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--platform`}
/>
<Dropdown
options={MIN_OSQUERY_VERSION_OPTIONS}
onChange={onChangeMinOsqueryVersionOptions}
placeholder="Select"
value={selectedMinOsqueryVersionOptions}
label="Minimum osquery version"
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
/>
<InputField
onChange={onChangeShard}
inputWrapperClass={`${baseClass}__form-field ${baseClass}__form-field--shard`}
value={selectedShard}
placeholder="- - -"
label="Shard"
type="number"
/>
</div>
)}
</div>
<div className={`${baseClass}__btn-wrap`}>
<Button
className={`${baseClass}__btn`}
type="button"
variant="brand"
onClick={onFormSubmit}
disabled={!selectedQuery}
>
Schedule
</Button>
<Button
className={`${baseClass}__btn`}
onClick={onCancel}
variant="inverse"
>
Cancel
</Button>
</div>
</form>
</Modal>
);
};
export default ScheduleEditorModal;

View File

@ -0,0 +1,71 @@
.schedule-editor-modal {
&__sandbox-info {
margin-top: $pad-medium;
p {
margin: 0;
margin-bottom: $pad-medium;
}
p:last-child {
margin-bottom: 0;
}
}
a {
color: $core-vibrant-blue;
font-weight: $regular;
font-size: $x-small;
text-decoration: none;
}
&__info-header {
font-weight: $bold;
}
&__btn-wrap {
display: flex;
flex-direction: row-reverse;
}
&__btn {
margin-left: 12px;
padding: 0 $pad-xlarge;
}
&__advanced-options-button {
margin: $pad-medium 0;
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
}
.downcarat {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 2px;
}
}
.upcarat {
&::after {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5) rotate(180deg);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-left: 2px;
margin-bottom: 4px;
margin-left: 14px;
}
}
.Select-value-label {
font-size: $small;
}
}

View File

@ -0,0 +1 @@
export { default } from "./ScheduleEditorModal";

View File

@ -0,0 +1,38 @@
/**
* Component when there is an error retrieving schedule set up in fleet
*/
import React from "react";
import OpenNewTabIcon from "../../../../../../assets/images/open-new-tab-12x12@2x.png";
import ErrorIcon from "../../../../../../assets/images/icon-error-16x16@2x.png";
const baseClass = "schedule-error";
const ScheduleError = (): JSX.Element => {
return (
<div className={`${baseClass}`}>
<div className={`${baseClass}__inner`}>
<div className="info">
<span className="info__header">
<img src={ErrorIcon} alt="error icon" id="error-icon" />
Something&apos;s gone wrong.
</span>
<span className="info__data">Refresh the page or log in again.</span>
<span className="info__data">
If this keeps happening, please&nbsp;
<a
href="https://github.com/fleetdm/fleet/issues"
target="_blank"
rel="noopener noreferrer"
>
file an issue.
<img src={OpenNewTabIcon} alt="open new tab" id="new-tab-icon" />
</a>
</span>
</div>
</div>
</div>
);
};
export default ScheduleError;

View File

@ -0,0 +1,48 @@
.schedule-error {
display: flex;
flex-direction: column;
align-items: center;
margin-top: $pad-xxxlarge;
#error-icon {
height: 12px;
width: 12px;
margin-right: 8px;
}
#new-tab-icon {
height: 12px;
width: 12px;
margin-left: 6px;
}
a {
font-size: $x-small;
color: $core-vibrant-blue;
font-weight: $bold;
text-decoration: none;
}
&__inner {
display: flex;
flex-direction: row;
}
.info {
&__header {
display: block;
color: $core-fleet-black;
font-weight: $bold;
font-size: $x-small;
text-align: left;
}
&__data {
display: block;
color: $core-fleet-black;
font-weight: normal;
font-size: $x-small;
text-align: left;
margin-top: 10px;
}
}
}

View File

@ -0,0 +1 @@
export { default } from "./ScheduleError";

View File

@ -0,0 +1,122 @@
/**
* Component when there is an error retrieving schedule set up in fleet
*/
import React, { useCallback } from "react";
import { useSelector, useDispatch } from "react-redux";
import { push } from "react-router-redux";
import paths from "router/paths";
import Button from "components/buttons/Button";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
// @ts-ignore
import globalScheduledQueryActions from "redux/nodes/entities/global_scheduled_queries/actions";
import TableContainer from "components/TableContainer";
import generateTableHeaders from "./ScheduleTableConfig";
// @ts-ignore
import scheduleSvg from "../../../../../../assets/images/schedule.svg";
const baseClass = "schedule-list-wrapper";
const noScheduleClass = "no-schedule";
interface IScheduleListWrapperProps {
onRemoveScheduledQueryClick: any;
allGlobalScheduledQueriesList: IGlobalScheduledQuery[];
toggleScheduleEditorModal: any;
}
interface IRootState {
entities: {
global_scheduled_queries: {
isLoading: boolean;
data: IGlobalScheduledQuery[];
};
};
}
const ScheduleListWrapper = (props: IScheduleListWrapperProps): JSX.Element => {
const {
onRemoveScheduledQueryClick,
allGlobalScheduledQueriesList,
toggleScheduleEditorModal,
} = props;
const dispatch = useDispatch();
const { MANAGE_PACKS } = paths;
const handleAdvanced = () => dispatch(push(MANAGE_PACKS));
const NoScheduledQueries = () => {
return (
<div className={`${noScheduleClass}`}>
<div className={`${noScheduleClass}__inner`}>
<img src={scheduleSvg} alt="No Schedule" />
<div className={`${noScheduleClass}__inner-text`}>
<h2>You don&apos;t have any queries scheduled.</h2>
<p>
Schedule a query, or go to your osquery packs via the
&lsquo;Advanced&rsquo; button.
</p>
<div className={`${noScheduleClass}__-cta-buttons`}>
<Button
variant="brand"
className={`${noScheduleClass}__schedule-button`}
onClick={toggleScheduleEditorModal}
>
Schedule a query
</Button>
<Button
variant="inverse"
onClick={handleAdvanced}
className={`${baseClass}__advanced-button`}
>
Advanced
</Button>
</div>
</div>
</div>
</div>
);
};
const tableHeaders = generateTableHeaders();
const loadingTableData = useSelector(
(state: IRootState) => state.entities.global_scheduled_queries.isLoading
);
// Search functionality disabled, needed if enabled
const onQueryChange = useCallback(
(queryData) => {
const { pageIndex, pageSize, searchQuery } = queryData;
dispatch(
globalScheduledQueryActions.loadAll({
page: pageIndex,
perPage: pageSize,
globalFilter: searchQuery,
})
);
},
[dispatch]
);
return (
<div className={`${baseClass}`}>
<TableContainer
resultsTitle={"queries"}
columns={tableHeaders}
data={allGlobalScheduledQueriesList}
isLoading={loadingTableData}
defaultSortHeader={"query"}
defaultSortDirection={"desc"}
showMarkAllPages={false}
isAllPagesSelected={false}
onQueryChange={onQueryChange}
inputPlaceHolder="Search"
searchable={false}
onPrimarySelectActionClick={onRemoveScheduledQueryClick}
primarySelectActionButtonText={"Remove query"}
emptyComponent={NoScheduledQueries}
/>
</div>
);
};
export default ScheduleListWrapper;

View File

@ -0,0 +1,88 @@
/* eslint-disable react/prop-types */
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
import { secondsToDhms } from "fleet/helpers";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
import TextCell from "components/TableContainer/DataTable/TextCell";
import { IGlobalScheduledQuery } from "interfaces/global_scheduled_query";
interface IHeaderProps {
column: {
title: string;
isSortedDesc: boolean;
};
getToggleAllRowsSelectedProps: () => any; // TODO: do better with types
toggleAllRowsSelected: () => void;
}
interface ICellProps {
cell: {
value: any;
};
row: {
original: IGlobalScheduledQuery;
getToggleRowSelectedProps: () => any; // TODO: do better with types
toggleRowSelected: () => void;
};
}
interface IDataColumn {
Header: ((props: IHeaderProps) => JSX.Element) | string;
Cell: (props: ICellProps) => JSX.Element;
id?: string;
title?: string;
accessor?: string;
disableHidden?: boolean;
disableSortBy?: boolean;
}
// NOTE: cellProps come from react-table
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
const generateTableHeaders = (): IDataColumn[] => {
return [
{
id: "selection",
Header: (cellProps: IHeaderProps): JSX.Element => {
const props = cellProps.getToggleAllRowsSelectedProps();
const checkboxProps = {
value: props.checked,
indeterminate: props.indeterminate,
onChange: () => cellProps.toggleAllRowsSelected(),
};
return <Checkbox {...checkboxProps} />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const props = cellProps.row.getToggleRowSelectedProps();
const checkboxProps = {
value: props.checked,
onChange: () => cellProps.row.toggleRowSelected(),
};
return <Checkbox {...checkboxProps} />;
},
disableHidden: true,
},
{
title: "Query name",
Header: "Query name",
disableSortBy: true,
accessor: "query_name",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "Frequency",
Header: "Frequency",
disableSortBy: true,
accessor: "interval",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={secondsToDhms(cellProps.cell.value)} />
),
},
];
};
export default generateTableHeaders;

View File

@ -0,0 +1,166 @@
.schedule-list-wrapper {
border-collapse: collapse;
&__wrapper {
border: 1px solid $ui-fleet-blue-15;
border-radius: 4px;
overflow: hidden;
margin-top: $pad-medium;
}
thead {
background-color: $ui-off-white;
border-bottom: 1px solid $ui-fleet-blue-15;
th + th {
border-left: 1px solid $ui-fleet-blue-15;
}
th {
font-size: $x-small;
font-weight: $bold;
text-align: left;
padding: $pad-medium $pad-large;
&:nth-child(1) {
border-top-left-radius: 3px;
width: 20px;
}
&:nth-child(2) {
width: calc(49% - 20px);
}
&:nth-child(3) {
width: 37%;
}
&:last-child {
border-top-right-radius: 3px;
}
}
}
&__th-pack-name {
padding-left: 0;
text-align: left;
}
&__select-all {
margin-bottom: 0;
}
&__empty-table {
text-align: center;
font-size: $x-small;
color: $core-fleet-black;
}
&__scheduled-query-count {
color: $core-fleet-black;
font-size: $x-small;
font-weight: $bold;
margin: 0 12px 0 0;
display: inline-block;
}
}
.no-schedule {
display: flex;
flex-direction: column;
align-items: center;
margin-top: $pad-xxxlarge;
h1 {
font-size: $large;
font-weight: $regular;
line-height: normal;
letter-spacing: normal;
color: $core-fleet-black;
}
h2 {
font-size: $small;
font-weight: $bold;
margin: 0 0 $pad-large;
line-height: 20px;
color: $core-fleet-black;
}
ul {
margin: 0;
padding: 0;
color: $core-fleet-black;
list-style: none;
li {
&::before {
content: "";
color: $core-vibrant-blue;
margin-right: $pad-medium;
}
}
}
&__inner {
display: flex;
flex-direction: row;
h1 {
font-size: $small;
font-weight: $bold;
margin-bottom: $pad-medium;
}
img {
width: 176px;
margin-right: $pad-xlarge;
}
p {
color: $core-fleet-black;
font-weight: $regular;
font-size: $x-small;
margin: 0;
margin-bottom: $pad-large;
}
.no-filter-results {
display: flex;
flex-direction: column;
width: 350px;
}
}
&__inner-text {
padding-left: $pad-xlarge;
width: 290px;
}
&__schedule-button {
margin-right: $pad-small;
}
.query-pagination__pager-wrap {
margin-top: $pad-medium;
}
&__no-hosts-contact {
text-align: left;
margin-top: $pad-large;
p {
color: $core-fleet-black;
font-weight: $bold;
font-size: $x-small;
margin: 0;
}
a {
color: $core-vibrant-blue;
font-weight: $regular;
font-size: $x-small;
text-decoration: none;
}
}
}

View File

@ -0,0 +1 @@
export { default } from "./ScheduleListWrapper";

View File

@ -0,0 +1 @@
export { default } from "./ManageSchedulePage";

View File

@ -6,6 +6,7 @@ const invitesSchema = new Schema("invites");
const labelsSchema = new Schema("labels");
const packsSchema = new Schema("packs");
const queriesSchema = new Schema("queries");
const globalScheduledQueriesSchema = new Schema("global_scheduled_queries");
const scheduledQueriesSchema = new Schema("scheduled_queries");
const targetsSchema = new Schema("targets");
const usersSchema = new Schema("users");
@ -18,6 +19,7 @@ export default {
LABELS: labelsSchema,
PACKS: packsSchema,
QUERIES: queriesSchema,
GLOBAL_SCHEDULED_QUERIES: globalScheduledQueriesSchema,
SCHEDULED_QUERIES: scheduledQueriesSchema,
TARGETS: targetsSchema,
USERS: usersSchema,

View File

@ -0,0 +1,3 @@
import config from "./config";
export default config.actions;

View File

@ -0,0 +1,16 @@
import { formatGlobalScheduledQueryForClient } from "fleet/helpers";
import Fleet from "fleet";
import Config from "redux/nodes/entities/base/config";
import schemas from "redux/nodes/entities/base/schemas";
const { GLOBAL_SCHEDULED_QUERIES: schema } = schemas;
export default new Config({
createFunc: Fleet.globalScheduledQueries.create,
destroyFunc: Fleet.globalScheduledQueries.destroy,
entityName: "global_scheduled_queries",
loadAllFunc: Fleet.globalScheduledQueries.loadAll,
parseEntityFunc: formatGlobalScheduledQueryForClient,
schema,
updateFunc: Fleet.globalScheduledQueries.update,
});

View File

@ -0,0 +1,3 @@
import config from "./config";
export default config.reducer;

View File

@ -6,6 +6,7 @@ import invites from "./invites/reducer";
import labels from "./labels/reducer";
import packs from "./packs/reducer";
import queries from "./queries/reducer";
import globalScheduledQueries from "./global_scheduled_queries/reducer";
import scheduledQueries from "./scheduled_queries/reducer";
import users from "./users/reducer";
import teams from "./teams/reducer";
@ -17,6 +18,7 @@ export default combineReducers({
labels,
packs,
queries,
global_scheduled_queries: globalScheduledQueries,
scheduled_queries: scheduledQueries,
users,
teams,

View File

@ -32,11 +32,13 @@ import LoginRoutes from "components/LoginRoutes";
import LogoutPage from "pages/LogoutPage";
import ManageHostsPage from "pages/hosts/ManageHostsPage";
import ManageQueriesPage from "pages/queries/ManageQueriesPage";
import ManageSchedulePage from "pages/schedule/ManageSchedulePage";
import PackPageWrapper from "components/packs/PackPageWrapper";
import PackComposerPage from "pages/packs/PackComposerPage";
import QueryPage from "pages/queries/QueryPage";
import QueryPageWrapper from "components/queries/QueryPageWrapper";
import RegistrationPage from "pages/RegistrationPage";
import SchedulePageWrapper from "components/schedule/SchedulePageWrapper";
import ApiOnlyUser from "pages/ApiOnlyUser";
import Fleet403 from "pages/Fleet403";
import Fleet404 from "pages/Fleet404";
@ -103,6 +105,9 @@ const routes = (
<Route path="edit" component={EditPackPage} />
</Route>
</Route>
<Route path="schedule" component={SchedulePageWrapper}>
<Route path="manage" component={ManageSchedulePage} />
</Route>
</Route>
<Route path="queries" component={QueryPageWrapper}>
<Route path="manage" component={ManageQueriesPage} />

View File

@ -38,6 +38,7 @@ export default {
MANAGE_PACKS: `${URL_PREFIX}/packs/manage`,
NEW_PACK: `${URL_PREFIX}/packs/new`,
MANAGE_QUERIES: `${URL_PREFIX}/queries/manage`,
MANAGE_SCHEDULE: `${URL_PREFIX}/schedule/manage`,
NEW_QUERY: `${URL_PREFIX}/queries/new`,
RESET_PASSWORD: `${URL_PREFIX}/login/reset`,
SETUP: `${URL_PREFIX}/setup`,

View File

@ -112,20 +112,20 @@ pre {
background-color: $core-fleet-black;
color: $core-white;
border-radius: 4px;
.string {
color: $rainbow-green;
.string {
color: $rainbow-green;
}
.number {
color: $rainbow-orange;
.number {
color: $rainbow-orange;
}
.boolean {
color: blue;
.boolean {
color: blue;
}
.null {
color: magenta;
.null {
color: magenta;
}
.key {
color: $core-white;
.key {
color: $core-white;
}
}

View File

@ -0,0 +1,57 @@
import createRequestMock from "test/mocks/create_request_mock";
import { globalScheduledQueryStub } from "test/stubs";
export default {
create: {
valid: (bearerToken, unformattedParams) => {
const params = {
interval: Number(unformattedParams.interval),
platform: unformattedParams.platform,
query_id: Number(unformattedParams.query_id),
removed: true,
snapshot: false,
shard: Number(unformattedParams.shard),
};
return createRequestMock({
bearerToken,
endpoint: "/api/v1/fleet/global/schedule",
method: "post",
params,
response: { scheduled: globalScheduledQueryStub },
responseStatus: 201,
});
},
},
destroy: {
valid: (bearerToken, globalScheduledQuery) => {
return createRequestMock({
bearerToken,
endpoint: `/api/v1/fleet/global/schedule/${globalScheduledQuery.id}`,
method: "delete",
response: {},
});
},
},
loadAll: {
valid: (bearerToken) => {
return createRequestMock({
bearerToken,
endpoint: "/api/v1/fleet/global/schedule",
method: "get",
response: { scheduled: [globalScheduledQueryStub] },
});
},
},
update: {
valid: (bearerToken, globalScheduledQuery, params) => {
return createRequestMock({
bearerToken,
endpoint: `/api/v1/fleet/global/schedule/${globalScheduledQuery.id}`,
method: "patch",
params,
response: { scheduled: { ...globalScheduledQuery, ...params } },
});
},
},
};

View File

@ -5,6 +5,7 @@ import invites from "test/mocks/invite_mocks";
import labels from "test/mocks/label_mocks";
import packs from "test/mocks/pack_mocks";
import queries from "test/mocks/query_mocks";
import globalScheduledQueries from "test/mocks/global_scheduled_query_mocks";
import scheduledQueries from "test/mocks/scheduled_query_mocks";
import sessions from "test/mocks/session_mocks";
import statusLabels from "test/mocks/status_label_mocks";
@ -19,6 +20,7 @@ export default {
labels,
packs,
queries,
globalScheduledQueries,
scheduledQueries,
sessions,
statusLabels,

View File

@ -154,6 +154,19 @@ export const scheduledQueryStub = {
snapshot: true,
};
export const globalScheduledQueryStub = {
id: 1,
interval: 60,
name: "Get all users",
query_name: "users",
platform: "darwin",
query: "SELECT * FROM users",
query_id: 5,
removed: false,
shard: 12,
snapshot: true,
};
export const teamStub: ITeam = {
description: "This is the test team",
host_count: 10,
@ -220,5 +233,6 @@ export default {
packStub,
queryStub,
scheduledQueryStub,
globalScheduledQueryStub,
userStub,
};

View File

@ -0,0 +1,51 @@
export const FREQUENCY_DROPDOWN_OPTIONS = [
{ value: 3600, label: "Every hour" },
{ value: 21600, label: "Every 6 hours" },
{ value: 43200, label: "Every 12 hours" },
{ value: 86400, label: "Every day" },
{ value: 604800, label: "Every week" },
];
export const PLATFORM_OPTIONS = [
{ label: "All", value: "" },
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
];
export const LOGGING_TYPE_OPTIONS = [
{ label: "Snapshot", value: "snapshot" },
{ label: "Differential", value: "differential" },
{
label: "Differential (Ignore Removals)",
value: "differential_ignore_removals",
},
];
export const MIN_OSQUERY_VERSION_OPTIONS = [
{ label: "All", value: "" },
{ label: "4.7.0 +", value: "4.7.0" },
{ label: "4.6.0 +", value: "4.6.0" },
{ label: "4.5.1 +", value: "4.5.1" },
{ label: "4.5.0 +", value: "4.5.0" },
{ label: "4.4.0 +", value: "4.4.0" },
{ label: "4.3.0 +", value: "4.3.0" },
{ label: "4.2.0 +", value: "4.2.0" },
{ label: "4.1.2 +", value: "4.1.2" },
{ label: "4.1.1 +", value: "4.1.1" },
{ label: "4.1.0 +", value: "4.1.0" },
{ label: "4.0.2 +", value: "4.0.2" },
{ label: "4.0.1 +", value: "4.0.1" },
{ label: "4.0.0 +", value: "4.0.0" },
{ label: "3.4.0 +", value: "3.4.0" },
{ label: "3.3.2 +", value: "3.3.2" },
{ label: "3.3.1 +", value: "3.3.1" },
{ label: "3.2.6 +", value: "3.2.6" },
{ label: "2.2.1 +", value: "2.2.1" },
{ label: "2.2.0 +", value: "2.2.0" },
{ label: "2.1.2 +", value: "2.1.2" },
{ label: "2.1.1 +", value: "2.1.1" },
{ label: "2.0.0 +", value: "2.0.0" },
{ label: "1.8.2 +", value: "1.8.2" },
{ label: "1.8.1 +", value: "1.8.1" },
];