Preferences - UI and API improvements (#654)
* fix(tracker): fix assist typings * fix(tracker): fix assist typings * change(ui) - preferences - removed old * change(ui) - preferences - wip * change(ui) - preferences - list * change(ui) - right box mardings * change(ui) - preferences - integration item paddings * change(ui) - preferences - integration icons * change(ui) - preferences - integration icons * change(ui) - preferences - integration - check status * change(ui) - preferences - integration - check status * change(ui) - preferences - metadata - move the delete button inside the modal * change(ui) - preferences - webhooks - modal and delete btn changes * change(ui) - preferences - modalContext updates * change(ui) - input field forward refs * change(ui) - metadata - modal * change(ui) - metadata - set deleting item to null * change(ui) - integrations * change(ui) - hoc withcopy * change(ui) - projects * change(ui) - users list modal * change(ui) - projects remove border for the last * change(ui) - integrations new api changes * change(ui) - github and jira changes * change(ui) - github and jira changes Co-authored-by: sylenien <nikita@openreplay.com>
5
frontend/app/assets/integrations/aws.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="111" height="66" viewBox="0 0 111 66" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.3319 24.0791C31.3319 25.4209 31.477 26.5088 31.7308 27.3066C32.0209 28.1044 32.3835 28.9747 32.8912 29.9176C33.0726 30.2077 33.1451 30.4978 33.1451 30.7517C33.1451 31.1143 32.9275 31.4769 32.4561 31.8396L30.1715 33.3627C29.8451 33.5802 29.5187 33.689 29.2286 33.689C28.866 33.689 28.5033 33.5077 28.1407 33.1813C27.633 32.6374 27.1978 32.0572 26.8352 31.4769C26.4726 30.8605 26.1099 30.1715 25.711 29.3374C22.8825 32.6737 19.3286 34.3418 15.0495 34.3418C12.0033 34.3418 9.57366 33.4715 7.79674 31.7308C6.01981 29.9901 5.11322 27.6693 5.11322 24.7682C5.11322 21.6857 6.20113 19.1835 8.41322 17.2978C10.6253 15.4121 13.5627 14.4693 17.2978 14.4693C18.5308 14.4693 19.8 14.578 21.1418 14.7594C22.4835 14.9407 23.8616 15.2308 25.3121 15.5572V12.9099C25.3121 10.1539 24.7319 8.23189 23.6077 7.10772C22.4473 5.98354 20.489 5.43958 17.6967 5.43958C16.4275 5.43958 15.122 5.58464 13.7803 5.91101C12.4385 6.23739 11.133 6.63629 9.86377 7.14398C9.28355 7.39783 8.84838 7.54288 8.59454 7.61541C8.34069 7.68794 8.15937 7.7242 8.01432 7.7242C7.50663 7.7242 7.25278 7.36156 7.25278 6.60002V4.8231C7.25278 4.24288 7.32531 3.80772 7.50663 3.55387C7.68795 3.30002 8.01432 3.04618 8.52201 2.79233C9.79124 2.13959 11.3143 1.59563 13.0912 1.16046C14.8682 0.689036 16.7539 0.471453 18.7484 0.471453C23.0638 0.471453 26.2187 1.45057 28.2495 3.40882C30.244 5.36706 31.2594 8.34068 31.2594 12.3297V24.0791H31.3319ZM16.6088 29.5912C17.8055 29.5912 19.0385 29.3737 20.344 28.9385C21.6495 28.5033 22.8099 27.7055 23.789 26.6176C24.3693 25.9286 24.8044 25.1671 25.022 24.2967C25.2396 23.4264 25.3846 22.3747 25.3846 21.1418V19.6187C24.333 19.3649 23.2088 19.1473 22.0484 19.0022C20.8879 18.8572 19.7638 18.7846 18.6396 18.7846C16.2099 18.7846 14.433 19.2561 13.2363 20.2352C12.0396 21.2143 11.4594 22.5923 11.4594 24.4055C11.4594 26.1099 11.8945 27.3791 12.8011 28.2495C13.6715 29.1561 14.9407 29.5912 16.6088 29.5912ZM45.7286 33.5077C45.0759 33.5077 44.6407 33.3989 44.3506 33.1451C44.0605 32.9275 43.8066 32.4198 43.589 31.7308L35.0671 3.69893C34.8495 2.97365 34.7407 2.50222 34.7407 2.24838C34.7407 1.66816 35.0308 1.34178 35.611 1.34178H39.1649C39.8539 1.34178 40.3253 1.45057 40.5792 1.70442C40.8693 1.922 41.0868 2.4297 41.3044 3.11871L47.3967 27.1253L53.0539 3.11871C53.2352 2.39343 53.4528 1.922 53.7429 1.70442C54.033 1.48684 54.5407 1.34178 55.1934 1.34178H58.0945C58.7835 1.34178 59.255 1.45057 59.5451 1.70442C59.8352 1.922 60.089 2.4297 60.2341 3.11871L65.9638 27.4154L72.2374 3.11871C72.455 2.39343 72.7088 1.922 72.9627 1.70442C73.2528 1.48684 73.7242 1.34178 74.377 1.34178H77.7495C78.3297 1.34178 78.6561 1.63189 78.6561 2.24838C78.6561 2.42969 78.6198 2.61101 78.5835 2.8286C78.5473 3.04618 78.4748 3.33629 78.3297 3.73519L69.5901 31.7671C69.3726 32.4923 69.1187 32.9638 68.8286 33.1813C68.5385 33.3989 68.0671 33.544 67.4506 33.544H64.3319C63.6429 33.544 63.1715 33.4352 62.8813 33.1813C62.5912 32.9275 62.3374 32.4561 62.1923 31.7308L56.5715 8.34068L50.9868 31.6945C50.8055 32.4198 50.5879 32.8912 50.2978 33.1451C50.0077 33.3989 49.5 33.5077 48.8473 33.5077H45.7286ZM92.3275 34.4868C90.4418 34.4868 88.5561 34.2693 86.7429 33.8341C84.9297 33.3989 83.5154 32.9275 82.5726 32.3835C81.9923 32.0572 81.5934 31.6945 81.4484 31.3682C81.3033 31.0418 81.2308 30.6791 81.2308 30.3528V28.5033C81.2308 27.7418 81.5209 27.3791 82.0649 27.3791C82.2824 27.3791 82.5 27.4154 82.7176 27.4879C82.9352 27.5605 83.2616 27.7055 83.6242 27.8506C84.8572 28.3945 86.1989 28.8297 87.6132 29.1198C89.0638 29.4099 90.478 29.555 91.9286 29.555C94.2132 29.555 95.9901 29.1561 97.2231 28.3583C98.4561 27.5605 99.1088 26.4 99.1088 24.9132C99.1088 23.8978 98.7824 23.0638 98.1297 22.3747C97.477 21.6857 96.244 21.0693 94.4671 20.489L89.2088 18.8572C86.5616 18.0231 84.6033 16.7901 83.4066 15.1583C82.2099 13.5627 81.5934 11.7857 81.5934 9.90002C81.5934 8.37695 81.9198 7.03519 82.5726 5.87475C83.2253 4.71431 84.0956 3.69893 85.1835 2.90112C86.2715 2.06706 87.5044 1.45057 88.955 1.01541C90.4055 0.580244 91.9286 0.398926 93.5242 0.398926C94.322 0.398926 95.1561 0.43519 95.9539 0.543981C96.7879 0.652772 97.5495 0.797827 98.311 0.942882C99.0363 1.1242 99.7253 1.30552 100.378 1.5231C101.031 1.74068 101.538 1.95827 101.901 2.17585C102.409 2.46596 102.771 2.75607 102.989 3.08244C103.207 3.37255 103.315 3.77145 103.315 4.27915V5.98354C103.315 6.74508 103.025 7.14398 102.481 7.14398C102.191 7.14398 101.72 6.99893 101.103 6.70882C99.0363 5.76596 96.7154 5.29453 94.1407 5.29453C92.0737 5.29453 90.4418 5.6209 89.3176 6.30991C88.1934 6.99893 87.6132 8.05057 87.6132 9.53739C87.6132 10.5528 87.9759 11.4231 88.7011 12.1121C89.4264 12.8011 90.7682 13.4901 92.6901 14.1066L97.8396 15.7385C100.451 16.5726 102.336 17.733 103.46 19.2198C104.585 20.7066 105.129 22.411 105.129 24.2967C105.129 25.8561 104.802 27.2704 104.186 28.5033C103.533 29.7363 102.663 30.8242 101.538 31.6945C100.414 32.6011 99.0726 33.2539 97.5132 33.7253C95.8813 34.233 94.177 34.4868 92.3275 34.4868Z" fill="#252F3E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M99.1813 52.1111C87.2505 60.9232 69.9165 65.6012 55.0121 65.6012C34.1242 65.6012 15.3033 57.877 1.0879 45.0397C-0.0362783 44.0243 0.979106 42.6463 2.32086 43.4441C17.6967 52.365 36.6626 57.7683 56.2813 57.7683C69.5176 57.7683 84.0593 55.0122 97.4407 49.3551C99.4352 48.4485 101.14 50.6606 99.1813 52.1111Z" fill="#FF9900"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M104.149 46.454C102.626 44.4957 94.0681 45.5111 90.1879 45.9825C89.0274 46.1276 88.8461 45.1122 89.8978 44.3507C96.7154 39.5639 107.921 40.9419 109.226 42.5375C110.532 44.1693 108.864 55.3748 102.481 60.7419C101.502 61.5759 100.559 61.1408 100.994 60.0529C102.445 56.4628 105.673 48.3759 104.149 46.454Z" fill="#FF9900"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
|
@ -1 +1,4 @@
|
|||
<svg width="2500" height="1719" viewBox="0 0 256 176" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M57.838 170.017c.151 1.663-.051 3.789-.14 5.436h56.864c.053-1.654.091-3.311.091-4.974 0-39.942-15.768-76.266-44.011-104.51C56.704 52.032 40.885 41.31 23.246 33.898L0 86.328c33.989 15.82 54.211 43.783 57.838 83.689zm69.197-1.644c.108 2.371-.062 4.732-.167 7.08h58.177c.077-2.355.13-4.714.13-7.08 0-28.826-5.66-56.82-16.82-83.207-10.767-25.456-26.169-48.306-45.778-67.915a216.421 216.421 0 0 0-15.686-14.218l-37.68 44.315c37.293 33.313 55.304 65.858 57.824 121.025zM235.263 64.39C226.595 41.785 213.935 19.521 198.727 0l-46.95 34.442c27.495 35.099 44.442 79.71 46.058 127.612.152 4.502-.164 8.969-.457 13.399h58.252c.226-4.448.447-8.916.344-13.399-.805-34.945-8.23-65.12-20.71-97.665z" fill="#3676A1"/></svg>
|
||||
<svg width="58" height="80" viewBox="0 0 58 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.9431 55.1235C30.7258 55.1235 32.1709 53.6784 32.1709 51.8957C32.1709 50.113 30.7258 48.6678 28.9431 48.6678C27.1604 48.6678 25.7153 50.113 25.7153 51.8957C25.7153 53.6784 27.1604 55.1235 28.9431 55.1235Z" fill="#303F9F"/>
|
||||
<path d="M28.9431 78.9612C21.7674 78.9532 14.8878 76.0991 9.81374 71.0251C4.7397 65.9511 1.8856 59.0715 1.87762 51.8957V38.4743C1.87762 37.9402 2.08961 37.428 2.46701 37.0502C2.8444 36.6724 3.35635 36.4598 3.89038 36.4592H13.4904L13.4579 5.38672L5.90313 10.036V27.7287C5.90313 28.2626 5.69107 28.7745 5.31361 29.152C4.93615 29.5294 4.42419 29.7415 3.89038 29.7415C3.35656 29.7415 2.84461 29.5294 2.46715 29.152C2.08968 28.7745 1.87762 28.2626 1.87762 27.7287V9.80643C1.87915 9.18865 2.03815 8.58147 2.33962 8.04224C2.64108 7.50301 3.07505 7.04955 3.60052 6.72469L11.9692 1.57455C12.5174 1.23696 13.1458 1.05179 13.7895 1.03816C14.4331 1.02454 15.0688 1.18295 15.6308 1.49703C16.1928 1.81112 16.6608 2.26951 16.9865 2.82488C17.3121 3.38025 17.4837 4.01247 17.4834 4.65629L17.5182 36.4592H28.9431C31.9963 36.4587 34.981 37.3637 37.5198 39.0596C40.0587 40.7555 42.0376 43.1662 43.2063 45.9868C44.375 48.8074 44.681 51.9113 44.0856 54.9058C43.4903 57.9003 42.0203 60.6511 39.8615 62.8102C37.7028 64.9692 34.9523 66.4396 31.9578 67.0355C28.9634 67.6313 25.8595 67.3257 23.0387 66.1574C20.2179 64.9891 17.8069 63.0106 16.1106 60.472C14.4143 57.9334 13.5089 54.9489 13.5089 51.8957L13.495 40.487H5.90313V51.8957C5.90313 56.4526 7.2544 60.9071 9.78607 64.696C12.3177 68.485 15.9161 71.4381 20.1261 73.1819C24.3361 74.9257 28.9687 75.382 33.438 74.493C37.9073 73.604 42.0127 71.4097 45.2349 68.1874C48.4571 64.9652 50.6514 60.8599 51.5404 56.3906C52.4294 51.9213 51.9732 47.2887 50.2293 43.0787C48.4855 38.8687 45.5324 35.2703 41.7435 32.7386C37.9546 30.207 33.5 28.8557 28.9431 28.8557H25.451C24.9171 28.8557 24.4052 28.6436 24.0277 28.2662C23.6503 27.8887 23.4382 27.3768 23.4382 26.843C23.4382 26.3091 23.6503 25.7972 24.0277 25.4197C24.4052 25.0423 24.9171 24.8302 25.451 24.8302H28.9431C36.1214 24.8302 43.0056 27.6817 48.0813 32.7575C53.1571 37.8333 56.0086 44.7175 56.0086 51.8957C56.0086 59.0739 53.1571 65.9581 48.0813 71.0339C43.0056 76.1097 36.1214 78.9612 28.9431 78.9612V78.9612ZM17.5228 40.487V51.8934C17.5224 54.1499 18.1911 56.3559 19.4444 58.2324C20.6978 60.1088 22.4794 61.5715 24.564 62.4353C26.6486 63.2992 28.9426 63.5254 31.1558 63.0855C33.3691 62.6455 35.4021 61.5591 36.9979 59.9637C38.5937 58.3683 39.6805 56.3354 40.1208 54.1223C40.5612 51.9092 40.3354 49.6151 39.472 47.5303C38.6086 45.4455 37.1463 43.6636 35.2701 42.4099C33.3939 41.1562 31.1881 40.487 28.9315 40.487H17.5228Z" fill="#303F9F"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 2.7 KiB |
6
frontend/app/assets/integrations/google-cloud.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="98" height="79" viewBox="0 0 98 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M61.7777 21.5287H64.7407L73.1851 13.0842L73.6 9.49903C68.7624 5.22927 62.9163 2.26322 56.6137 0.881054C50.3112 -0.501108 43.7603 -0.253748 37.58 1.59977C31.3996 3.45328 25.7938 6.85177 21.292 11.4742C16.7903 16.0966 13.5412 21.7903 11.8518 28.0176C12.7926 27.632 13.8347 27.5694 14.8148 27.8398L31.7037 25.0546C31.7037 25.0546 32.5629 23.6324 33.0074 23.7213C36.6261 19.7469 41.6271 17.3061 46.9867 16.8985C52.3462 16.4909 57.6588 18.1473 61.837 21.5287H61.7777Z" fill="#EA4335"/>
|
||||
<path d="M85.2149 28.0176C83.2739 20.8698 79.2887 14.4441 73.7482 9.52869L61.8964 21.3805C64.3664 23.3988 66.3459 25.9516 67.6855 28.8464C69.0251 31.7412 69.6899 34.9025 69.6297 38.0916V40.1954C71.0149 40.1954 72.3865 40.4682 73.6663 40.9983C74.946 41.5284 76.1089 42.3053 77.0884 43.2848C78.0678 44.2643 78.8448 45.4271 79.3749 46.7069C79.905 47.9867 80.1778 49.3583 80.1778 50.7435C80.1778 52.1287 79.905 53.5003 79.3749 54.7801C78.8448 56.0599 78.0678 57.2227 77.0884 58.2022C76.1089 59.1817 74.946 59.9586 73.6663 60.4887C72.3865 61.0188 71.0149 61.2916 69.6297 61.2916H48.5334L46.4297 63.425V76.0768L48.5334 78.1805H69.6297C75.5208 78.2264 81.2701 76.3749 86.0273 72.8999C90.7846 69.4248 94.2969 64.5109 96.0449 58.8849C97.7928 53.259 97.6835 47.2198 95.7331 41.6608C93.7826 36.1018 90.0947 31.3181 85.2149 28.0176V28.0176Z" fill="#4285F4"/>
|
||||
<path d="M27.4074 78.0621H48.5037V61.1733H27.4074C25.9044 61.1729 24.419 60.8496 23.0519 60.2251L20.0889 61.1436L11.5852 69.5881L10.8445 72.551C15.6132 76.1519 21.432 78.0881 27.4074 78.0621V78.0621Z" fill="#34A853"/>
|
||||
<path d="M27.4074 23.2768C21.6913 23.3109 16.1286 25.1295 11.4963 28.4786C6.86394 31.8276 3.39326 36.5399 1.56901 41.9571C-0.255244 47.3744 -0.341967 53.2262 1.32095 58.6951C2.98387 64.1641 6.31338 68.9771 10.8445 72.462L23.0815 60.2249C21.5264 59.5224 20.165 58.453 19.1141 57.1086C18.0632 55.7642 17.3541 54.1849 17.0477 52.5062C16.7413 50.8275 16.8468 49.0996 17.355 47.4706C17.8633 45.8416 18.7592 44.3603 19.9658 43.1537C21.1724 41.947 22.6537 41.0512 24.2827 40.5429C25.9117 40.0347 27.6396 39.9292 29.3183 40.2356C30.997 40.542 32.5763 41.251 33.9207 42.302C35.2651 43.3529 36.3345 44.7143 37.0371 46.2694L49.2741 34.0323C46.7056 30.6746 43.3953 27.9565 39.6019 26.0906C35.8085 24.2248 31.6349 23.2617 27.4074 23.2768V23.2768Z" fill="#FBBC05"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -1 +1,12 @@
|
|||
<svg id="CMYK_-_square" data-name="CMYK - square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 681.02 551.55"><defs><style>.cls-1{fill:#0097a0;}.cls-2{fill:#5bc6cc;}.cls-3{fill:#231f20;}</style></defs><title>NewRelic-logo-square</title><g id="outlines"><path class="cls-1" d="M692.8,220.54C660.86,73.7,484.77-12.68,299.47,27.61s-309.63,192-277.7,338.83,208,233.22,393.32,192.93S724.72,367.37,692.8,220.54ZM344.87,476.79c-103.41,0-187.2-83.82-187.2-187.22s83.8-187.19,187.2-187.19,187.2,83.81,187.2,187.19S448.25,476.79,344.87,476.79Z" transform="translate(-16.78 -17.71)"/><path class="cls-2" d="M391.53,57.56c-132.32,0-239.61,107.28-239.61,239.6S259.21,536.78,391.53,536.78,631.15,429.49,631.15,297.16,523.85,57.56,391.53,57.56ZM344.87,473.78c-101.75,0-184.19-82.47-184.19-184.21S243.12,105.4,344.87,105.4,529,187.85,529,289.57,446.58,473.78,344.87,473.78Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M278.93,271.2l-20.19-42.33c-4.82-10-9.77-21.36-11.46-26.7l-.39.39c.65,7.55.78,17.06.91,25l.52,43.63H233.61V181.08h16.93l21.88,44a164.17,164.17,0,0,1,9.25,23.18l.39-.39c-.39-4.56-1.3-17.45-1.3-25.66l-.26-41.15h14.2V271.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M321.51,242.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.73a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.27,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H346C346,220.93,341.31,214.55,334.15,214.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M437,271.46H423.61l-8.07-30.34c-2.08-7.81-4.3-18-4.3-18H411s-1,6.51-4.3,18.62l-7.94,29.69H385.32l-18-65.25,14.2-2,7.16,31.91c1.82,8.2,3.39,17.32,3.39,17.32h.39a178.91,178.91,0,0,1,3.78-17.71l8.47-30.47h14.07L426.22,235c2.74,10.68,4.17,18.75,4.17,18.75h.39s1.56-10,3.26-17.71l6.77-30.74h14.85Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M267.62,387.2l-7.81-13.94c-6.25-11.07-10.42-17.32-15.37-22.27a7.64,7.64,0,0,0-5.86-2.73V387.2H223.86V297.08h27.48c20.19,0,29.3,11.72,29.3,25.79,0,12.89-8.33,24.75-22.4,24.75,3.26,1.69,9.25,10.42,13.93,18l13.28,21.62Zm-20.84-78h-8.21v28.52h7.68c7.81,0,12-1,14.72-3.78,2.47-2.47,4-6.25,4-10.94C265,313.88,260.06,309.19,246.78,309.19Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M305.12,358.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.72a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.28,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H329.6C329.6,336.93,324.92,330.55,317.75,330.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M371.28,388.63c-14.46,0-14.46-13-14.46-18.62V313.88a106.72,106.72,0,0,0-1.3-19.27l14.72-3.26c1,4,1.17,9.51,1.17,18.1v55.87c0,8.86.39,10.29,1.43,11.85a4,4,0,0,0,4.69,1l2.34,8.86A22.44,22.44,0,0,1,371.28,388.63Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M396.15,311.53A9.34,9.34,0,0,1,386.9,302a9.44,9.44,0,1,1,9.25,9.51ZM389,387.2V322.34l14.46-2.6V387.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M444.46,388.89c-18,0-28-12.63-28-33.86,0-24,14.33-35.42,29-35.42,7.16,0,12.37,1.69,18.23,7.16l-7.16,9.51c-3.91-3.52-7.29-5.08-11.07-5.08a11.2,11.2,0,0,0-10.42,6.64c-2,4-2.73,10.16-2.73,18.36,0,9,1.43,14.72,4.43,18A11.58,11.58,0,0,0,445.5,378c4.56,0,9-2.21,13.28-6.51l6.77,8.72C459.57,386.16,453.32,388.89,444.46,388.89Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M477.78,388.64A9.67,9.67,0,1,1,487.4,379,9.63,9.63,0,0,1,477.78,388.64Zm0-17.42a7.78,7.78,0,1,0,7.44,7.75A7.55,7.55,0,0,0,477.78,371.22Zm1.9,13.1c-.42-.73-.6-1-1-1.79-1.07-2-1.4-2.5-1.79-2.65a.72.72,0,0,0-.34-.08v4.52H474.4V373.48h4a3,3,0,0,1,3.2,3.17,2.78,2.78,0,0,1-2.42,3,2.47,2.47,0,0,1,.44.47c.62.78,2.6,4.21,2.6,4.21Zm-1.14-8.94a4.35,4.35,0,0,0-1.22-.16h-.78v2.94h.73c.94,0,1.35-.1,1.64-.36a1.53,1.53,0,0,0,.42-1.09A1.28,1.28,0,0,0,478.53,375.38Z" transform="translate(-16.78 -17.71)"/></g></svg>
|
||||
<svg width="71" height="81" viewBox="0 0 71 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_2090)">
|
||||
<path d="M57.0479 28.0403V52.9611L35.3182 65.424V81.0001L70.6399 60.7517V20.2498L57.0479 28.0403Z" fill="#00AC69"/>
|
||||
<path d="M35.3215 15.5812L57.0512 28.039L70.6433 20.2484L35.3215 0L0 20.2484L13.5868 28.039L35.3215 15.5812Z" fill="#1CE783"/>
|
||||
<path d="M21.7348 48.2938V73.2146L35.3215 81.0001V40.5032L-6.10352e-05 20.2498V35.8309L21.7348 48.2938Z" fill="black" fill-opacity="0.87"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_2090">
|
||||
<rect width="70.8739" height="81" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 633 B |
|
|
@ -1,20 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="rollbar-mark-color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 304 240" style="enable-background:new 0 0 304 240;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#3A4757;}
|
||||
.st1{fill:#F7941D;}
|
||||
.st2{fill:#BFD730;}
|
||||
.st3{fill:#00BAD9;}
|
||||
</style>
|
||||
<title>rollbar-logo-color-vertical</title>
|
||||
<g id="icon">
|
||||
<path class="st0" d="M303.8,239.1V25.7c-0.5-13.6-0.9-34.3-31.4-21.9C221.7,22.4,170.6,40.2,120.3,60C82.2,75,40.5,91.4,19,171.6
|
||||
c-5.6,21-13.4,46.4-19,67.5h49.4c4.6-17,10.2-38.4,14.8-55.4c15.4-57.4,45.6-69.3,73.2-80.1C176.9,88,217,73.8,257,59.1v179.9
|
||||
H303.8z"/>
|
||||
<path class="st1" d="M119,124.5c-5,2.8-9.8,6.1-14.1,9.9c-14.9,13.3-23,32.2-28,51.1l-14.1,51.9H119V124.5z"/>
|
||||
<path class="st2" d="M180.1,99.7c-12.7,4.7-25.3,9.6-38,14.5c-3.4,1.3-6.7,2.6-10,4v119.2H180L180.1,99.7z"/>
|
||||
<path class="st3" d="M243.8,237.4v-161c-16.8,6.1-33.7,12.2-50.5,18.4v142.6H243.8z"/>
|
||||
<svg width="88" height="71" viewBox="0 0 88 71" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_2051)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M84.9393 2.09491C84.9382 1.95919 84.9245 1.82386 84.8988 1.69059C84.8988 1.65016 84.8785 1.61377 84.8664 1.57333C84.8543 1.5329 84.8138 1.39139 84.7815 1.30244L84.721 1.17306C84.6834 1.09588 84.64 1.02156 84.5916 0.950679C84.5632 0.906204 84.5351 0.86173 84.5027 0.817255L84.4459 0.740435C84.4136 0.704046 84.3773 0.675744 84.345 0.643397L84.264 0.546361L84.1993 0.501886C84.1401 0.451215 84.0781 0.403969 84.0134 0.360372L83.8556 0.259293C83.7862 0.222856 83.7146 0.190456 83.6415 0.162255L83.4717 0.0935209C83.3949 0.0692618 83.3139 0.0571321 83.2371 0.0409594L83.0633 0.00457045C82.9676 -0.00152348 82.8718 -0.00152348 82.7761 0.00457045H82.6144C81.531 0.101607 67.214 1.46821 51.122 8.64491C41.4586 12.9428 33.9786 19.5414 29.2724 27.5509L28.0596 28.0766C10.8314 35.7666 0.537354 50.7145 0.537354 68.0637V68.3549C0.537763 68.7543 0.652111 69.1453 0.866967 69.4819C1.08183 69.8185 1.38827 70.0867 1.75031 70.2553C2.02817 70.3826 2.33014 70.4488 2.63578 70.4493H59.8268C59.9381 70.4499 60.049 70.4404 60.1584 70.4209L60.3041 70.3847C60.3727 70.3644 60.4416 70.3523 60.5103 70.3281C60.5789 70.3039 60.6112 70.2795 60.6638 70.2553C60.7164 70.2311 60.7851 70.2069 60.8419 70.1746C60.9486 70.1114 61.0499 70.0397 61.1451 69.9602L84.1914 50.5286C84.427 50.3306 84.6158 50.0829 84.7444 49.8031C84.873 49.5235 84.9382 49.219 84.9353 48.9111V2.09491H84.9393ZM64.1005 61.9788L61.889 63.8427V22.501L80.7466 6.60713V47.9489L64.1005 61.9788ZM26.4503 51.0177H57.696V66.2567H8.36905L26.4503 51.0177ZM52.8241 12.4779C60.1892 9.23502 67.8935 6.82515 75.7937 5.29308L58.9375 19.505C51.1569 20.4543 43.5042 22.2563 36.1176 24.8784C40.2903 19.7839 45.9182 15.5588 52.8241 12.4779ZM32.1108 30.9068C40.3192 27.528 48.9123 25.1719 57.696 23.8918V46.825H27.825C28.0404 41.2644 29.5051 35.8237 32.1108 30.9068ZM26.3573 33.5835C24.5907 38.1675 23.6516 43.0286 23.5836 47.9408L4.98487 63.6365C6.39191 50.8844 13.9648 40.2183 26.3573 33.5835Z" fill="#3569F3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_2051">
|
||||
<rect width="86.7778" height="71" fill="white" transform="translate(0.537354)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -52,7 +52,7 @@ export default class Client extends React.PureComponent {
|
|||
<div className={ styles.tabMenu }>
|
||||
<PreferencesMenu activeTab={activeTab} />
|
||||
</div>
|
||||
<div className={ styles.tabContent }>
|
||||
<div className="bg-white w-full rounded-lg mx-4 my-6 p-5">
|
||||
{ activeTab && this.renderActiveTab() }
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,59 +4,74 @@ import { edit, save } from 'Duck/customField';
|
|||
import { Form, Input, Button, Message } from 'UI';
|
||||
import styles from './customFieldForm.module.css';
|
||||
|
||||
@connect(state => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]),
|
||||
}), {
|
||||
edit,
|
||||
save,
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
saving: state.getIn(['customFields', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{
|
||||
edit,
|
||||
save,
|
||||
}
|
||||
)
|
||||
class CustomFieldForm extends React.PureComponent {
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [ name ]: value });
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [name]: value });
|
||||
|
||||
render() {
|
||||
const { field, errors} = this.props;
|
||||
const exists = field.exists();
|
||||
return (
|
||||
<Form className={ styles.wrapper }>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="key"
|
||||
value={ field.key }
|
||||
onChange={ this.write }
|
||||
placeholder="Field Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
render() {
|
||||
const { field, errors } = this.props;
|
||||
const exists = field.exists();
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{exists ? 'Update' : 'Add'} Metadata Field</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Field Name'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="key"
|
||||
value={field.key}
|
||||
onChange={this.write}
|
||||
placeholder="Field Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
{ errors &&
|
||||
<div className="mb-3">
|
||||
{ errors.map(error => <Message visible={ errors } size="mini" error key={ error } className={ styles.error }>{ error }</Message>) }
|
||||
</div>
|
||||
}
|
||||
{errors && (
|
||||
<div className="mb-3">
|
||||
{errors.map((error) => (
|
||||
<Message visible={errors} size="mini" error key={error} className={styles.error}>
|
||||
{error}
|
||||
</Message>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={ () => this.props.onSave(field) }
|
||||
disabled={ !field.validate() }
|
||||
loading={ this.props.saving }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ exists ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
<Button
|
||||
data-hidden={ !exists }
|
||||
onClick={ this.props.onClose }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={() => this.props.onSave(field)}
|
||||
disabled={!field.validate()}
|
||||
loading={this.props.saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{exists ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
<Button data-hidden={!exists} onClick={this.props.onClose}>
|
||||
{'Cancel'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button variant="text" icon="trash" data-hidden={!exists} onClick={this.props.onDelete}></Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomFieldForm;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { IconButton, SlideModal, Loader, NoContent, Icon, TextLink } from 'UI';
|
||||
import { Button, Loader, NoContent, TextLink } from 'UI';
|
||||
import { init, fetchList, save, remove } from 'Duck/customField';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import styles from './customFields.module.css';
|
||||
|
|
@ -10,121 +10,118 @@ import CustomFieldForm from './CustomFieldForm';
|
|||
import ListItem from './ListItem';
|
||||
import { confirm } from 'UI';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
@connect(state => ({
|
||||
fields: state.getIn(['customFields', 'list']).sortBy(i => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
errors: state.getIn([ 'customFields', 'saveRequest', 'errors' ]),
|
||||
}), {
|
||||
init,
|
||||
fetchList,
|
||||
save,
|
||||
remove,
|
||||
})
|
||||
@withPageTitle('Metadata - OpenReplay Preferences')
|
||||
class CustomFields extends React.Component {
|
||||
state = { showModal: false, currentSite: this.props.sites.get(0), deletingItem: null };
|
||||
function CustomFields(props) {
|
||||
const [currentSite, setCurrentSite] = React.useState(props.sites.get(0));
|
||||
const [deletingItem, setDeletingItem] = React.useState(null);
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
componentWillMount() {
|
||||
const activeSite = this.props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
|
||||
this.props.fetchList(activeSite.id);
|
||||
}
|
||||
useEffect(() => {
|
||||
const activeSite = props.sites.get(0);
|
||||
if (!activeSite) return;
|
||||
|
||||
save = (field) => {
|
||||
const { currentSite } = this.state;
|
||||
this.props.save(currentSite.id, field).then(() => {
|
||||
const { errors } = this.props;
|
||||
if (!errors || errors.size === 0) {
|
||||
return this.closeModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
props.fetchList(activeSite.id);
|
||||
}, []);
|
||||
|
||||
closeModal = () => this.setState({ showModal: false });
|
||||
init = (field) => {
|
||||
this.props.init(field);
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
|
||||
onChangeSelect = ({ value }) => {
|
||||
const site = this.props.sites.find(s => s.id === value.value);
|
||||
this.setState({ currentSite: site })
|
||||
this.props.fetchList(site.id);
|
||||
}
|
||||
|
||||
removeMetadata = async (field) => {
|
||||
if (await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`
|
||||
})) {
|
||||
const { currentSite } = this.state;
|
||||
this.setState({ deletingItem: field.index });
|
||||
this.props.remove(currentSite.id, field.index)
|
||||
.then(() => this.setState({ deletingItem: null }));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fields, field, loading } = this.props;
|
||||
const { showModal, currentSite, deletingItem } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<SlideModal
|
||||
title={ `${ (field.exists() ? 'Update' : 'Add') + ' Metadata Field' }` }
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ showModal && <CustomFieldForm onClose={ this.closeModal } onSave={ this.save } /> }
|
||||
onClose={ this.closeModal }
|
||||
/>
|
||||
<div className={ styles.tabHeader }>
|
||||
<h3 className={ cn(styles.tabTitle, "text-2xl") }>{ 'Metadata' }</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown
|
||||
value={ currentSite && currentSite.id }
|
||||
onChange={ this.onChangeSelect }
|
||||
/>
|
||||
</div>
|
||||
<IconButton circle icon="plus" outline onClick={ () => this.init() } />
|
||||
<TextLink
|
||||
icon="book"
|
||||
className="ml-auto color-gray-medium"
|
||||
href="https://docs.openreplay.com/installation/metadata"
|
||||
label="Documentation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Loader loading={ loading }>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
</div>
|
||||
const save = (field) => {
|
||||
props.save(currentSite.id, field).then(() => {
|
||||
const { errors } = props;
|
||||
if (!errors || errors.size === 0) {
|
||||
hideModal();
|
||||
}
|
||||
size="small"
|
||||
show={ fields.size === 0 }
|
||||
// animatedIcon="empty-state"
|
||||
>
|
||||
<div className={ styles.list }>
|
||||
{ fields.filter(i => i.index).map(field => (
|
||||
<ListItem
|
||||
disabled={deletingItem && deletingItem === field.index}
|
||||
key={ field._key }
|
||||
field={ field }
|
||||
onEdit={ this.init }
|
||||
onDelete={ () => this.removeMetadata(field) }
|
||||
});
|
||||
};
|
||||
|
||||
const init = (field) => {
|
||||
props.init(field);
|
||||
showModal(<CustomFieldForm onClose={hideModal} onSave={save} onDelete={() => removeMetadata(field)} />);
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }) => {
|
||||
const site = props.sites.find((s) => s.id === value.value);
|
||||
setCurrentSite(site);
|
||||
props.fetchList(site.id);
|
||||
};
|
||||
|
||||
const removeMetadata = async (field) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Metadata',
|
||||
confirmation: `Are you sure you want to remove?`,
|
||||
})
|
||||
) {
|
||||
setDeletingItem(field.index);
|
||||
props
|
||||
.remove(currentSite.id, field.index)
|
||||
.then(() => {
|
||||
hideModal();
|
||||
})
|
||||
.finally(() => {
|
||||
setDeletingItem(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { fields, loading } = props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.tabHeader}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
<SiteDropdown value={currentSite && currentSite.id} onChange={onChangeSelect} />
|
||||
</div>
|
||||
<Button rounded={true} icon="plus" variant="outline" onClick={() => init()} />
|
||||
<TextLink
|
||||
icon="book"
|
||||
className="ml-auto color-gray-medium"
|
||||
href="https://docs.openreplay.com/installation/metadata"
|
||||
label="Documentation"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No data available.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={fields.size === 0}
|
||||
>
|
||||
<div className={styles.list}>
|
||||
{fields
|
||||
.filter((i) => i.index)
|
||||
.map((field) => (
|
||||
<ListItem
|
||||
disabled={deletingItem && deletingItem === field.index}
|
||||
key={field._key}
|
||||
field={field}
|
||||
onEdit={init}
|
||||
// onDelete={ () => removeMetadata(field) }
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CustomFields;
|
||||
export default connect(
|
||||
(state) => ({
|
||||
fields: state.getIn(['customFields', 'list']).sortBy((i) => i.index),
|
||||
field: state.getIn(['customFields', 'instance']),
|
||||
loading: state.getIn(['customFields', 'fetchRequest', 'loading']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
errors: state.getIn(['customFields', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
save,
|
||||
remove,
|
||||
}
|
||||
)(withPageTitle('Metadata - OpenReplay Preferences')(CustomFields));
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames'
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import styles from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ field, onEdit, onDelete, disabled }) => {
|
||||
return (
|
||||
<div className={ cn(styles.wrapper, field.index === 0 ? styles.preDefined : '', { [styles.disabled] : disabled} ) } onClick={ () => field.index != 0 && onEdit(field) } >
|
||||
<span>{ field.key }</span>
|
||||
<div className={ styles.actions } data-hidden={ field.index === 0}>
|
||||
<div className={ styles.button } onClick={ (e) => { e.stopPropagation(); onDelete(field) } }>
|
||||
const ListItem = ({ field, onEdit, disabled }) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.wrapper, field.index === 0 ? styles.preDefined : '', { [styles.disabled]: disabled })}
|
||||
onClick={() => field.index != 0 && onEdit(field)}
|
||||
>
|
||||
<span>{field.key}</span>
|
||||
<div className={styles.actions} data-hidden={field.index === 0}>
|
||||
{/* <div className={ styles.button } onClick={ (e) => { e.stopPropagation(); onDelete(field) } }>
|
||||
<Icon name="trash" color="teal" size="16" />
|
||||
</div> */}
|
||||
<div className={styles.button}>
|
||||
<Icon name="edit" color="teal" size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ styles.button }>
|
||||
<Icon name="edit" color="teal" size="18"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
|
|
|
|||
|
|
@ -1,59 +1,56 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import AssistScript from './AssistScript'
|
||||
import AssistNpm from './AssistNpm'
|
||||
import AssistScript from './AssistScript';
|
||||
import AssistNpm from './AssistNpm';
|
||||
import { Tabs } from 'UI';
|
||||
import { useState } from 'react';
|
||||
|
||||
const NPM = 'NPM'
|
||||
const SCRIPT = 'SCRIPT'
|
||||
const NPM = 'NPM';
|
||||
const SCRIPT = 'SCRIPT';
|
||||
const TABS = [
|
||||
{ key: SCRIPT, text: SCRIPT },
|
||||
{ key: NPM, text: NPM },
|
||||
]
|
||||
{ key: SCRIPT, text: SCRIPT },
|
||||
{ key: NPM, text: NPM },
|
||||
];
|
||||
|
||||
const AssistDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
const [activeTab, setActiveTab] = useState(SCRIPT)
|
||||
|
||||
const { projectKey } = props;
|
||||
const [activeTab, setActiveTab] = useState(SCRIPT);
|
||||
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case SCRIPT:
|
||||
return <AssistScript projectKey={projectKey} />
|
||||
case NPM:
|
||||
return <AssistNpm projectKey={projectKey} />
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const renderActiveTab = () => {
|
||||
switch (activeTab) {
|
||||
case SCRIPT:
|
||||
return <AssistScript projectKey={projectKey} />;
|
||||
case NPM:
|
||||
return <AssistNpm projectKey={projectKey} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Assist</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them
|
||||
without requiring any 3rd-party screen sharing software.
|
||||
</div>
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>OpenReplay Assist allows you to support your users by seeing their live screen and instantly hopping on call (WebRTC) with them without requiring any 3rd-party screen sharing software.</div>
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-assist`}</Highlight>
|
||||
<div className="mb-4" />
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-assist`}
|
||||
</Highlight>
|
||||
<div className="mb-4" />
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<Tabs tabs={TABS} active={activeTab} onClick={(tab) => setActiveTab(tab)} />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
active={ activeTab } onClick={ (tab) => setActiveTab(tab) }
|
||||
/>
|
||||
<div className="py-5">{renderActiveTab()}</div>
|
||||
|
||||
<div className="py-5">
|
||||
{ renderActiveTab() }
|
||||
</div>
|
||||
|
||||
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Install Assist" url="https://docs.openreplay.com/installation/assist" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AssistDoc.displayName = "AssistDoc";
|
||||
AssistDoc.displayName = 'AssistDoc';
|
||||
|
||||
export default AssistDoc;
|
||||
|
|
|
|||
|
|
@ -1,40 +1,46 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const AxiosDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-axios`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires axios@^0.21.2 as a peer dependency.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Axios</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture axios requests and inspect them later on while replaying session recordings. This is very useful
|
||||
for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-axios`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>
|
||||
Initialize the @openreplay/tracker package as usual then load the axios plugin. Note that OpenReplay axios plugin requires
|
||||
axios@^0.21.2 as a peer dependency.
|
||||
</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
import trackerAxios from '@openreplay/tracker-axios';
|
||||
const tracker = new OpenReplay({
|
||||
projectKey: '${projectKey}'
|
||||
});
|
||||
tracker.use(trackerAxios(options)); // check list of available options below
|
||||
tracker.start();`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerAxios from '@openreplay/tracker-axios/cjs';
|
||||
const tracker = new OpenReplay({
|
||||
projectKey: '${projectKey}'
|
||||
|
|
@ -47,15 +53,16 @@ function MyApp() {
|
|||
}, [])
|
||||
//...
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/axios" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/axios" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AxiosDoc.displayName = "AxiosDoc";
|
||||
AxiosDoc.displayName = 'AxiosDoc';
|
||||
|
||||
export default AxiosDoc;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,35 @@
|
|||
import React from 'react';
|
||||
import { tokenRE } from 'Types/integrations/bugsnagConfig';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import ProjectListDropdown from './ProjectListDropdown';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const BugsnagForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Bugsnag</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Bugsnag with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Bugsnag" url="https://docs.openreplay.com/integrations/bugsnag" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="bugsnag"
|
||||
formFields={[
|
||||
{
|
||||
key: 'authorizationToken',
|
||||
label: 'Authorisation Token',
|
||||
},
|
||||
{
|
||||
key: 'bugsnagProjectId',
|
||||
label: 'Project',
|
||||
checkIfDisplayed: (config) => tokenRE.test(config.authorizationToken),
|
||||
component: ProjectListDropdown,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="bugsnag"
|
||||
formFields={[ {
|
||||
key: "authorizationToken",
|
||||
label: "Authorisation Token",
|
||||
}, {
|
||||
key: "bugsnagProjectId",
|
||||
label: "Project",
|
||||
checkIfDisplayed: config => tokenRE.test(config.authorizationToken),
|
||||
component: ProjectListDropdown,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
BugsnagForm.displayName = "BugsnagForm";
|
||||
BugsnagForm.displayName = 'BugsnagForm';
|
||||
|
||||
export default BugsnagForm;
|
||||
export default BugsnagForm;
|
||||
|
|
|
|||
|
|
@ -1,43 +1,48 @@
|
|||
import React from 'react';
|
||||
import { ACCESS_KEY_ID_LENGTH, SECRET_ACCESS_KEY_LENGTH } from 'Types/integrations/cloudwatchConfig';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import LogGroupDropdown from './LogGroupDropdown';
|
||||
import RegionDropdown from './RegionDropdown';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const CloudwatchForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Cloud Watch</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate CloudWatch with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate CloudWatch" url="https://docs.openreplay.com/integrations/cloudwatch" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="cloudwatch"
|
||||
formFields={[
|
||||
{
|
||||
key: 'awsAccessKeyId',
|
||||
label: 'AWS Access Key ID',
|
||||
},
|
||||
{
|
||||
key: 'awsSecretAccessKey',
|
||||
label: 'AWS Secret Access Key',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
component: RegionDropdown,
|
||||
},
|
||||
{
|
||||
key: 'logGroupName',
|
||||
label: 'Log Group Name',
|
||||
component: LogGroupDropdown,
|
||||
checkIfDisplayed: (config) =>
|
||||
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
|
||||
config.region !== '' &&
|
||||
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="cloudwatch"
|
||||
formFields={[ {
|
||||
key: "awsAccessKeyId",
|
||||
label: "AWS Access Key ID",
|
||||
}, {
|
||||
key: "awsSecretAccessKey",
|
||||
label: "AWS Secret Access Key",
|
||||
}, {
|
||||
key: "region",
|
||||
label: "Region",
|
||||
component: RegionDropdown,
|
||||
}, {
|
||||
key: "logGroupName",
|
||||
label: "Log Group Name",
|
||||
component: LogGroupDropdown,
|
||||
checkIfDisplayed: config =>
|
||||
config.awsSecretAccessKey.length === SECRET_ACCESS_KEY_LENGTH &&
|
||||
config.region !== '' &&
|
||||
config.awsAccessKeyId.length === ACCESS_KEY_ID_LENGTH
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
CloudwatchForm.displayName = "CloudwatchForm";
|
||||
CloudwatchForm.displayName = 'CloudwatchForm';
|
||||
|
||||
export default CloudwatchForm;
|
||||
export default CloudwatchForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const DatadogForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Datadog</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Datadog with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Datadog" url="https://docs.openreplay.com/integrations/datadog" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="datadog"
|
||||
formFields={[
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
key: 'applicationKey',
|
||||
label: 'Application Key',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="datadog"
|
||||
formFields={[ {
|
||||
key: "apiKey",
|
||||
label: "API Key",
|
||||
autoFocus: true,
|
||||
}, {
|
||||
key: "applicationKey",
|
||||
label: "Application Key",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
DatadogForm.displayName = "DatadogForm";
|
||||
DatadogForm.displayName = 'DatadogForm';
|
||||
|
||||
export default DatadogForm;
|
||||
|
|
|
|||
|
|
@ -1,75 +1,88 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import { withRequest } from 'HOCs';
|
||||
import { edit } from 'Duck/integrations/actions';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
@connect(state => ({
|
||||
config: state.getIn([ 'elasticsearch', 'instance' ])
|
||||
}), { edit })
|
||||
@connect(
|
||||
(state) => ({
|
||||
config: state.getIn(['elasticsearch', 'instance']),
|
||||
}),
|
||||
{ edit }
|
||||
)
|
||||
@withRequest({
|
||||
dataName: "isValid",
|
||||
initialData: false,
|
||||
dataWrapper: data => data.state,
|
||||
requestName: "validateConfig",
|
||||
endpoint: '/integrations/elasticsearch/test',
|
||||
method: 'POST',
|
||||
dataName: 'isValid',
|
||||
initialData: false,
|
||||
dataWrapper: (data) => data.state,
|
||||
requestName: 'validateConfig',
|
||||
endpoint: '/integrations/elasticsearch/test',
|
||||
method: 'POST',
|
||||
})
|
||||
export default class ElasticsearchForm extends React.PureComponent {
|
||||
componentWillReceiveProps(newProps) {
|
||||
const { config: { host, port, apiKeyId, apiKey } } = this.props;
|
||||
const { loading, config } = newProps;
|
||||
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
|
||||
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
|
||||
this.validateConfig(newProps);
|
||||
componentWillReceiveProps(newProps) {
|
||||
const {
|
||||
config: { host, port, apiKeyId, apiKey },
|
||||
} = this.props;
|
||||
const { loading, config } = newProps;
|
||||
const valuesChanged = host !== config.host || port !== config.port || apiKeyId !== config.apiKeyId || apiKey !== config.apiKey;
|
||||
if (!loading && valuesChanged && newProps.config.validateKeys() && newProps) {
|
||||
this.validateConfig(newProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateConfig = (newProps) => {
|
||||
const { config } = newProps;
|
||||
this.props.validateConfig({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
apiKeyId: config.apiKeyId,
|
||||
apiKey: config.apiKey,
|
||||
}).then((res) => {
|
||||
const { isValid } = this.props;
|
||||
this.props.edit('elasticsearch', { isValid: isValid })
|
||||
});
|
||||
}
|
||||
validateConfig = (newProps) => {
|
||||
const { config } = newProps;
|
||||
this.props
|
||||
.validateConfig({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
apiKeyId: config.apiKeyId,
|
||||
apiKey: config.apiKey,
|
||||
})
|
||||
.then((res) => {
|
||||
const { isValid } = this.props;
|
||||
this.props.edit('elasticsearch', { isValid: isValid });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="elasticsearch"
|
||||
formFields={[ {
|
||||
key: "host",
|
||||
label: "Host",
|
||||
}, {
|
||||
key: "apiKeyId",
|
||||
label: "API Key ID",
|
||||
}, {
|
||||
key: "apiKey",
|
||||
label: "API Key",
|
||||
}, {
|
||||
key: "indexes",
|
||||
label: "Indexes",
|
||||
}, {
|
||||
key: "port",
|
||||
label: "Port",
|
||||
type: "number",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
};
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Elasticsearch</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Elasticsearch with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Elasticsearch" url="https://docs.openreplay.com/integrations/elastic" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="elasticsearch"
|
||||
formFields={[
|
||||
{
|
||||
key: 'host',
|
||||
label: 'Host',
|
||||
},
|
||||
{
|
||||
key: 'apiKeyId',
|
||||
label: 'API Key ID',
|
||||
},
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'API Key',
|
||||
},
|
||||
{
|
||||
key: 'indexes',
|
||||
label: 'Indexes',
|
||||
},
|
||||
{
|
||||
key: 'port',
|
||||
label: 'Port',
|
||||
type: 'number',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const FetchDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-fetch --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Use the provided fetch method from the plugin instead of the one built-in.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Fetch</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture fetch payloads and inspect them later on while replaying session recordings. This is very useful
|
||||
for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-fetch --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Use the provided fetch method from the plugin instead of the one built-in.</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import tracker from '@openreplay/tracker';
|
||||
import trackerFetch from '@openreplay/tracker-fetch';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -34,11 +37,11 @@ tracker.start();
|
|||
export const fetch = tracker.use(trackerFetch(<options>)); // check list of available options below
|
||||
//...
|
||||
fetch('https://api.openreplay.com/').then(response => console.log(response.json()));`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerFetch from '@openreplay/tracker-fetch/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -54,15 +57,16 @@ export const fetch = tracker.use(trackerFetch(<options>)); // check list of avai
|
|||
//...
|
||||
fetch('https://api.openreplay.com/').then(response => console.log(response.json()));
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/fetch" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Fetch" url="https://docs.openreplay.com/plugins/fetch" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FetchDoc.displayName = "FetchDoc";
|
||||
FetchDoc.displayName = 'FetchDoc';
|
||||
|
||||
export default FetchDoc;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,31 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const GithubForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
|
||||
</div>
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Github</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>Integrate GitHub with OpenReplay and create issues directly from the recording page.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Github" url="https://docs.openreplay.com/integrations/github" />
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject
|
||||
name="github"
|
||||
customPath="github"
|
||||
formFields={[
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
ignoreProject
|
||||
name="issues"
|
||||
customPath="github"
|
||||
formFields={[
|
||||
{
|
||||
key: "token",
|
||||
label: "Token",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
GithubForm.displayName = "GithubForm";
|
||||
GithubForm.displayName = 'GithubForm';
|
||||
|
||||
export default GithubForm;
|
||||
export default GithubForm;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,36 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
|
||||
const GraphQLDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<p>This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</p>
|
||||
<p>GraphQL plugin is compatible with Apollo and Relay implementations.</p>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-graphql --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It returns result without changes.</p>
|
||||
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">GraphQL</h3>
|
||||
<div className="p-5">
|
||||
<p>
|
||||
This plugin allows you to capture GraphQL requests and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</p>
|
||||
<p>GraphQL plugin is compatible with Apollo and Relay implementations.</p>
|
||||
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-graphql --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>
|
||||
The plugin call will return the function, which receives four variables operationKind, operationName, variables and result. It
|
||||
returns result without changes.
|
||||
</p>
|
||||
|
||||
<div className="py-3" />
|
||||
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
import trackerGraphQL from '@openreplay/tracker-graphql';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -33,11 +39,11 @@ const tracker = new OpenReplay({
|
|||
tracker.start();
|
||||
//...
|
||||
export const recordGraphQL = tracker.use(trackerGraphQL());`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerGraphQL from '@openreplay/tracker-graphql/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -51,15 +57,16 @@ function SomeFunctionalComponent() {
|
|||
}
|
||||
//...
|
||||
export const recordGraphQL = tracker.use(trackerGraphQL());`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate GraphQL" url="https://docs.openreplay.com/plugins/graphql" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate GraphQL" url="https://docs.openreplay.com/plugins/graphql" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GraphQLDoc.displayName = "GraphQLDoc";
|
||||
GraphQLDoc.displayName = 'GraphQLDoc';
|
||||
|
||||
export default GraphQLDoc;
|
||||
|
|
|
|||
|
|
@ -1,144 +1,147 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Input, Form, Button, Checkbox } from 'UI';
|
||||
import { Input, Form, Button, Checkbox, Loader } from 'UI';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import { save, init, edit, remove, fetchList } from 'Duck/integrations/actions';
|
||||
import { fetchIntegrationList } from 'Duck/integrations/integrations';
|
||||
|
||||
@connect((state, { name, customPath }) => ({
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
initialSiteId: state.getIn([ 'site', 'siteId' ]),
|
||||
list: state.getIn([ name, 'list' ]),
|
||||
config: state.getIn([ name, 'instance']),
|
||||
saving: state.getIn([ customPath || name, 'saveRequest', 'loading']),
|
||||
removing: state.getIn([ name, 'removeRequest', 'loading']),
|
||||
}), {
|
||||
save,
|
||||
init,
|
||||
edit,
|
||||
remove,
|
||||
fetchList
|
||||
})
|
||||
@connect(
|
||||
(state, { name, customPath }) => ({
|
||||
sites: state.getIn(['site', 'list']),
|
||||
initialSiteId: state.getIn(['site', 'siteId']),
|
||||
list: state.getIn([name, 'list']),
|
||||
config: state.getIn([name, 'instance']),
|
||||
loading: state.getIn([name, 'fetchRequest', 'loading']),
|
||||
saving: state.getIn([customPath || name, 'saveRequest', 'loading']),
|
||||
removing: state.getIn([name, 'removeRequest', 'loading']),
|
||||
siteId: state.getIn(['integrations', 'siteId']),
|
||||
}),
|
||||
{
|
||||
save,
|
||||
init,
|
||||
edit,
|
||||
remove,
|
||||
fetchList,
|
||||
fetchIntegrationList,
|
||||
}
|
||||
)
|
||||
export default class IntegrationForm extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const currentSiteId = this.props.initialSiteId;
|
||||
this.state = { currentSiteId };
|
||||
this.init(currentSiteId);
|
||||
}
|
||||
|
||||
write = ({ target: { value, name: key, type, checked } }) => {
|
||||
if (type === 'checkbox')
|
||||
this.props.edit(this.props.name, { [ key ]: checked })
|
||||
else
|
||||
this.props.edit(this.props.name, { [ key ]: value })
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// const currentSiteId = this.props.initialSiteId;
|
||||
// this.state = { currentSiteId };
|
||||
// this.init(currentSiteId);
|
||||
}
|
||||
|
||||
onChangeSelect = ({ value }) => {
|
||||
const { sites, list, name } = this.props;
|
||||
const site = sites.find(s => s.id === value.value);
|
||||
this.setState({ currentSiteId: site.id })
|
||||
this.init(value.value);
|
||||
}
|
||||
write = ({ target: { value, name: key, type, checked } }) => {
|
||||
if (type === 'checkbox') this.props.edit(this.props.name, { [key]: checked });
|
||||
else this.props.edit(this.props.name, { [key]: value });
|
||||
};
|
||||
|
||||
init = (siteId) => {
|
||||
const { list, name } = this.props;
|
||||
const config = (parseInt(siteId) > 0) ? list.find(s => s.projectId === siteId) : undefined;
|
||||
this.props.init(name, config ? config : list.first());
|
||||
}
|
||||
// onChangeSelect = ({ value }) => {
|
||||
// const { sites, list, name } = this.props;
|
||||
// const site = sites.find((s) => s.id === value.value);
|
||||
// this.setState({ currentSiteId: site.id });
|
||||
// this.init(value.value);
|
||||
// };
|
||||
|
||||
save = () => {
|
||||
const { config, name, customPath } = this.props;
|
||||
const isExists = config.exists();
|
||||
const { currentSiteId } = this.state;
|
||||
const { ignoreProject } = this.props;
|
||||
this.props.save(customPath || name, (!ignoreProject ? currentSiteId : null), config)
|
||||
.then(() => {
|
||||
this.props.fetchList(name)
|
||||
this.props.onClose();
|
||||
if (isExists) return;
|
||||
});
|
||||
}
|
||||
// init = (siteId) => {
|
||||
// const { list, name } = this.props;
|
||||
// const config = parseInt(siteId) > 0 ? list.find((s) => s.projectId === siteId) : undefined;
|
||||
// this.props.init(name, config ? config : list.first());
|
||||
// };
|
||||
|
||||
remove = () => {
|
||||
const { name, config, ignoreProject } = this.props;
|
||||
this.props.remove(name, !ignoreProject ? config.projectId : null).then(function() {
|
||||
this.props.onClose();
|
||||
this.props.fetchList(name)
|
||||
}.bind(this));
|
||||
}
|
||||
save = () => {
|
||||
const { config, name, customPath, ignoreProject } = this.props;
|
||||
const isExists = config.exists();
|
||||
// const { currentSiteId } = this.state;
|
||||
this.props.save(customPath || name, !ignoreProject ? this.props.siteId : null, config).then(() => {
|
||||
// this.props.fetchList(name);
|
||||
this.props.onClose();
|
||||
if (isExists) return;
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props;
|
||||
const { currentSiteId } = this.state;
|
||||
remove = () => {
|
||||
const { name, config, ignoreProject } = this.props;
|
||||
this.props.remove(name, !ignoreProject ? config.projectId : null).then(
|
||||
function () {
|
||||
this.props.onClose();
|
||||
this.props.fetchList(name);
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ph-20">
|
||||
<Form>
|
||||
{!ignoreProject &&
|
||||
<Form.Field>
|
||||
<label>{ 'OpenReplay Project' }</label>
|
||||
<SiteDropdown
|
||||
value={ currentSiteId }
|
||||
onChange={ this.onChangeSelect }
|
||||
/>
|
||||
</Form.Field>
|
||||
}
|
||||
render() {
|
||||
const { config, saving, removing, formFields, name, loading, ignoreProject } = this.props;
|
||||
// const { currentSiteId } = this.state;
|
||||
|
||||
{ formFields.map(({
|
||||
key,
|
||||
label,
|
||||
placeholder=label,
|
||||
component: Component = 'input',
|
||||
type = "text",
|
||||
checkIfDisplayed,
|
||||
autoFocus=false
|
||||
}) => (typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
|
||||
((type === 'checkbox') ?
|
||||
<Form.Field key={ key }>
|
||||
<Checkbox
|
||||
label={label}
|
||||
name={ key }
|
||||
value={ config[ key ] }
|
||||
onChange={ this.write }
|
||||
placeholder={ placeholder }
|
||||
type={ Component === 'input' ? type : null }
|
||||
/>
|
||||
</Form.Field>
|
||||
:
|
||||
<Form.Field key={ key }>
|
||||
<label>{ label }</label>
|
||||
<Input
|
||||
name={ key }
|
||||
value={ config[ key ] }
|
||||
onChange={ this.write }
|
||||
placeholder={ placeholder }
|
||||
type={ Component === 'input' ? type : null }
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</Form.Field>
|
||||
)
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={ this.save }
|
||||
disabled={ !config.validate() }
|
||||
loading={ saving || loading }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ config.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className="ph-20">
|
||||
<Form>
|
||||
{/* {!ignoreProject && (
|
||||
<Form.Field>
|
||||
<label>{'OpenReplay Project'}</label>
|
||||
<SiteDropdown value={currentSiteId} onChange={this.onChangeSelect} />
|
||||
</Form.Field>
|
||||
)} */}
|
||||
|
||||
{config.exists() && (
|
||||
<Button
|
||||
loading={ removing }
|
||||
onClick={ this.remove }
|
||||
>
|
||||
{ 'Delete' }
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{formFields.map(
|
||||
({
|
||||
key,
|
||||
label,
|
||||
placeholder = label,
|
||||
component: Component = 'input',
|
||||
type = 'text',
|
||||
checkIfDisplayed,
|
||||
autoFocus = false,
|
||||
}) =>
|
||||
(typeof checkIfDisplayed !== 'function' || checkIfDisplayed(config)) &&
|
||||
(type === 'checkbox' ? (
|
||||
<Form.Field key={key}>
|
||||
<Checkbox
|
||||
label={label}
|
||||
name={key}
|
||||
value={config[key]}
|
||||
onChange={this.write}
|
||||
placeholder={placeholder}
|
||||
type={Component === 'input' ? type : null}
|
||||
/>
|
||||
</Form.Field>
|
||||
) : (
|
||||
<Form.Field key={key}>
|
||||
<label>{label}</label>
|
||||
<Input
|
||||
name={key}
|
||||
value={config[key]}
|
||||
onChange={this.write}
|
||||
placeholder={placeholder}
|
||||
type={Component === 'input' ? type : null}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</Form.Field>
|
||||
))
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={this.save}
|
||||
disabled={!config.validate()}
|
||||
loading={saving || loading}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{config.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
|
||||
{config.exists() && (
|
||||
<Button loading={removing} onClick={this.remove}>
|
||||
{'Delete'}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
|
||||
const onDocLinkClick = (e, link) => {
|
||||
e.stopPropagation();
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
|
||||
const IntegrationItem = ({
|
||||
deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false
|
||||
}) => {
|
||||
return (
|
||||
<div className={ cn(stl.wrapper, 'mb-4', { [stl.integrated] : integrated })} onClick={ e => onClick(e, url) }>
|
||||
{integrated && (
|
||||
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
|
||||
<Icon name="check" size="14" color="white" />
|
||||
</div>
|
||||
)}
|
||||
<img className="h-12 w-12" src={'/assets/' + icon + '.svg'} alt="integration" />
|
||||
<h4 className="my-2">{ title }</h4>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default IntegrationItem;
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon, Popup } from 'UI';
|
||||
import stl from './integrationItem.module.css';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
integration: any;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
integrated?: boolean;
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const IntegrationItem = (props: Props) => {
|
||||
const { integration, integrated, hide = false } = props;
|
||||
return hide ? <></> : (
|
||||
<div className={cn(stl.wrapper, 'mb-4', { [stl.integrated]: integrated })} onClick={(e) => props.onClick(e)}>
|
||||
{integrated && (
|
||||
<div className="m-2 absolute right-0 top-0 h-4 w-4 rounded-full bg-teal flex items-center justify-center">
|
||||
<Popup content="Integrated" delay={0}>
|
||||
<Icon name="check" size="14" color="white" />
|
||||
</Popup>
|
||||
</div>
|
||||
)}
|
||||
<img className="h-12 w-12" src={'/assets/' + integration.icon + '.svg'} alt="integration" />
|
||||
<div className="text-center mt-2">
|
||||
<h4 className="">{integration.title}</h4>
|
||||
<p className="text-sm color-gray-medium m-0 p-0 h-3">{integration.subtitle && integration.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect((state: any, props: Props) => {
|
||||
const list = state.getIn([props.integration.slug, 'list']) || [];
|
||||
return {
|
||||
// integrated: props.integration.slug === 'issues' ? !!(list.first() && list.first().token) : list.size > 0,
|
||||
};
|
||||
})(IntegrationItem);
|
||||
166
frontend/app/components/Client/Integrations/Integrations.tsx
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import React, { useEffect } from 'react';
|
||||
import BugsnagForm from './BugsnagForm';
|
||||
import CloudwatchForm from './CloudwatchForm';
|
||||
import DatadogForm from './DatadogForm';
|
||||
import ElasticsearchForm from './ElasticsearchForm';
|
||||
import GithubForm from './GithubForm';
|
||||
import IntegrationItem from './IntegrationItem';
|
||||
import JiraForm from './JiraForm';
|
||||
import NewrelicForm from './NewrelicForm';
|
||||
import RollbarForm from './RollbarForm';
|
||||
import SentryForm from './SentryForm';
|
||||
import SlackForm from './SlackForm';
|
||||
import StackdriverForm from './StackdriverForm';
|
||||
import SumoLogicForm from './SumoLogicForm';
|
||||
import { fetch, init } from 'Duck/integrations/actions';
|
||||
import { fetchIntegrationList, setSiteId } from 'Duck/integrations/integrations';
|
||||
import { connect } from 'react-redux';
|
||||
import SiteDropdown from 'Shared/SiteDropdown';
|
||||
import ReduxDoc from './ReduxDoc';
|
||||
import VueDoc from './VueDoc';
|
||||
import GraphQLDoc from './GraphQLDoc';
|
||||
import NgRxDoc from './NgRxDoc';
|
||||
import MobxDoc from './MobxDoc';
|
||||
import FetchDoc from './FetchDoc';
|
||||
import ProfilerDoc from './ProfilerDoc';
|
||||
import AxiosDoc from './AxiosDoc';
|
||||
import AssistDoc from './AssistDoc';
|
||||
|
||||
interface Props {
|
||||
fetch: (name: string, siteId: string) => void;
|
||||
init: () => void;
|
||||
fetchIntegrationList: (siteId: any) => void;
|
||||
integratedList: any;
|
||||
initialSiteId: string;
|
||||
setSiteId: (siteId: string) => void;
|
||||
siteId: string;
|
||||
}
|
||||
function Integrations(props: Props) {
|
||||
const { initialSiteId } = props;
|
||||
const { showModal } = useModal();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [integratedList, setIntegratedList] = React.useState([]);
|
||||
// const [siteId, setSiteId] = React.useState(props.siteId || initialSiteId);
|
||||
|
||||
useEffect(() => {
|
||||
const list = props.integratedList.filter((item: any) => item.integrated).map((item: any) => item.name);
|
||||
setIntegratedList(list);
|
||||
}, [props.integratedList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.siteId) {
|
||||
props.setSiteId(initialSiteId);
|
||||
props.fetchIntegrationList(initialSiteId);
|
||||
} else {
|
||||
props.fetchIntegrationList(props.siteId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
const onClick = (integration: any) => {
|
||||
if (integration.slug) {
|
||||
props.fetch(integration.slug, props.siteId);
|
||||
}
|
||||
showModal(integration.component, { right: true });
|
||||
};
|
||||
|
||||
const onChangeSelect = ({ value }: any) => {
|
||||
props.setSiteId(value.value);
|
||||
props.fetchIntegrationList(value.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
{integrations.map((cat: any) => (
|
||||
<div className="mb-2 border-b last:border-none py-3">
|
||||
<div className="flex items-center">
|
||||
<h2 className="font-medium text-lg">{cat.title}</h2>
|
||||
{cat.isProject && (
|
||||
<div className="flex flex-wrap ml-4">
|
||||
<SiteDropdown value={props.siteId} onChange={onChangeSelect} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="">{cat.description}</div>
|
||||
|
||||
<div className="flex flex-wrap mt-4">
|
||||
{cat.integrations.map((integration: any) => (
|
||||
<IntegrationItem
|
||||
integrated={integratedList.includes(integration.slug)}
|
||||
key={integration.name}
|
||||
integration={integration}
|
||||
onClick={() => onClick(integration)}
|
||||
hide={(integration.slug === 'github' && integratedList.includes('jira') || integration.slug === 'jira' && integratedList.includes('github'))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
initialSiteId: state.getIn(['site', 'siteId']),
|
||||
integratedList: state.getIn(['integrations', 'list']),
|
||||
siteId: state.getIn(['integrations', 'siteId']),
|
||||
}),
|
||||
{ fetch, init, fetchIntegrationList, setSiteId }
|
||||
)(Integrations);
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
title: 'Issue Reporting and Collaborations',
|
||||
description: 'Seamlessly report issues or share issues with your team right from OpenReplay.',
|
||||
integrations: [
|
||||
{ title: 'Jira', slug: 'jira', category: 'Errors', icon: 'integrations/jira', component: <JiraForm /> },
|
||||
{ title: 'Github', slug: 'github', category: 'Errors', icon: 'integrations/github', component: <GithubForm /> },
|
||||
{ title: 'Slack', category: 'Errors', icon: 'integrations/slack', component: <SlackForm /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Backend Logging',
|
||||
isProject: true,
|
||||
description: 'Sync your backend errors with sessions replays and see what happened front-to-back.',
|
||||
integrations: [
|
||||
{ title: 'Sentry', slug: 'sentry', icon: 'integrations/sentry', component: <SentryForm /> },
|
||||
{ title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: <BugsnagForm /> },
|
||||
{ title: 'Rollbar', slug: 'rollbar', icon: 'integrations/rollbar', component: <RollbarForm /> },
|
||||
{ title: 'Elasticsearch', slug: 'elasticsearch', icon: 'integrations/elasticsearch', component: <ElasticsearchForm /> },
|
||||
{ title: 'Datadog', slug: 'datadog', icon: 'integrations/datadog', component: <DatadogForm /> },
|
||||
{ title: 'Sumo Logic', slug: 'sumologic', icon: 'integrations/sumologic', component: <SumoLogicForm /> },
|
||||
{
|
||||
title: 'Google Cloud',
|
||||
slug: 'stackdriver',
|
||||
subtitle: '(Stackdriver)',
|
||||
icon: 'integrations/google-cloud',
|
||||
component: <StackdriverForm />,
|
||||
},
|
||||
{ title: 'AWS', slug: 'cloudwatch', subtitle: '(CloudWatch)', icon: 'integrations/aws', component: <CloudwatchForm /> },
|
||||
{ title: 'Newrelic', slug: 'newrelic', icon: 'integrations/newrelic', component: <NewrelicForm /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Plugins',
|
||||
description:
|
||||
"Reproduce issues as if they happened in your own browser. Plugins help capture your application's store, HTTP requeets, GraphQL queries, and more.",
|
||||
integrations: [
|
||||
{ title: 'Redux', slug: '', icon: 'integrations/redux', component: <ReduxDoc /> },
|
||||
{ title: 'VueX', slug: '', icon: 'integrations/vuejs', component: <VueDoc /> },
|
||||
{ title: 'GraphQL', slug: '', icon: 'integrations/graphql', component: <GraphQLDoc /> },
|
||||
{ title: 'NgRx', slug: '', icon: 'integrations/ngrx', component: <NgRxDoc /> },
|
||||
{ title: 'MobX', slug: '', icon: 'integrations/mobx', component: <MobxDoc /> },
|
||||
{ title: 'Fetch', slug: '', icon: 'integrations/openreplay', component: <FetchDoc /> },
|
||||
{ title: 'Profiler', slug: '', icon: 'integrations/openreplay', component: <ProfilerDoc /> },
|
||||
{ title: 'Axios', slug: '', icon: 'integrations/openreplay', component: <AxiosDoc /> },
|
||||
{ title: 'Assist', slug: '', icon: 'integrations/openreplay', component: <AssistDoc /> },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
@ -1,37 +1,41 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const JiraForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Jira Cloud with OpenReplay.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
|
||||
</div>
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Jira</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Jira Cloud with OpenReplay.</div>
|
||||
<div className="mt-8">
|
||||
<DocLink className="mt-4" label="Integrate Jira Cloud" url="https://docs.openreplay.com/integrations/jira" />
|
||||
</div>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
ignoreProject={true}
|
||||
name="jira"
|
||||
customPath="jira"
|
||||
formFields={[
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username',
|
||||
autoFocus: true,
|
||||
},
|
||||
{
|
||||
key: 'token',
|
||||
label: 'API Token',
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
label: 'JIRA URL',
|
||||
placeholder: 'E.x. https://myjira.atlassian.net',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
ignoreProject={true}
|
||||
name="issues"
|
||||
customPath="jira"
|
||||
formFields={[ {
|
||||
key: "username",
|
||||
label: "Username",
|
||||
autoFocus: true
|
||||
}, {
|
||||
key: "token",
|
||||
label: "API Token",
|
||||
}, {
|
||||
key: "url",
|
||||
label: "JIRA URL",
|
||||
placeholder: 'E.x. https://myjira.atlassian.net'
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
JiraForm.displayName = "JiraForm";
|
||||
JiraForm.displayName = 'JiraForm';
|
||||
|
||||
export default JiraForm;
|
||||
export default JiraForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,35 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const MobxDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-mobx --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux chain.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">MobX</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture MobX events and inspect them later on while replaying session recordings. This is very useful
|
||||
for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-mobx --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>
|
||||
Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated middleware into your Redux
|
||||
chain.
|
||||
</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
import trackerMobX from '@openreplay/tracker-mobx';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -31,11 +37,11 @@ const tracker = new OpenReplay({
|
|||
});
|
||||
tracker.use(trackerMobX(<options>)); // check list of available options below
|
||||
tracker.start();`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerMobX from '@openreplay/tracker-mobx/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -48,15 +54,16 @@ function SomeFunctionalComponent() {
|
|||
tracker.start();
|
||||
}, [])
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate MobX" url="https://docs.openreplay.com/plugins/mobx" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate MobX" url="https://docs.openreplay.com/plugins/mobx" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MobxDoc.displayName = "MobxDoc";
|
||||
MobxDoc.displayName = 'MobxDoc';
|
||||
|
||||
export default MobxDoc;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,36 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import IntegrationForm from '../IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const NewrelicForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate NewRelic" url="https://docs.openreplay.com/integrations/newrelic" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">New Relic</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate NewRelic with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate NewRelic" url="https://docs.openreplay.com/integrations/newrelic" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="newrelic"
|
||||
formFields={[
|
||||
{
|
||||
key: 'applicationId',
|
||||
label: 'Application Id',
|
||||
},
|
||||
{
|
||||
key: 'xQueryKey',
|
||||
label: 'X-Query-Key',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'EU Region',
|
||||
type: 'checkbox',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="newrelic"
|
||||
formFields={[ {
|
||||
key: "applicationId",
|
||||
label: "Application Id",
|
||||
}, {
|
||||
key: "xQueryKey",
|
||||
label: "X-Query-Key",
|
||||
}, {
|
||||
key: 'region',
|
||||
label: 'EU Region',
|
||||
type: 'checkbox'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
NewrelicForm.displayName = "NewrelicForm";
|
||||
NewrelicForm.displayName = 'NewrelicForm';
|
||||
|
||||
export default NewrelicForm;
|
||||
export default NewrelicForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const NgRxDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-ngrx --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Add the generated meta-reducer into your imports. See NgRx documentation for more details.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">NgRx</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture NgRx actions/state and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-ngrx --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Add the generated meta-reducer into your imports. See NgRx documentation for more details.</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
import { reducers } from './reducers';
|
||||
import OpenReplay from '@openreplay/tracker';
|
||||
import trackerNgRx from '@openreplay/tracker-ngrx';
|
||||
|
|
@ -39,11 +42,11 @@ const metaReducers = [tracker.use(trackerNgRx(<options>))]; // check list of ava
|
|||
imports: [StoreModule.forRoot(reducers, { metaReducers })]
|
||||
})
|
||||
export class AppModule {}`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { StoreModule } from '@ngrx/store';
|
||||
import { reducers } from './reducers';
|
||||
import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerNgRx from '@openreplay/tracker-ngrx/cjs';
|
||||
|
|
@ -64,15 +67,16 @@ const metaReducers = [tracker.use(trackerNgRx(<options>))]; // check list of ava
|
|||
})
|
||||
export class AppModule {}
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate NgRx" url="https://docs.openreplay.com/plugins/ngrx" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate NgRx" url="https://docs.openreplay.com/plugins/ngrx" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NgRxDoc.displayName = "NgRxDoc";
|
||||
NgRxDoc.displayName = 'NgRxDoc';
|
||||
|
||||
export default NgRxDoc;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import ToggleContent from 'Shared/ToggleContent'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from 'Shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const ProfilerDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function call.</div>
|
||||
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-profiler --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Profiler</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
The profiler plugin allows you to measure your JS functions' performance and capture both arguments and result for each function
|
||||
call.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
<div className="font-bold my-2">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-profiler --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<p>Initialize the tracker and load the plugin into it. Then decorate any function inside your code with the generated function.</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<div className="font-bold my-2">Usage</div>
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker';
|
||||
import trackerProfiler from '@openreplay/tracker-profiler';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -36,11 +39,11 @@ export const profiler = tracker.use(trackerProfiler());
|
|||
const fn = profiler('call_name')(() => {
|
||||
//...
|
||||
}, thisArg); // thisArg is optional`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerProfiler from '@openreplay/tracker-profiler/cjs';
|
||||
//...
|
||||
const tracker = new OpenReplay({
|
||||
|
|
@ -58,15 +61,16 @@ const fn = profiler('call_name')(() => {
|
|||
//...
|
||||
}, thisArg); // thisArg is optional
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Profiler" url="https://docs.openreplay.com/plugins/profiler" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Profiler" url="https://docs.openreplay.com/plugins/profiler" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilerDoc.displayName = "ProfilerDoc";
|
||||
ProfilerDoc.displayName = 'ProfilerDoc';
|
||||
|
||||
export default ProfilerDoc;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,31 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from '../../../shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const ReduxDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-redux --save`}
|
||||
</Highlight>
|
||||
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">Redux</h3>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Usage</div>
|
||||
<p>Initialize the tracker then put the generated middleware into your Redux chain.</p>
|
||||
<div className="py-3" />
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture Redux actions/state and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-redux --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Usage</div>
|
||||
<p>Initialize the tracker then put the generated middleware into your Redux chain.</p>
|
||||
<div className="py-3" />
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
import OpenReplay from '@openreplay/tracker';
|
||||
import trackerRedux from '@openreplay/tracker-redux';
|
||||
//...
|
||||
|
|
@ -35,11 +38,11 @@ const store = createStore(
|
|||
reducer,
|
||||
applyMiddleware(tracker.use(trackerRedux(<options>))) // check list of available options below
|
||||
);`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import { applyMiddleware, createStore } from 'redux';
|
||||
import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerRedux from '@openreplay/tracker-redux/cjs';
|
||||
//...
|
||||
|
|
@ -57,15 +60,16 @@ const store = createStore(
|
|||
applyMiddleware(tracker.use(trackerRedux(<options>))) // check list of available options below
|
||||
);
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Redux" url="https://docs.openreplay.com/plugins/redux" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Redux" url="https://docs.openreplay.com/plugins/redux" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReduxDoc.displayName = "ReduxDoc";
|
||||
ReduxDoc.displayName = 'ReduxDoc';
|
||||
|
||||
export default ReduxDoc;
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const RollbarForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate Rollbar" url="https://docs.openreplay.com/integrations/rollbar" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Rollbar</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Rollbar with OpenReplay and see backend errors alongside session replays.</div>
|
||||
<DocLink className="mt-4" label="Integrate Rollbar" url="https://docs.openreplay.com/integrations/rollbar" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="rollbar"
|
||||
formFields={[
|
||||
{
|
||||
key: 'accessToken',
|
||||
label: 'Access Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="rollbar"
|
||||
formFields={[ {
|
||||
key: "accessToken",
|
||||
label: "Access Token",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
RollbarForm.displayName = "RollbarForm";
|
||||
RollbarForm.displayName = 'RollbarForm';
|
||||
|
||||
export default RollbarForm;
|
||||
export default RollbarForm;
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const SentryForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Sentry" url="https://docs.openreplay.com/integrations/sentry" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Sentry</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Sentry with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Sentry" url="https://docs.openreplay.com/integrations/sentry" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="sentry"
|
||||
formFields={[
|
||||
{
|
||||
key: 'organizationSlug',
|
||||
label: 'Organization Slug',
|
||||
},
|
||||
{
|
||||
key: 'projectSlug',
|
||||
label: 'Project Slug',
|
||||
},
|
||||
{
|
||||
key: 'token',
|
||||
label: 'Token',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="sentry"
|
||||
formFields={[ {
|
||||
key: "organizationSlug",
|
||||
label: "Organization Slug",
|
||||
}, {
|
||||
key: "projectSlug",
|
||||
label: "Project Slug",
|
||||
}, {
|
||||
key: "token",
|
||||
label: "Token",
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
SentryForm.displayName = "SentryForm";
|
||||
SentryForm.displayName = 'SentryForm';
|
||||
|
||||
export default SentryForm;
|
||||
export default SentryForm;
|
||||
|
|
|
|||
|
|
@ -1,101 +1,91 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { edit, save, init, update } from 'Duck/integrations/slack'
|
||||
import { Form, Input, Button, Message } from 'UI'
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit, save, init, update } from 'Duck/integrations/slack';
|
||||
import { Form, Input, Button, Message } from 'UI';
|
||||
import { confirm } from 'UI';
|
||||
import { remove } from 'Duck/integrations/slack'
|
||||
import { remove } from 'Duck/integrations/slack';
|
||||
|
||||
class SlackAddForm extends React.PureComponent {
|
||||
componentWillUnmount() {
|
||||
this.props.init({});
|
||||
}
|
||||
|
||||
save = () => {
|
||||
const instance = this.props.instance;
|
||||
if(instance.exists()) {
|
||||
this.props.update(this.props.instance)
|
||||
} else {
|
||||
this.props.save(this.props.instance)
|
||||
}
|
||||
}
|
||||
|
||||
remove = async (id) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`
|
||||
})) {
|
||||
this.props.remove(id);
|
||||
componentWillUnmount() {
|
||||
this.props.init({});
|
||||
}
|
||||
}
|
||||
|
||||
write = ({ target: { name, value } }) => this.props.edit({ [ name ]: value });
|
||||
|
||||
render() {
|
||||
const { instance, saving, errors, onClose } = this.props;
|
||||
return (
|
||||
<div className="p-5" style={{ minWidth: '300px'}}>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Name</label>
|
||||
<Input
|
||||
name="name"
|
||||
value={ instance.name }
|
||||
onChange={ this.write }
|
||||
placeholder="Enter any name"
|
||||
type="text"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>URL</label>
|
||||
<Input
|
||||
name="endpoint"
|
||||
value={ instance.endpoint }
|
||||
onChange={ this.write }
|
||||
placeholder="Slack webhook URL"
|
||||
type="text"
|
||||
/>
|
||||
</Form.Field>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={ this.save }
|
||||
disabled={ !instance.validate() }
|
||||
loading={ saving }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ instance.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={ onClose }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={ () => this.remove(instance.webhookId) }
|
||||
disabled={ !instance.exists() }
|
||||
>
|
||||
{ 'Delete' }
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{ errors &&
|
||||
<div className="my-3">
|
||||
{ errors.map(error => <Message visible={ errors } size="mini" error key={ error } >{ error }</Message>) }
|
||||
</div>
|
||||
save = () => {
|
||||
const instance = this.props.instance;
|
||||
if (instance.exists()) {
|
||||
this.props.update(this.props.instance);
|
||||
} else {
|
||||
this.props.save(this.props.instance);
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
remove = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this channel?`,
|
||||
})
|
||||
) {
|
||||
this.props.remove(id);
|
||||
}
|
||||
};
|
||||
|
||||
write = ({ target: { name, value } }) => this.props.edit({ [name]: value });
|
||||
|
||||
render() {
|
||||
const { instance, saving, errors, onClose } = this.props;
|
||||
return (
|
||||
<div className="p-5" style={{ minWidth: '300px' }}>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<label>Name</label>
|
||||
<Input name="name" value={instance.name} onChange={this.write} placeholder="Enter any name" type="text" />
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>URL</label>
|
||||
<Input name="endpoint" value={instance.endpoint} onChange={this.write} placeholder="Slack webhook URL" type="text" />
|
||||
</Form.Field>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={this.save}
|
||||
disabled={!instance.validate()}
|
||||
loading={saving}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{instance.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
|
||||
<Button onClick={onClose}>{'Cancel'}</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={() => this.remove(instance.webhookId)} disabled={!instance.exists()}>
|
||||
{'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{errors && (
|
||||
<div className="my-3">
|
||||
{errors.map((error) => (
|
||||
<Message visible={errors} size="mini" error key={error}>
|
||||
{error}
|
||||
</Message>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
instance: state.getIn(['slack', 'instance']),
|
||||
saving: state.getIn(['slack', 'saveRequest', 'loading']),
|
||||
errors: state.getIn([ 'slack', 'saveRequest', 'errors' ]),
|
||||
}), { edit, save, init, remove, update })(SlackAddForm)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
instance: state.getIn(['slack', 'instance']),
|
||||
saving: state.getIn(['slack', 'saveRequest', 'loading']),
|
||||
errors: state.getIn(['slack', 'saveRequest', 'errors']),
|
||||
}),
|
||||
{ edit, save, init, remove, update }
|
||||
)(SlackAddForm);
|
||||
|
|
|
|||
|
|
@ -1,49 +1,47 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { NoContent } from 'UI';
|
||||
import { remove, edit } from 'Duck/integrations/slack'
|
||||
import { remove, edit, init } from 'Duck/integrations/slack';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
function SlackChannelList(props) {
|
||||
const { list } = props;
|
||||
const { list } = props;
|
||||
|
||||
const onEdit = (instance) => {
|
||||
props.edit(instance)
|
||||
props.onEdit()
|
||||
}
|
||||
const onEdit = (instance) => {
|
||||
props.edit(instance);
|
||||
props.onEdit();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.</div>
|
||||
{/* <DocLink className="mt-4" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" /> */}
|
||||
<DocLink className="mt-4 text-base" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={ list.size === 0 }
|
||||
>
|
||||
{list.map(c => (
|
||||
<div
|
||||
key={c.webhookId}
|
||||
className="border-t px-5 py-2 flex items-center justify-between cursor-pointer"
|
||||
onClick={() => onEdit(c)}
|
||||
>
|
||||
<div className="flex-grow-0" style={{ maxWidth: '90%'}}>
|
||||
<div>{c.name}</div>
|
||||
<div className="truncate test-xs color-gray-medium">
|
||||
{c.endpoint}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<NoContent
|
||||
title={
|
||||
<div className="p-5 mb-4">
|
||||
<div className="text-base text-left">
|
||||
Integrate Slack with OpenReplay and share insights with the rest of the team, directly from the recording page.
|
||||
</div>
|
||||
<DocLink className="mt-4 text-base" label="Integrate Slack" url="https://docs.openreplay.com/integrations/slack" />
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={list.size === 0}
|
||||
>
|
||||
{list.map((c) => (
|
||||
<div key={c.webhookId} className="border-t px-5 py-2 flex items-center justify-between cursor-pointer" onClick={() => onEdit(c)}>
|
||||
<div className="flex-grow-0" style={{ maxWidth: '90%' }}>
|
||||
<div>{c.name}</div>
|
||||
<div className="truncate test-xs color-gray-medium">{c.endpoint}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['slack', 'list'])
|
||||
}), { remove, edit })(SlackChannelList)
|
||||
export default connect(
|
||||
(state) => ({
|
||||
list: state.getIn(['slack', 'list']),
|
||||
}),
|
||||
{ remove, edit, init }
|
||||
)(SlackChannelList);
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import SlackChannelList from './SlackChannelList/SlackChannelList';
|
||||
|
||||
const SlackForm = (props) => {
|
||||
const { onEdit } = props;
|
||||
return (
|
||||
<>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SlackForm.displayName = "SlackForm";
|
||||
|
||||
export default SlackForm;
|
||||
48
frontend/app/components/Client/Integrations/SlackForm.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import SlackChannelList from './SlackChannelList/SlackChannelList';
|
||||
import { fetchList } from 'Duck/integrations/slack';
|
||||
import { connect } from 'react-redux';
|
||||
import SlackAddForm from './SlackAddForm';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
interface Props {
|
||||
onEdit: (integration: any) => void;
|
||||
istance: any;
|
||||
fetchList: any;
|
||||
}
|
||||
const SlackForm = (props: Props) => {
|
||||
const { istance } = props;
|
||||
const { hideModal } = useModal();
|
||||
const [active, setActive] = React.useState(false);
|
||||
|
||||
const onEdit = () => {
|
||||
setActive(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto flex items-start" style={{ width: active ? '650px' : '350px' }}>
|
||||
<div style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Slack</h3>
|
||||
<SlackChannelList onEdit={onEdit} />
|
||||
</div>
|
||||
{active && (
|
||||
<div className="border-l h-full">
|
||||
<SlackAddForm onClose={() => setActive(false)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SlackForm.displayName = 'SlackForm';
|
||||
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
istance: state.getIn(['slack', 'instance']),
|
||||
}),
|
||||
{ fetchList }
|
||||
)(SlackForm);
|
||||
|
|
@ -1,29 +1,32 @@
|
|||
import React from 'react';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import IntegrationForm from './IntegrationForm';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const StackdriverForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Stackdriver" url="https://docs.openreplay.com/integrations/stackdriver" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Stackdriver</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate Stackdriver with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate Stackdriver" url="https://docs.openreplay.com/integrations/stackdriver" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="stackdriver"
|
||||
formFields={[
|
||||
{
|
||||
key: 'logName',
|
||||
label: 'Log Name',
|
||||
},
|
||||
{
|
||||
key: 'serviceAccountCredentials',
|
||||
label: 'Service Account Credentials (JSON)',
|
||||
component: 'textarea',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="stackdriver"
|
||||
formFields={[ {
|
||||
key: "logName",
|
||||
label: "Log Name",
|
||||
}, {
|
||||
key: "serviceAccountCredentials",
|
||||
label: "Service Account Credentials (JSON)",
|
||||
component: 'textarea',
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
StackdriverForm.displayName = "StackdriverForm";
|
||||
StackdriverForm.displayName = 'StackdriverForm';
|
||||
|
||||
export default StackdriverForm;
|
||||
|
|
|
|||
|
|
@ -4,30 +4,34 @@ import RegionDropdown from './RegionDropdown';
|
|||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const SumoLogicForm = (props) => (
|
||||
<>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate SumoLogic" url="https://docs.openreplay.com/integrations/sumo" />
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">Sumologic</h3>
|
||||
<div className="p-5 border-b mb-4">
|
||||
<div>How to integrate SumoLogic with OpenReplay and see backend errors alongside session recordings.</div>
|
||||
<DocLink className="mt-4" label="Integrate SumoLogic" url="https://docs.openreplay.com/integrations/sumo" />
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{...props}
|
||||
name="sumologic"
|
||||
formFields={[
|
||||
{
|
||||
key: 'accessId',
|
||||
label: 'Access ID',
|
||||
},
|
||||
{
|
||||
key: 'accessKey',
|
||||
label: 'Access Key',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
component: RegionDropdown,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<IntegrationForm
|
||||
{ ...props }
|
||||
name="sumologic"
|
||||
formFields={[ {
|
||||
key: "accessId",
|
||||
label: "Access ID",
|
||||
}, {
|
||||
key: "accessKey",
|
||||
label: "Access Key",
|
||||
}, {
|
||||
key: "region",
|
||||
label: "Region",
|
||||
component: RegionDropdown,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
SumoLogicForm.displayName = "SumoLogicForm";
|
||||
SumoLogicForm.displayName = 'SumoLogicForm';
|
||||
|
||||
export default SumoLogicForm;
|
||||
|
|
|
|||
|
|
@ -1,29 +1,34 @@
|
|||
import React from 'react';
|
||||
import Highlight from 'react-highlight'
|
||||
import Highlight from 'react-highlight';
|
||||
import ToggleContent from '../../../shared/ToggleContent';
|
||||
import DocLink from 'Shared/DocLink/DocLink';
|
||||
|
||||
const VueDoc = (props) => {
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div>This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very useful for understanding and fixing issues.</div>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Installation</div>
|
||||
<Highlight className="js">
|
||||
{`npm i @openreplay/tracker-vuex --save`}
|
||||
</Highlight>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Usage</div>
|
||||
<p>Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins field of your store.</p>
|
||||
<div className="py-3" />
|
||||
const { projectKey } = props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '500px' }}>
|
||||
<h3 className="p-5 text-2xl">VueX</h3>
|
||||
<div className="p-5">
|
||||
<div>
|
||||
This plugin allows you to capture VueX mutations/state and inspect them later on while replaying session recordings. This is very
|
||||
useful for understanding and fixing issues.
|
||||
</div>
|
||||
|
||||
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import Vuex from 'vuex'
|
||||
<div className="font-bold my-2 text-lg">Installation</div>
|
||||
<Highlight className="js">{`npm i @openreplay/tracker-vuex --save`}</Highlight>
|
||||
|
||||
<div className="font-bold my-2 text-lg">Usage</div>
|
||||
<p>
|
||||
Initialize the @openreplay/tracker package as usual and load the plugin into it. Then put the generated plugin into your plugins
|
||||
field of your store.
|
||||
</p>
|
||||
<div className="py-3" />
|
||||
|
||||
<ToggleContent
|
||||
label="Server-Side-Rendered (SSR)?"
|
||||
first={
|
||||
<Highlight className="js">
|
||||
{`import Vuex from 'vuex'
|
||||
import OpenReplay from '@openreplay/tracker';
|
||||
import trackerVuex from '@openreplay/tracker-vuex';
|
||||
//...
|
||||
|
|
@ -36,11 +41,11 @@ const store = new Vuex.Store({
|
|||
//...
|
||||
plugins: [tracker.use(trackerVuex(<options>))] // check list of available options below
|
||||
});`}
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import Vuex from 'vuex'
|
||||
</Highlight>
|
||||
}
|
||||
second={
|
||||
<Highlight className="js">
|
||||
{`import Vuex from 'vuex'
|
||||
import OpenReplay from '@openreplay/tracker/cjs';
|
||||
import trackerVuex from '@openreplay/tracker-vuex/cjs';
|
||||
//...
|
||||
|
|
@ -58,15 +63,16 @@ const store = new Vuex.Store({
|
|||
plugins: [tracker.use(trackerVuex(<options>))] // check list of available options below
|
||||
});
|
||||
}`}
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
</Highlight>
|
||||
}
|
||||
/>
|
||||
|
||||
<DocLink className="mt-4" label="Integrate Vuex" url="https://docs.openreplay.com/plugins/vuex" />
|
||||
</div>
|
||||
)
|
||||
<DocLink className="mt-4" label="Integrate Vuex" url="https://docs.openreplay.com/plugins/vuex" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VueDoc.displayName = "VueDoc";
|
||||
VueDoc.displayName = 'VueDoc';
|
||||
|
||||
export default VueDoc;
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
import styles from './integrationItem.module.css';
|
||||
|
||||
const onDocLinkClick = (e, link) => {
|
||||
e.stopPropagation();
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
|
||||
const IntegrationItem = ({
|
||||
deleteHandler = null, icon, url = null, title = '', description = '', onClick = null, dockLink = '', integrated = false
|
||||
}) => {
|
||||
return (
|
||||
<div className={ styles.wrapper } onClick={ e => onClick(e, url) }>
|
||||
<Icon name={ icon } size="50" />
|
||||
<h4 className="text-lg my-4">{ title }</h4>
|
||||
<p className={ styles.description }>{ description }</p>
|
||||
<div className={ styles.externalIcon } data-hidden={ !url }><Icon name="external-link-alt" size="18" /></div>
|
||||
<div className={ styles.footer }>
|
||||
{deleteHandler && (
|
||||
<div className={ cn(styles.docsLink, 'color-red') } onClick={ deleteHandler }>
|
||||
<Icon name="trash" size="16" color="red" marginRight="5" />
|
||||
{ 'Remove' }
|
||||
</div>
|
||||
)}
|
||||
{ dockLink && (
|
||||
<div className={ cn(styles.docsLink, !dockLink ? styles.hidden : '') } onClick={ (e) => onDocLinkClick(e, dockLink) }>
|
||||
<Icon name="book" size="16" color="gray-medium" marginRight="5" />
|
||||
{ 'Documentation' }
|
||||
</div>
|
||||
)}
|
||||
<div className={ styles.integratedCheck } data-hidden={ !integrated }>
|
||||
<Icon name="check-circle" size="16" color="teal" marginRight="5" />
|
||||
{ 'Integrated' }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default IntegrationItem;
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
/* min-height: 250px; */
|
||||
/* min-width: 260px; */
|
||||
/* max-width: 300px; */
|
||||
|
|
|
|||
|
|
@ -1,46 +1,50 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import stl from './notifications.module.css'
|
||||
import { Checkbox } from 'UI'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRequest } from 'HOCs'
|
||||
import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config'
|
||||
import React, { useEffect } from 'react';
|
||||
import cn from 'classnames';
|
||||
import stl from './notifications.module.css';
|
||||
import { Checkbox, Toggler } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRequest } from 'HOCs';
|
||||
import { fetch as fetchConfig, edit as editConfig, save as saveConfig } from 'Duck/config';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
|
||||
function Notifications(props) {
|
||||
const { config } = props;
|
||||
const { config } = props;
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchConfig();
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
props.fetchConfig();
|
||||
}, []);
|
||||
|
||||
const onChange = () => {
|
||||
const _config = { 'weeklyReport' : !config.weeklyReport };
|
||||
props.editConfig(_config);
|
||||
props.saveConfig(_config)
|
||||
}
|
||||
const onChange = () => {
|
||||
const _config = { weeklyReport: !config.weeklyReport };
|
||||
props.editConfig(_config);
|
||||
props.saveConfig(_config);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={ stl.wrapper }>
|
||||
<div className={ stl.tabHeader }>
|
||||
{ <h3 className={ cn(stl.tabTitle, "text-2xl") }>{ 'Notifications' }</h3> }
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<Checkbox
|
||||
name="isPublic"
|
||||
className="font-medium"
|
||||
type="checkbox"
|
||||
checked={ config.weeklyReport }
|
||||
onClick={ onChange }
|
||||
className="mr-8"
|
||||
label="Send me a weekly report for each project."
|
||||
/>
|
||||
<img src="/assets/img/img-newsletter.png" style={{ width: '400px'}}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className={stl.tabHeader}>{<h3 className={cn(stl.tabTitle, 'text-2xl')}>{'Notifications'}</h3>}</div>
|
||||
<div className="">
|
||||
<div className="text-lg font-medium">Weekly project summary</div>
|
||||
<div className="mb-4">Receive wekly report for each project on email.</div>
|
||||
<Toggler checked={config.weeklyReport} name="test" onChange={onChange} label={config.weeklyReport ? 'Yes' : 'No'} />
|
||||
{/* <Checkbox
|
||||
name="isPublic"
|
||||
className="font-medium"
|
||||
type="checkbox"
|
||||
checked={config.weeklyReport}
|
||||
onClick={onChange}
|
||||
className="mr-8"
|
||||
label="Send me a weekly report for each project."
|
||||
/> */}
|
||||
{/* <img src="/assets/img/img-newsletter.png" style={{ width: '400px'}}/> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
config: state.getIn(['config', 'options'])
|
||||
}), { fetchConfig, editConfig, saveConfig })(withPageTitle('Notifications - OpenReplay Preferences')(Notifications));
|
||||
export default connect(
|
||||
(state) => ({
|
||||
config: state.getIn(['config', 'options']),
|
||||
}),
|
||||
{ fetchConfig, editConfig, saveConfig }
|
||||
)(withPageTitle('Notifications - OpenReplay Preferences')(Notifications));
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className={cn(stl.wrapper, 'h-full overflow-y-auto pb-24')}>
|
||||
<div className={cn(stl.header, 'flex items-end')}>
|
||||
<div className={stl.label}>
|
||||
<span>Preferences</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.PROFILE}
|
||||
title="Account"
|
||||
|
|
@ -29,7 +29,7 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.INTEGRATIONS}
|
||||
title="Integrations"
|
||||
|
|
@ -38,7 +38,7 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
iconName="tags"
|
||||
active={activeTab === CLIENT_TABS.CUSTOM_FIELDS}
|
||||
|
|
@ -48,7 +48,7 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
</div>
|
||||
|
||||
{
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.WEBHOOKS}
|
||||
title="Webhooks"
|
||||
|
|
@ -58,7 +58,7 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
</div>
|
||||
}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.SITES}
|
||||
title="Projects"
|
||||
|
|
@ -68,7 +68,7 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
</div>
|
||||
|
||||
{isEnterprise && isAdmin && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.MANAGE_ROLES}
|
||||
title="Roles & Access"
|
||||
|
|
@ -79,7 +79,7 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
)}
|
||||
|
||||
{isEnterprise && isAdmin && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.AUDIT}
|
||||
title="Audit"
|
||||
|
|
@ -90,17 +90,17 @@ function PreferencesMenu({ account, activeTab, history, isEnterprise }) {
|
|||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mb-4">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.MANAGE_USERS}
|
||||
title="Team"
|
||||
iconName="users"
|
||||
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.MANAGE_USERS}
|
||||
title="Team"
|
||||
iconName="users"
|
||||
onClick={() => setTab(CLIENT_TABS.MANAGE_USERS)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="mb-2">
|
||||
<SideMenuitem
|
||||
active={activeTab === CLIENT_TABS.NOTIFICATIONS}
|
||||
title="Notifications"
|
||||
|
|
|
|||
|
|
@ -2,27 +2,29 @@ import React from 'react';
|
|||
import { Popup, Button, IconButton } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
import { connect } from 'react-redux';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import NewSiteForm from '../NewSiteForm';
|
||||
|
||||
const PERMISSION_WARNING = 'You don’t have the permissions to perform this action.';
|
||||
const LIMIT_WARNING = 'You have reached site limit.';
|
||||
|
||||
function AddProjectButton({ isAdmin = false, onClick }: any) {
|
||||
function AddProjectButton({ isAdmin = false, init = () => {} }: any) {
|
||||
const { userStore } = useStore();
|
||||
const { showModal, hideModal } = useModal();
|
||||
const limtis = useObserver(() => userStore.limits);
|
||||
const canAddProject = useObserver(() => isAdmin && (limtis.projects === -1 || limtis.projects > 0));
|
||||
|
||||
const onClick = () => {
|
||||
init();
|
||||
showModal(<NewSiteForm onClose={hideModal} />, { right: true });
|
||||
};
|
||||
return (
|
||||
<Popup content={`${!isAdmin ? PERMISSION_WARNING : !canAddProject ? LIMIT_WARNING : 'Add a Project'}`}>
|
||||
<Button rounded={true} variant="outline" icon="plus" onClick={onClick} disabled={!canAddProject || !isAdmin}></Button>
|
||||
{/* <IconButton
|
||||
id="add-button"
|
||||
disabled={ !canAddProject || !isAdmin }
|
||||
circle
|
||||
icon="plus"
|
||||
outline
|
||||
onClick={ onClick }
|
||||
/> */}
|
||||
</Popup>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddProjectButton;
|
||||
export default connect(null, { init, remove, fetchGDPR })(AddProjectButton);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import React from 'react';
|
||||
import TrackingCodeModal from 'Shared/TrackingCodeModal';
|
||||
import { Button } from 'UI';
|
||||
|
||||
interface Props {
|
||||
site: any;
|
||||
}
|
||||
function InstallButton(props: Props) {
|
||||
const { site } = props;
|
||||
const { showModal, hideModal } = useModal();
|
||||
const onClick = () => {
|
||||
showModal(
|
||||
<TrackingCodeModal title="Tracking Code" subTitle={`(Unique to ${site.host})`} onClose={hideModal} site={site} />,
|
||||
{ right: true }
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Button size="small" variant="primary" onClick={onClick}>
|
||||
{'Installation Steps'}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default InstallButton;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './InstallButton'
|
||||
|
|
@ -1,121 +1,122 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Form, Input, Button, Icon } from 'UI';
|
||||
import { save, edit, update , fetchList, remove } from 'Duck/site';
|
||||
import { save, edit, update, fetchList, remove } from 'Duck/site';
|
||||
import { pushNewSite } from 'Duck/user';
|
||||
import { setSiteId } from 'Duck/site';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import styles from './siteForm.module.css';
|
||||
import { confirm } from 'UI';
|
||||
|
||||
@connect(state => ({
|
||||
site: state.getIn([ 'site', 'instance' ]),
|
||||
sites: state.getIn([ 'site', 'list' ]),
|
||||
siteList: state.getIn([ 'site', 'list' ]),
|
||||
loading: state.getIn([ 'site', 'save', 'loading' ]) || state.getIn([ 'site', 'remove', 'loading' ]),
|
||||
}), {
|
||||
save,
|
||||
remove,
|
||||
edit,
|
||||
update,
|
||||
pushNewSite,
|
||||
fetchList,
|
||||
setSiteId
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
sites: state.getIn(['site', 'list']),
|
||||
siteList: state.getIn(['site', 'list']),
|
||||
loading: state.getIn(['site', 'save', 'loading']) || state.getIn(['site', 'remove', 'loading']),
|
||||
}),
|
||||
{
|
||||
save,
|
||||
remove,
|
||||
edit,
|
||||
update,
|
||||
pushNewSite,
|
||||
fetchList,
|
||||
setSiteId,
|
||||
}
|
||||
)
|
||||
@withRouter
|
||||
export default class NewSiteForm extends React.PureComponent {
|
||||
state = {
|
||||
existsError: false,
|
||||
}
|
||||
state = {
|
||||
existsError: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { location: { pathname }, match: { params: { siteId } } } = this.props;
|
||||
if (pathname.includes('onboarding')) {
|
||||
this.props.setSiteId(siteId);
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
const {
|
||||
location: { pathname },
|
||||
match: {
|
||||
params: { siteId },
|
||||
},
|
||||
} = this.props;
|
||||
if (pathname.includes('onboarding')) {
|
||||
this.props.setSiteId(siteId);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = e => {
|
||||
e.preventDefault();
|
||||
const { site, siteList, location: { pathname } } = this.props;
|
||||
if (!site.exists() && siteList.some(({ name }) => name === site.name)) {
|
||||
return this.setState({ existsError: true });
|
||||
}
|
||||
if (site.exists()) {
|
||||
this.props.update(this.props.site, this.props.site.id).then(() => {
|
||||
this.props.onClose(null)
|
||||
this.props.fetchList();
|
||||
})
|
||||
} else {
|
||||
this.props.save(this.props.site).then(() => {
|
||||
this.props.fetchList().then(() => {
|
||||
const { sites } = this.props;
|
||||
const site = sites.last();
|
||||
if (!pathname.includes('/client')) {
|
||||
this.props.setSiteId(site.get('id'))
|
||||
}
|
||||
this.props.onClose(null, site)
|
||||
})
|
||||
|
||||
// this.props.pushNewSite(site)
|
||||
});
|
||||
}
|
||||
}
|
||||
onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const {
|
||||
site,
|
||||
siteList,
|
||||
location: { pathname },
|
||||
} = this.props;
|
||||
if (!site.exists() && siteList.some(({ name }) => name === site.name)) {
|
||||
return this.setState({ existsError: true });
|
||||
}
|
||||
if (site.exists()) {
|
||||
this.props.update(this.props.site, this.props.site.id).then(() => {
|
||||
this.props.onClose(null);
|
||||
this.props.fetchList();
|
||||
});
|
||||
} else {
|
||||
this.props.save(this.props.site).then(() => {
|
||||
this.props.fetchList().then(() => {
|
||||
const { sites } = this.props;
|
||||
const site = sites.last();
|
||||
if (!pathname.includes('/client')) {
|
||||
this.props.setSiteId(site.get('id'));
|
||||
}
|
||||
this.props.onClose(null, site);
|
||||
});
|
||||
|
||||
remove = async (site) => {
|
||||
if (await confirm({
|
||||
header: 'Projects',
|
||||
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`
|
||||
})) {
|
||||
this.props.remove(site.id).then(() => {
|
||||
this.props.onClose(null)
|
||||
});
|
||||
}
|
||||
};
|
||||
// this.props.pushNewSite(site)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
edit = ({ target: { name, value } }) => {
|
||||
this.setState({ existsError: false });
|
||||
this.props.edit({ [ name ]: value });
|
||||
}
|
||||
remove = async (site) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Projects',
|
||||
confirmation: `Are you sure you want to delete this Project? We won't be able to record anymore sessions.`,
|
||||
})
|
||||
) {
|
||||
this.props.remove(site.id).then(() => {
|
||||
this.props.onClose(null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { site, loading } = this.props;
|
||||
return (
|
||||
<Form className={ styles.formWrapper } onSubmit={ site.validate() && this.onSubmit }>
|
||||
<div className={ styles.content }>
|
||||
<Form.Field>
|
||||
<label>{ 'Name' }</label>
|
||||
<Input
|
||||
placeholder="Ex. openreplay"
|
||||
name="name"
|
||||
value={ site.name }
|
||||
onChange={ this.edit }
|
||||
className={ styles.input }
|
||||
/>
|
||||
</Form.Field>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="float-left mr-2"
|
||||
loading={ loading }
|
||||
disabled={ !site.validate() }
|
||||
>
|
||||
{site.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{site.exists() && (
|
||||
<Button variant="text" type="button" onClick={() => this.remove(site)}>
|
||||
<Icon name="trash" size="16" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{ this.state.existsError &&
|
||||
<div className={ styles.errorMessage }>
|
||||
{ "Site exists already. Please choose another one." }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
edit = ({ target: { name, value } }) => {
|
||||
this.setState({ existsError: false });
|
||||
this.props.edit({ [name]: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { site, loading } = this.props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{site.exists() ? 'Edit Project' : 'New Project'}</h3>
|
||||
<Form className={styles.formWrapper} onSubmit={site.validate() && this.onSubmit}>
|
||||
<div className={styles.content}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input placeholder="Ex. openreplay" name="name" value={site.name} onChange={this.edit} className={styles.input} />
|
||||
</Form.Field>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<Button variant="primary" type="submit" className="float-left mr-2" loading={loading} disabled={!site.validate()}>
|
||||
{site.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{site.exists() && (
|
||||
<Button variant="text" type="button" onClick={() => this.remove(site)}>
|
||||
<Icon name="trash" size="16" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{this.state.existsError && <div className={styles.errorMessage}>{'Site exists already. Please choose another one.'}</div>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
8
frontend/app/components/Client/Sites/ProjectKey.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { withCopy } from 'HOCs';
|
||||
import React from 'react';
|
||||
|
||||
function ProjectKey({ value, tooltip }: any) {
|
||||
return <div className="rounded border bg-gray-lightest w-fit px-2">{value}</div>;
|
||||
}
|
||||
|
||||
export default withCopy(ProjectKey);
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { Loader, SlideModal, Icon, Button, Popup, TextLink } from 'UI';
|
||||
import { Loader, Button, Popup, TextLink } from 'UI';
|
||||
import { init, remove, fetchGDPR } from 'Duck/site';
|
||||
import { RED, YELLOW, GREEN, STATUS_COLOR_MAP } from 'Types/site';
|
||||
import stl from './sites.module.css';
|
||||
import NewSiteForm from './NewSiteForm';
|
||||
import GDPRForm from './GDPRForm';
|
||||
import TrackingCodeModal from 'Shared/TrackingCodeModal';
|
||||
import BlockedIps from './BlockedIps';
|
||||
import { confirm, PageTitle } from 'UI';
|
||||
import SiteSearch from './SiteSearch';
|
||||
import AddProjectButton from './AddProjectButton';
|
||||
import InstallButton from './InstallButton';
|
||||
import ProjectKey from './ProjectKey';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { getInitials } from 'App/utils';
|
||||
|
||||
const STATUS_MESSAGE_MAP = {
|
||||
[RED]: ' There seems to be an issue (please verify your installation)',
|
||||
|
|
@ -20,11 +20,7 @@ const STATUS_MESSAGE_MAP = {
|
|||
[GREEN]: 'All good!',
|
||||
};
|
||||
|
||||
const BLOCKED_IPS = 'BLOCKED_IPS';
|
||||
const NONE = 'NONE';
|
||||
|
||||
const NEW_SITE_FORM = 'NEW_SITE_FORM';
|
||||
const GDPR_FORM = 'GDPR_FORM';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
|
|
@ -43,20 +39,9 @@ const GDPR_FORM = 'GDPR_FORM';
|
|||
@withPageTitle('Projects - OpenReplay Preferences')
|
||||
class Sites extends React.PureComponent {
|
||||
state = {
|
||||
showTrackingCode: false,
|
||||
modalContent: NONE,
|
||||
detailContent: NONE,
|
||||
searchQuery: '',
|
||||
};
|
||||
|
||||
toggleBlockedIp = () => {
|
||||
this.setState({
|
||||
detailContent: this.state.detailContent === BLOCKED_IPS ? NONE : BLOCKED_IPS,
|
||||
});
|
||||
};
|
||||
|
||||
closeModal = () => this.setState({ modalContent: NONE, detailContent: NONE });
|
||||
|
||||
edit = (site) => {
|
||||
this.props.init(site);
|
||||
this.setState({ modalContent: NEW_SITE_FORM });
|
||||
|
|
@ -73,128 +58,59 @@ class Sites extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
showGDPRForm = (site) => {
|
||||
this.props.init(site);
|
||||
this.setState({ modalContent: GDPR_FORM });
|
||||
};
|
||||
|
||||
showNewSiteForm = () => {
|
||||
this.props.init();
|
||||
this.setState({ modalContent: NEW_SITE_FORM });
|
||||
};
|
||||
|
||||
showTrackingCode = (site) => {
|
||||
this.props.init(site);
|
||||
this.setState({ showTrackingCode: true });
|
||||
};
|
||||
|
||||
getModalTitle() {
|
||||
switch (this.state.modalContent) {
|
||||
case NEW_SITE_FORM:
|
||||
return this.props.site.exists() ? 'Update Project' : 'New Project';
|
||||
case GDPR_FORM:
|
||||
return 'Project Settings';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
renderModalContent() {
|
||||
switch (this.state.modalContent) {
|
||||
case NEW_SITE_FORM:
|
||||
return <NewSiteForm onClose={this.closeModal} />;
|
||||
case GDPR_FORM:
|
||||
return <GDPRForm onClose={this.closeModal} toggleBlockedIp={this.toggleBlockedIp} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderModalDetailContent() {
|
||||
switch (this.state.detailContent) {
|
||||
case BLOCKED_IPS:
|
||||
return <BlockedIps />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, sites, site, user, account } = this.props;
|
||||
const { modalContent, showTrackingCode } = this.state;
|
||||
const { loading, sites, user } = this.props;
|
||||
const isAdmin = user.admin || user.superAdmin;
|
||||
const filteredSites = sites.filter((site) => site.name.toLowerCase().includes(this.state.searchQuery.toLowerCase()));
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<TrackingCodeModal
|
||||
title="Tracking Code"
|
||||
subTitle={`(Unique to ${site.host})`}
|
||||
displayed={showTrackingCode}
|
||||
onClose={() => this.setState({ showTrackingCode: false })}
|
||||
site={site}
|
||||
/>
|
||||
<SlideModal
|
||||
title={this.getModalTitle()}
|
||||
size="small"
|
||||
isDisplayed={modalContent !== NONE}
|
||||
content={this.renderModalContent()}
|
||||
onClose={this.closeModal}
|
||||
detailContent={this.renderModalDetailContent()}
|
||||
/>
|
||||
<div className={stl.wrapper}>
|
||||
<div className={stl.tabHeader}>
|
||||
<PageTitle
|
||||
title={<div className="mr-4">Projects</div>}
|
||||
actionButton={<AddProjectButton isAdmin={isAdmin} onClick={this.showNewSiteForm} />}
|
||||
/>
|
||||
<PageTitle title={<div className="mr-4">Projects</div>} actionButton={<AddProjectButton isAdmin={isAdmin} />} />
|
||||
|
||||
<div className="flex ml-auto items-center">
|
||||
<TextLink icon="book" className="mr-4" href="https://docs.openreplay.com/installation" label="Documentation" />
|
||||
<TextLink icon="book" className="mr-4" href="https://docs.openreplay.com/installation" label="Installation Docs" />
|
||||
<SiteSearch onChange={(value) => this.setState({ searchQuery: value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={stl.list}>
|
||||
<div className="grid grid-cols-12 gap-2 w-full items-center border-b px-2 py-3 font-medium">
|
||||
<div className="col-span-4">Name</div>
|
||||
<div className="col-span-4">Project Name</div>
|
||||
<div className="col-span-4">Key</div>
|
||||
<div className="col-span-4"></div>
|
||||
</div>
|
||||
{filteredSites.map((_site) => (
|
||||
<div
|
||||
key={_site.key}
|
||||
className="grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center border-b px-2 py-3"
|
||||
className="grid grid-cols-12 gap-2 w-full group hover:bg-active-blue items-center border-b px-2 py-3 last:border-none"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
<div className="flex items-center">
|
||||
<Popup content={STATUS_MESSAGE_MAP[_site.status]} inverted position="top center">
|
||||
<div style={{ width: '10px' }}>
|
||||
<Icon name="circle" size="10" color={STATUS_COLOR_MAP[_site.status]} />
|
||||
<Popup content={STATUS_MESSAGE_MAP[_site.status]} inverted>
|
||||
<div className="relative flex items-center justify-center w-10 h-10">
|
||||
<div
|
||||
className="absolute left-0 right-0 top-0 bottom-0 mx-auto w-10 h-10 rounded-full opacity-10"
|
||||
style={{ backgroundColor: STATUS_COLOR_MAP[_site.status] }}
|
||||
/>
|
||||
<div className="text-lg uppercase" style={{ color: STATUS_COLOR_MAP[_site.status] }}>
|
||||
{getInitials(_site.name)}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
<span className="ml-2">{_site.host}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<span className="px-2 py-1 bg-gray-lightest rounded border text-sm">{_site.projectKey}</span>
|
||||
<ProjectKey value={_site.projectKey} tooltip="Project key copied to clipboard" />
|
||||
</div>
|
||||
<div className="col-span-4 justify-self-end flex items-center">
|
||||
<div className="mr-4">
|
||||
<Button size="small" variant="primary" onClick={() => this.showTrackingCode(_site)}>
|
||||
{'Installation Steps'}
|
||||
</Button>
|
||||
<InstallButton site={_site} />
|
||||
</div>
|
||||
<div className="invisible group-hover:visible">
|
||||
<Button
|
||||
variant="text"
|
||||
className={cn('mx-3', { hidden: !isAdmin })}
|
||||
disabled={!isAdmin}
|
||||
onClick={() => isAdmin && this.edit(_site)}
|
||||
data-clickable
|
||||
>
|
||||
<Icon name="edit" size="16" color="teal" />
|
||||
</Button>
|
||||
<EditButton isAdmin={isAdmin} onClick={() => this.props.init(_site)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -207,3 +123,12 @@ class Sites extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default Sites;
|
||||
|
||||
function EditButton({ isAdmin, onClick }) {
|
||||
const { showModal, hideModal } = useModal();
|
||||
const _onClick = () => {
|
||||
onClick();
|
||||
showModal(<NewSiteForm onClose={hideModal} />);
|
||||
};
|
||||
return <Button icon="edit" variant="text" disabled={!isAdmin} onClick={_onClick} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ function UsersView(props: Props) {
|
|||
const userCount = useObserver(() => userStore.list.length);
|
||||
const roles = useObserver(() => roleStore.list);
|
||||
const { showModal } = useModal();
|
||||
|
||||
const reachedLimit = (limits.remaining + userStore.modifiedCount) <= 0;
|
||||
|
||||
const reachedLimit = limits.remaining + userStore.modifiedCount <= 0;
|
||||
const isAdmin = account.admin || account.superAdmin;
|
||||
|
||||
const editHandler = (user = null) => {
|
||||
const editHandler = (user: any = null) => {
|
||||
userStore.initUser(user).then(() => {
|
||||
showModal(<UserForm />, {});
|
||||
showModal(<UserForm />, { right: true });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (roles.length === 0 && isEnterprise) {
|
||||
|
|
@ -41,10 +41,12 @@ function UsersView(props: Props) {
|
|||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<PageTitle
|
||||
title={<div>Team <span className="color-gray-medium">{userCount}</span></div>}
|
||||
actionButton={(
|
||||
<AddUserButton isAdmin={isAdmin} onClick={() => editHandler(null)} />
|
||||
)}
|
||||
title={
|
||||
<div>
|
||||
Team <span className="color-gray-medium">{userCount}</span>
|
||||
</div>
|
||||
}
|
||||
actionButton={<AddUserButton isAdmin={isAdmin} onClick={() => editHandler(null)} />}
|
||||
/>
|
||||
<div>
|
||||
<UserSearch />
|
||||
|
|
@ -55,8 +57,8 @@ function UsersView(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
account: state.getIn([ 'user', 'account' ]),
|
||||
isEnterprise: state.getIn([ 'user', 'account', 'edition' ]) === 'ee',
|
||||
limits: state.getIn([ 'user', 'account', 'limits', 'teamMember' ]),
|
||||
}))(UsersView);
|
||||
export default connect((state: any) => ({
|
||||
account: state.getIn(['user', 'account']),
|
||||
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee',
|
||||
limits: state.getIn(['user', 'account', 'limits', 'teamMember']),
|
||||
}))(UsersView);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ interface Props {
|
|||
isEnterprise?: boolean;
|
||||
}
|
||||
function UserList(props: Props) {
|
||||
const { isEnterprise = false, isOnboarding = false } = props;
|
||||
const { isEnterprise = false, isOnboarding = false } = props;
|
||||
const { userStore } = useStore();
|
||||
const loading = useObserver(() => userStore.loading);
|
||||
const users = useObserver(() => userStore.list);
|
||||
|
|
@ -22,24 +22,24 @@ function UserList(props: Props) {
|
|||
|
||||
const filterList = (list) => {
|
||||
const filterRE = getRE(searchQuery, 'i');
|
||||
let _list = list.filter(w => {
|
||||
let _list = list.filter((w) => {
|
||||
return filterRE.test(w.email) || filterRE.test(w.roleName);
|
||||
});
|
||||
return _list
|
||||
}
|
||||
|
||||
return _list;
|
||||
};
|
||||
|
||||
const list: any = searchQuery !== '' ? filterList(users) : users;
|
||||
const length = list.length;
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
userStore.fetchUsers();
|
||||
}, []);
|
||||
|
||||
const editHandler = (user) => {
|
||||
const editHandler = (user: any) => {
|
||||
userStore.initUser(user).then(() => {
|
||||
showModal(<UserForm />, { });
|
||||
showModal(<UserForm />, { right: true });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
|
|
@ -56,7 +56,7 @@ function UserList(props: Props) {
|
|||
<div className="grid grid-cols-12 p-3 border-b font-medium">
|
||||
<div className="col-span-5">Name</div>
|
||||
<div className="col-span-3">Role</div>
|
||||
{!isOnboarding && <div className="col-span-2">Created On</div> }
|
||||
{!isOnboarding && <div className="col-span-2">Created On</div>}
|
||||
<div className="col-span-2"></div>
|
||||
</div>
|
||||
|
||||
|
|
@ -88,4 +88,4 @@ function UserList(props: Props) {
|
|||
));
|
||||
}
|
||||
|
||||
export default UserList;
|
||||
export default UserList;
|
||||
|
|
|
|||
|
|
@ -3,22 +3,19 @@ import { Icon } from 'UI';
|
|||
import styles from './listItem.module.css';
|
||||
|
||||
const ListItem = ({ webhook, onEdit, onDelete }) => {
|
||||
return (
|
||||
<div className={ styles.wrapper } onClick={ onEdit }>
|
||||
<div>
|
||||
<span>{ webhook.name }</span>
|
||||
<div className={ styles.endpoint }>{ webhook.endpoint }</div>
|
||||
</div>
|
||||
<div className={ styles.actions }>
|
||||
<div className={ styles.button } onClick={ (e) => { e.stopPropagation(); onDelete(webhook) } }>
|
||||
<Icon name="trash" color="teal" size="16" />
|
||||
return (
|
||||
<div className={styles.wrapper} onClick={onEdit}>
|
||||
<div>
|
||||
<span>{webhook.name}</span>
|
||||
<div className={styles.endpoint}>{webhook.endpoint}</div>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.button}>
|
||||
<Icon name="edit" color="teal" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={ styles.button }>
|
||||
<Icon name="edit" color="teal" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
export default ListItem;
|
||||
|
|
|
|||
|
|
@ -4,80 +4,91 @@ import { edit, save } from 'Duck/webhook';
|
|||
import { Form, Button, Input } from 'UI';
|
||||
import styles from './webhookForm.module.css';
|
||||
|
||||
@connect(state => ({
|
||||
webhook: state.getIn(['webhooks', 'instance']),
|
||||
loading: state.getIn(['webhooks', 'saveRequest', 'loading']),
|
||||
}), {
|
||||
edit,
|
||||
save,
|
||||
})
|
||||
@connect(
|
||||
(state) => ({
|
||||
webhook: state.getIn(['webhooks', 'instance']),
|
||||
loading: state.getIn(['webhooks', 'saveRequest', 'loading']),
|
||||
}),
|
||||
{
|
||||
edit,
|
||||
save,
|
||||
}
|
||||
)
|
||||
class WebhookForm extends React.PureComponent {
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [ name ]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [ name ]: value });
|
||||
setFocus = () => this.focusElement.focus();
|
||||
onChangeSelect = (event, { name, value }) => this.props.edit({ [name]: value });
|
||||
write = ({ target: { value, name } }) => this.props.edit({ [name]: value });
|
||||
|
||||
save = () => {
|
||||
this.props.save(this.props.webhook).then(() => {
|
||||
this.props.onClose();
|
||||
});
|
||||
};
|
||||
save = () => {
|
||||
this.props.save(this.props.webhook).then(() => {
|
||||
this.props.onClose();
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { webhook, loading } = this.props;
|
||||
return (
|
||||
<Form className={ styles.wrapper }>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="name"
|
||||
value={ webhook.name }
|
||||
onChange={ this.write }
|
||||
placeholder="Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
render() {
|
||||
const { webhook, loading } = this.props;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '350px' }}>
|
||||
<h3 className="p-5 text-2xl">{webhook.exists() ? 'Update' : 'Add'} Webhook</h3>
|
||||
<Form className={styles.wrapper}>
|
||||
<Form.Field>
|
||||
<label>{'Name'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="name"
|
||||
value={webhook.name}
|
||||
onChange={this.write}
|
||||
placeholder="Name"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Endpoint'}</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="endpoint"
|
||||
value={ webhook.endpoint }
|
||||
onChange={ this.write }
|
||||
placeholder="Endpoint"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>{'Endpoint'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="endpoint"
|
||||
value={webhook.endpoint}
|
||||
onChange={this.write}
|
||||
placeholder="Endpoint"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<label>{'Auth Header (optional)'}</label>
|
||||
<Input
|
||||
ref={ (ref) => { this.focusElement = ref; } }
|
||||
name="authHeader"
|
||||
value={ webhook.authHeader }
|
||||
onChange={ this.write }
|
||||
placeholder="Auth Header"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<label>{'Auth Header (optional)'}</label>
|
||||
<Input
|
||||
ref={(ref) => {
|
||||
this.focusElement = ref;
|
||||
}}
|
||||
name="authHeader"
|
||||
value={webhook.authHeader}
|
||||
onChange={this.write}
|
||||
placeholder="Auth Header"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Button
|
||||
onClick={ this.save }
|
||||
disabled={ !webhook.validate() }
|
||||
loading={ loading }
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{ webhook.exists() ? 'Update' : 'Add' }
|
||||
</Button>
|
||||
{ webhook.exists() && (
|
||||
<Button
|
||||
onClick={ this.props.onClose }
|
||||
>
|
||||
{ 'Cancel' }
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
onClick={this.save}
|
||||
disabled={!webhook.validate()}
|
||||
loading={loading}
|
||||
variant="primary"
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{webhook.exists() ? 'Update' : 'Add'}
|
||||
</Button>
|
||||
{webhook.exists() && <Button onClick={this.props.onClose}>{'Cancel'}</Button>}
|
||||
</div>
|
||||
{webhook.exists() && <Button icon="trash" variant="text" onClick={() => this.props.onDelete(webhook.webhookId)}></Button>}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebhookForm;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { IconButton, SlideModal, Loader, NoContent } from 'UI';
|
||||
import { Button, Loader, NoContent } from 'UI';
|
||||
import { init, fetchList, remove } from 'Duck/webhook';
|
||||
import WebhookForm from './WebhookForm';
|
||||
import ListItem from './ListItem';
|
||||
|
|
@ -10,87 +10,74 @@ import styles from './webhooks.module.css';
|
|||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { confirm } from 'UI';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
|
||||
@connect(state => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
loading: state.getIn(['webhooks', 'loading']),
|
||||
}), {
|
||||
init,
|
||||
fetchList,
|
||||
remove,
|
||||
})
|
||||
@withPageTitle('Webhooks - OpenReplay Preferences')
|
||||
class Webhooks extends React.PureComponent {
|
||||
state = { showModal: false };
|
||||
function Webhooks(props) {
|
||||
const { webhooks, loading } = props;
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchList();
|
||||
}
|
||||
const noSlackWebhooks = webhooks.filter((hook) => hook.type !== 'slack');
|
||||
useEffect(() => {
|
||||
props.fetchList();
|
||||
}, []);
|
||||
|
||||
closeModal = () => this.setState({ showModal: false });
|
||||
init = (v) => {
|
||||
this.props.init(v);
|
||||
this.setState({ showModal: true });
|
||||
}
|
||||
const init = (v) => {
|
||||
props.init(v);
|
||||
showModal(<WebhookForm onClose={hideModal} onDelete={removeWebhook} />);
|
||||
};
|
||||
|
||||
removeWebhook = async (id) => {
|
||||
if (await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to remove this webhook?`
|
||||
})) {
|
||||
this.props.remove(id).then(() => {
|
||||
toast.success('Webhook removed successfully');
|
||||
});
|
||||
}
|
||||
}
|
||||
const removeWebhook = async (id) => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to remove this webhook?`,
|
||||
})
|
||||
) {
|
||||
props.remove(id).then(() => {
|
||||
toast.success('Webhook removed successfully');
|
||||
});
|
||||
hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { webhooks, loading } = this.props;
|
||||
const { showModal } = this.state;
|
||||
|
||||
const noSlackWebhooks = webhooks.filter(hook => hook.type !== 'slack');
|
||||
return (
|
||||
<div>
|
||||
<SlideModal
|
||||
title="Add Webhook"
|
||||
size="small"
|
||||
isDisplayed={ showModal }
|
||||
content={ <WebhookForm onClose={ this.closeModal } /> }
|
||||
onClose={ this.closeModal }
|
||||
/>
|
||||
<div className={ styles.tabHeader }>
|
||||
<h3 className={ cn(styles.tabTitle, "text-2xl") }>{ 'Webhooks' }</h3>
|
||||
<IconButton circle icon="plus" outline onClick={ () => this.init() } />
|
||||
</div>
|
||||
|
||||
<Loader loading={ loading }>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No webhooks available.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={ noSlackWebhooks.size === 0 }
|
||||
// animatedIcon="no-results"
|
||||
>
|
||||
<div className={ styles.list }>
|
||||
{ noSlackWebhooks.map(webhook => (
|
||||
<ListItem
|
||||
key={ webhook.key }
|
||||
webhook={ webhook }
|
||||
onEdit={ () => this.init(webhook) }
|
||||
onDelete={ () => this.removeWebhook(webhook.webhookId) }
|
||||
/>
|
||||
))}
|
||||
<div>
|
||||
<div className={styles.tabHeader}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
|
||||
<Button rounded={true} icon="plus" variant="outline" onClick={() => init()} />
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
|
||||
<div className="mt-6 text-2xl">No webhooks available.</div>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
show={noSlackWebhooks.size === 0}
|
||||
>
|
||||
<div className="cursor-pointer">
|
||||
{noSlackWebhooks.map((webhook) => (
|
||||
<ListItem key={webhook.key} webhook={webhook} onEdit={() => init(webhook)} />
|
||||
))}
|
||||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Webhooks;
|
||||
export default connect(
|
||||
(state) => ({
|
||||
webhooks: state.getIn(['webhooks', 'list']),
|
||||
loading: state.getIn(['webhooks', 'loading']),
|
||||
}),
|
||||
{
|
||||
init,
|
||||
fetchList,
|
||||
remove,
|
||||
}
|
||||
)(withPageTitle('Webhooks - OpenReplay Preferences')(Webhooks));
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import ReactDOM from 'react-dom';
|
|||
import ModalOverlay from './ModalOverlay';
|
||||
|
||||
export default function Modal({ component, props, hideModal }: any) {
|
||||
return component ? ReactDOM.createPortal(
|
||||
<ModalOverlay
|
||||
hideModal={hideModal}
|
||||
left={!props.right}
|
||||
right={props.right}
|
||||
>
|
||||
{component}
|
||||
</ModalOverlay>,
|
||||
document.querySelector("#modal-root"),
|
||||
) : <></>;
|
||||
}
|
||||
return component ? (
|
||||
ReactDOM.createPortal(
|
||||
<ModalOverlay hideModal={hideModal} left={!props.right} right={props.right}>
|
||||
{component}
|
||||
</ModalOverlay>,
|
||||
document.querySelector('#modal-root')
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,14 @@
|
|||
import React from 'react';
|
||||
import stl from './ModalOverlay.module.css'
|
||||
import stl from './ModalOverlay.module.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
function ModalOverlay({ hideModal, children, left = false, right = false }: any) {
|
||||
return (
|
||||
<div className="fixed w-full h-screen" style={{ zIndex: 999 }}>
|
||||
<div
|
||||
onClick={hideModal}
|
||||
className={stl.overlay}
|
||||
style={{ background: "rgba(0,0,0,0.5)" }}
|
||||
/>
|
||||
<div className={cn(stl.slide, { [stl.slideLeft] : left, [stl.slideRight] : right })}>{children}</div>
|
||||
<div onClick={hideModal} className={stl.overlay} style={{ background: 'rgba(0,0,0,0.5)' }} />
|
||||
<div className={cn(stl.slide, { [stl.slideLeft]: left, [stl.slideRight]: right })}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalOverlay;
|
||||
export default ModalOverlay;
|
||||
|
|
|
|||
|
|
@ -3,60 +3,59 @@ import React, { Component, createContext } from 'react';
|
|||
import Modal from './Modal';
|
||||
|
||||
const ModalContext = createContext({
|
||||
component: null,
|
||||
props: {
|
||||
right: false,
|
||||
onClose: () => {},
|
||||
},
|
||||
showModal: (component: any, props: any) => {},
|
||||
hideModal: () => {}
|
||||
component: null,
|
||||
props: {
|
||||
right: true,
|
||||
onClose: () => {},
|
||||
},
|
||||
showModal: (component: any, props: any) => {},
|
||||
hideModal: () => {},
|
||||
});
|
||||
|
||||
export class ModalProvider extends Component {
|
||||
|
||||
handleKeyDown = (e: any) => {
|
||||
if (e.keyCode === 27) {
|
||||
this.hideModal();
|
||||
}
|
||||
}
|
||||
|
||||
showModal = (component, props = { }) => {
|
||||
this.setState({
|
||||
component,
|
||||
props
|
||||
});
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector("body").style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
hideModal = () => {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector("body").style.overflow = 'visible';
|
||||
const { props } = this.state;
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
handleKeyDown = (e: any) => {
|
||||
if (e.keyCode === 27) {
|
||||
this.hideModal();
|
||||
}
|
||||
};
|
||||
this.setState({
|
||||
component: null,
|
||||
props: {}
|
||||
});
|
||||
}
|
||||
|
||||
state = {
|
||||
component: null,
|
||||
props: {},
|
||||
showModal: this.showModal,
|
||||
hideModal: this.hideModal
|
||||
};
|
||||
showModal = (component, props = { right: true }) => {
|
||||
this.setState({
|
||||
component,
|
||||
props,
|
||||
});
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector('body').style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalContext.Provider value={this.state}>
|
||||
<Modal {...this.state} />
|
||||
{this.props.children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
}
|
||||
hideModal = () => {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector('body').style.overflow = 'visible';
|
||||
const { props } = this.state;
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
this.setState({
|
||||
component: null,
|
||||
props: {},
|
||||
});
|
||||
};
|
||||
|
||||
state = {
|
||||
component: null,
|
||||
props: {},
|
||||
showModal: this.showModal,
|
||||
hideModal: this.hideModal,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalContext.Provider value={this.state}>
|
||||
<Modal {...this.state} />
|
||||
{this.props.children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ModalConsumer = ModalContext.Consumer;
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { default as withRequest } from './withRequest';
|
||||
export { default as withToggle } from './withToggle';
|
||||
export { default as withToggle } from './withToggle';
|
||||
export { default as withCopy } from './withCopy'
|
||||
27
frontend/app/components/hocs/withCopy.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Tooltip } from 'react-tippy';
|
||||
|
||||
const withCopy = (WrappedComponent: React.ComponentType) => {
|
||||
const ComponentWithCopy = (props: any) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const { value, tooltip } = props;
|
||||
const copyToClipboard = (text: string) => {
|
||||
copy(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
return (
|
||||
<div onClick={() => copyToClipboard(value)} className="w-fit">
|
||||
<Tooltip delay={0} arrow animation="fade" hideOnClick={false} title={copied ? tooltip : 'Click to copy'}>
|
||||
<WrappedComponent {...props} copyToClipboard={copyToClipboard} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return ComponentWithCopy;
|
||||
};
|
||||
|
||||
export default withCopy;
|
||||
|
|
@ -2,66 +2,66 @@ import React from 'react';
|
|||
import APIClient from 'App/api_client';
|
||||
|
||||
export default ({
|
||||
initialData = null,
|
||||
endpoint = '',
|
||||
method = 'GET',
|
||||
requestName = "request",
|
||||
loadingName = "loading",
|
||||
errorName = "requestError",
|
||||
dataName = "data",
|
||||
dataWrapper = data => data,
|
||||
loadOnInitialize = false,
|
||||
resetBeforeRequest = false, // Probably use handler?
|
||||
}) => BaseComponent => class extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: typeof initialData === 'function' ? initialData(props) : initialData,
|
||||
loading: loadOnInitialize,
|
||||
error: false,
|
||||
};
|
||||
if (loadOnInitialize) {
|
||||
this.request();
|
||||
}
|
||||
}
|
||||
initialData = null,
|
||||
endpoint = '',
|
||||
method = 'GET',
|
||||
requestName = 'request',
|
||||
loadingName = 'loading',
|
||||
errorName = 'requestError',
|
||||
dataName = 'data',
|
||||
dataWrapper = (data) => data,
|
||||
loadOnInitialize = false,
|
||||
resetBeforeRequest = false, // Probably use handler?
|
||||
}) =>
|
||||
(BaseComponent) =>
|
||||
class extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
data: typeof initialData === 'function' ? initialData(props) : initialData,
|
||||
loading: loadOnInitialize,
|
||||
error: false,
|
||||
};
|
||||
if (loadOnInitialize) {
|
||||
this.request();
|
||||
}
|
||||
}
|
||||
|
||||
request = (params, edpParams) => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
error: false,
|
||||
data: resetBeforeRequest
|
||||
? (typeof initialData === 'function' ? initialData(this.props) : initialData)
|
||||
: this.state.data,
|
||||
});
|
||||
const edp = typeof endpoint === 'function'
|
||||
? endpoint(this.props, edpParams)
|
||||
: endpoint;
|
||||
return new APIClient()[ method.toLowerCase() ](edp, params)
|
||||
.then(response => response.json())
|
||||
.then(({ errors, data }) => {
|
||||
if (errors) {
|
||||
return this.setError();
|
||||
}
|
||||
this.setState({
|
||||
data: dataWrapper(data, this.state.data),
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
.catch(this.setError);
|
||||
}
|
||||
request = (params, edpParams) => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
error: false,
|
||||
data: resetBeforeRequest ? (typeof initialData === 'function' ? initialData(this.props) : initialData) : this.state.data,
|
||||
});
|
||||
const edp = typeof endpoint === 'function' ? endpoint(this.props, edpParams) : endpoint;
|
||||
return new APIClient()
|
||||
[method.toLowerCase()](edp, params)
|
||||
.then((response) => response.json())
|
||||
.then(({ errors, data }) => {
|
||||
if (errors) {
|
||||
return this.setError();
|
||||
}
|
||||
this.setState({
|
||||
data: dataWrapper(data, this.state.data),
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
.catch(this.setError);
|
||||
};
|
||||
|
||||
setError = () => this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
})
|
||||
setError = () =>
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: true,
|
||||
});
|
||||
|
||||
render() {
|
||||
const ownProps = {
|
||||
[ requestName ]: this.request,
|
||||
[ loadingName ]: this.state.loading,
|
||||
[ dataName ]: this.state.data,
|
||||
[ errorName ]: this.state.error,
|
||||
};
|
||||
return <BaseComponent { ...this.props } { ...ownProps } />
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const ownProps = {
|
||||
[requestName]: this.request,
|
||||
[loadingName]: this.state.loading,
|
||||
[dataName]: this.state.data,
|
||||
[errorName]: this.state.error,
|
||||
};
|
||||
return <BaseComponent {...this.props} {...ownProps} />;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,65 +3,79 @@ import { Modal, Icon, Tabs } from 'UI';
|
|||
import styles from './trackingCodeModal.module.css';
|
||||
import { editGDPR, saveGDPR } from 'Duck/site';
|
||||
import { connect } from 'react-redux';
|
||||
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
||||
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
||||
import InstallDocs from './InstallDocs';
|
||||
import cn from 'classnames';
|
||||
|
||||
const PROJECT = 'Using Script';
|
||||
const DOCUMENTATION = 'Using NPM';
|
||||
const TABS = [
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
{ key: DOCUMENTATION, text: DOCUMENTATION },
|
||||
{ key: PROJECT, text: PROJECT },
|
||||
];
|
||||
|
||||
class TrackingCodeModal extends React.PureComponent {
|
||||
state = { copied: false, changed: false, activeTab: DOCUMENTATION };
|
||||
state = { copied: false, changed: false, activeTab: DOCUMENTATION };
|
||||
|
||||
setActiveTab = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
}
|
||||
setActiveTab = (tab) => {
|
||||
this.setState({ activeTab: tab });
|
||||
};
|
||||
|
||||
renderActiveTab = () => {
|
||||
const { site } = this.props;
|
||||
switch (this.state.activeTab) {
|
||||
case PROJECT:
|
||||
return <ProjectCodeSnippet />;
|
||||
case DOCUMENTATION:
|
||||
return <InstallDocs site={site} />;
|
||||
renderActiveTab = () => {
|
||||
const { site } = this.props;
|
||||
switch (this.state.activeTab) {
|
||||
case PROJECT:
|
||||
return <ProjectCodeSnippet />;
|
||||
case DOCUMENTATION:
|
||||
return <InstallDocs site={site} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { site, displayed, onClose, title = '', subTitle } = this.props;
|
||||
const { activeTab } = this.state;
|
||||
return (
|
||||
<div className="bg-white h-screen overflow-y-auto" style={{ width: '700px' }}>
|
||||
<h3 className="p-5 text-2xl">
|
||||
{title} {subTitle && <span className="text-sm color-gray-dark">{subTitle}</span>}
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<Tabs className="px-5" tabs={TABS} active={activeTab} onClick={this.setActiveTab} />
|
||||
<div className="p-5">{this.renderActiveTab()}</div>
|
||||
</div>
|
||||
</div>
|
||||
// displayed &&
|
||||
// <Modal size="large" onClose={ onClose } open={ displayed } style={{ top: "85px" }} >
|
||||
// <Modal.Header className={ styles.modalHeader }>
|
||||
// <div>{ title } { subTitle && <span className="text-sm color-gray-dark">{subTitle}</span>}</div>
|
||||
// <div className={ cn(styles.closeButton, { 'hidden' : !onClose }) } role="button" tabIndex="-1" onClick={ onClose }>
|
||||
// <Icon name="close" size="14" />
|
||||
// </div>
|
||||
// </Modal.Header>
|
||||
// <Modal.Content className={ cn(styles.content, 'overflow-y-auto') }>
|
||||
// <Tabs
|
||||
// className="px-5"
|
||||
// tabs={ TABS }
|
||||
// active={ activeTab } onClick={ this.setActiveTab } />
|
||||
// <div className="p-5">
|
||||
// { this.renderActiveTab() }
|
||||
// </div>
|
||||
// </Modal.Content>
|
||||
// </Modal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { site, displayed, onClose, title = '', subTitle } = this.props;
|
||||
const { activeTab } = this.state;
|
||||
return (
|
||||
displayed &&
|
||||
<Modal size="large" onClose={ onClose } open={ displayed } style={{ top: "85px" }} >
|
||||
<Modal.Header className={ styles.modalHeader }>
|
||||
<div>{ title } { subTitle && <span className="text-sm color-gray-dark">{subTitle}</span>}</div>
|
||||
<div className={ cn(styles.closeButton, { 'hidden' : !onClose }) } role="button" tabIndex="-1" onClick={ onClose }>
|
||||
<Icon name="close" size="14" />
|
||||
</div>
|
||||
</Modal.Header>
|
||||
<Modal.Content className={ cn(styles.content, 'overflow-y-auto') }>
|
||||
<Tabs
|
||||
className="px-5"
|
||||
tabs={ TABS }
|
||||
active={ activeTab } onClick={ this.setActiveTab } />
|
||||
<div className="p-5">
|
||||
{ this.renderActiveTab() }
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
site: state.getIn([ 'site', 'instance' ]),
|
||||
gdpr: state.getIn([ 'site', 'instance', 'gdpr' ]),
|
||||
saving: state.getIn([ 'site', 'saveGDPR', 'loading' ]),
|
||||
}), {
|
||||
editGDPR, saveGDPR
|
||||
})(TrackingCodeModal);
|
||||
export default connect(
|
||||
(state) => ({
|
||||
site: state.getIn(['site', 'instance']),
|
||||
gdpr: state.getIn(['site', 'instance', 'gdpr']),
|
||||
saving: state.getIn(['site', 'saveGDPR', 'loading']),
|
||||
}),
|
||||
{
|
||||
editGDPR,
|
||||
saveGDPR,
|
||||
}
|
||||
)(TrackingCodeModal);
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@ import React from 'react';
|
|||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
onSubmit?: any
|
||||
[x: string]: any
|
||||
onSubmit?: any;
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
|
||||
interface FormFieldProps {
|
||||
children: React.ReactNode;
|
||||
[x: string]: any
|
||||
[x: string]: any;
|
||||
}
|
||||
function FormField (props: FormFieldProps) {
|
||||
function FormField(props: FormFieldProps) {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<div {...rest} className="flex flex-col mb-4 form-field">
|
||||
|
|
@ -20,16 +19,18 @@ function FormField (props: FormFieldProps) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function Form(props: Props) {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<form {...rest} onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.onSubmit) {
|
||||
props.onSubmit(e);
|
||||
}
|
||||
}}>
|
||||
<form
|
||||
{...rest}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (props.onSubmit) {
|
||||
props.onSubmit(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
|
|
@ -37,4 +38,4 @@ function Form(props: Props) {
|
|||
|
||||
Form.Field = FormField;
|
||||
|
||||
export default Form;
|
||||
export default Form;
|
||||
|
|
|
|||
|
|
@ -11,13 +11,14 @@ interface Props {
|
|||
rows?: number;
|
||||
[x: string]: any;
|
||||
}
|
||||
function Input(props: Props) {
|
||||
const Input = React.forwardRef((props: Props, ref: any) => {
|
||||
const { className = '', leadingButton = '', wrapperClassName = '', icon = '', type = 'text', rows = 4, ...rest } = props;
|
||||
return (
|
||||
<div className={cn({ relative: icon || leadingButton }, wrapperClassName)}>
|
||||
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />}
|
||||
{type === 'textarea' ? (
|
||||
<textarea
|
||||
ref={ref}
|
||||
rows={rows}
|
||||
style={{ resize: 'none' }}
|
||||
maxLength={500}
|
||||
|
|
@ -26,6 +27,7 @@ function Input(props: Props) {
|
|||
/>
|
||||
) : (
|
||||
<input
|
||||
ref={ref}
|
||||
type={type}
|
||||
style={{ height: '36px' }}
|
||||
className={cn('p-2 border border-gray-light bg-white w-full rounded', className, { 'pl-10': icon })}
|
||||
|
|
@ -36,6 +38,6 @@ function Input(props: Props) {
|
|||
{leadingButton && <div className="absolute top-0 bottom-0 right-0">{leadingButton}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default Input;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,14 @@
|
|||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
function TextLink({
|
||||
target = '_blank',
|
||||
href = '',
|
||||
icon = '',
|
||||
label='',
|
||||
className = ''
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
target={target}
|
||||
className={cn('cursor-pointer flex items-center default-hover', className)}
|
||||
href={href}
|
||||
>
|
||||
{ icon && <Icon name={icon} size="16" color="gray-medium" marginRight="5" /> }
|
||||
{label}
|
||||
</a>
|
||||
)
|
||||
function TextLink({ target = '_blank', href = '', icon = '', label = '', className = '' }) {
|
||||
return (
|
||||
<a target={target} className={cn('link cursor-pointer flex items-center default-hover', className)} href={href}>
|
||||
{icon && <Icon name={icon} size="16" color="teal" marginRight="5" />}
|
||||
{label}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextLink
|
||||
export default TextLink;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,14 @@
|
|||
import React from 'react';
|
||||
import styles from './toggler.module.css';
|
||||
|
||||
export default ({
|
||||
onChange,
|
||||
name,
|
||||
className = '',
|
||||
checked,
|
||||
label = '',
|
||||
plain = false,
|
||||
}) => (
|
||||
<div className={ className }>
|
||||
<label className={styles.label}>
|
||||
<div className={ plain ? styles.switchPlain : styles.switch }>
|
||||
<input
|
||||
type={ styles.checkbox }
|
||||
onClick={ onChange }
|
||||
name={ name }
|
||||
checked={ checked }
|
||||
/>
|
||||
<span className={ `${ plain ? styles.sliderPlain : styles.slider } ${ checked ? styles.checked : '' }` } />
|
||||
</div>
|
||||
{ label && <span>{ label }</span> }
|
||||
</label>
|
||||
</div>
|
||||
export default ({ onChange, name, className = '', checked, label = '', plain = false }) => (
|
||||
<div className={className}>
|
||||
<label className={styles.label}>
|
||||
<div className={plain ? styles.switchPlain : styles.switch}>
|
||||
<input type={styles.checkbox} onClick={onChange} name={name} checked={checked} />
|
||||
<span className={`${plain ? styles.sliderPlain : styles.slider} ${checked ? styles.checked : ''}`} />
|
||||
</div>
|
||||
{label && <span>{label}</span>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,39 +1,47 @@
|
|||
import { array } from '../funcTools/tools';
|
||||
import { fetchListType, saveType, editType, initType, removeType } from '../funcTools/types';
|
||||
import { fetchListType, fetchType, saveType, editType, initType, removeType } from '../funcTools/types';
|
||||
|
||||
export function fetchList(name) {
|
||||
return {
|
||||
types: fetchListType(name).array,
|
||||
call: client => client.get(`/integrations/${ name }`),
|
||||
name
|
||||
};
|
||||
return {
|
||||
types: fetchListType(name).array,
|
||||
call: (client) => client.get(`/integrations/${name}`),
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetch(name, siteId) {
|
||||
return {
|
||||
types: fetchType(name).array,
|
||||
call: (client) => client.get(siteId && name !== 'github' && name !== 'jira' ? `/${siteId}/integrations/${name}` : `/integrations/${name}`),
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
export function save(name, siteId, instance) {
|
||||
return {
|
||||
types: saveType(name).array,
|
||||
call: client => client.post( (siteId ? `/${siteId}` : '') + `/integrations/${ name }`, instance.toData()),
|
||||
};
|
||||
return {
|
||||
types: saveType(name).array,
|
||||
call: (client) => client.post((siteId ? `/${siteId}` : '') + `/integrations/${name}`, instance.toData()),
|
||||
};
|
||||
}
|
||||
|
||||
export function edit(name, instance) {
|
||||
return {
|
||||
type: editType(name),
|
||||
instance,
|
||||
};
|
||||
return {
|
||||
type: editType(name),
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
export function init(name, instance) {
|
||||
return {
|
||||
type: initType(name),
|
||||
instance,
|
||||
};
|
||||
return {
|
||||
type: initType(name),
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
export function remove(name, siteId) {
|
||||
return {
|
||||
types: removeType(name).array,
|
||||
call: client => client.delete((siteId ? `/${siteId}` : '') + `/integrations/${ name }`),
|
||||
siteId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
types: removeType(name).array,
|
||||
call: (client) => client.delete((siteId ? `/${siteId}` : '') + `/integrations/${name}`),
|
||||
siteId,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,23 +11,25 @@ import JiraConfig from 'Types/integrations/jiraConfig';
|
|||
import GithubConfig from 'Types/integrations/githubConfig';
|
||||
import IssueTracker from 'Types/integrations/issueTracker';
|
||||
import slack from './slack';
|
||||
import integrations from './integrations';
|
||||
|
||||
import { createIntegrationReducer } from './reducer'
|
||||
import { createIntegrationReducer } from './reducer';
|
||||
|
||||
export default {
|
||||
sentry: createIntegrationReducer("sentry", SentryConfig),
|
||||
datadog: createIntegrationReducer("datadog", DatadogConfig),
|
||||
stackdriver: createIntegrationReducer("stackdriver", StackdriverConfig),
|
||||
rollbar: createIntegrationReducer("rollbar", RollbarConfig),
|
||||
newrelic: createIntegrationReducer("newrelic", NewrelicConfig),
|
||||
bugsnag: createIntegrationReducer("bugsnag", BugsnagConfig),
|
||||
cloudwatch: createIntegrationReducer("cloudwatch", CloudWatch),
|
||||
elasticsearch: createIntegrationReducer("elasticsearch", ElasticsearchConfig),
|
||||
sumologic: createIntegrationReducer("sumologic", SumoLogicConfig),
|
||||
jira: createIntegrationReducer("jira", JiraConfig),
|
||||
issues: createIntegrationReducer("issues", IssueTracker),
|
||||
github: createIntegrationReducer("github", GithubConfig),
|
||||
slack,
|
||||
export default {
|
||||
sentry: createIntegrationReducer('sentry', SentryConfig),
|
||||
datadog: createIntegrationReducer('datadog', DatadogConfig),
|
||||
stackdriver: createIntegrationReducer('stackdriver', StackdriverConfig),
|
||||
rollbar: createIntegrationReducer('rollbar', RollbarConfig),
|
||||
newrelic: createIntegrationReducer('newrelic', NewrelicConfig),
|
||||
bugsnag: createIntegrationReducer('bugsnag', BugsnagConfig),
|
||||
cloudwatch: createIntegrationReducer('cloudwatch', CloudWatch),
|
||||
elasticsearch: createIntegrationReducer('elasticsearch', ElasticsearchConfig),
|
||||
sumologic: createIntegrationReducer('sumologic', SumoLogicConfig),
|
||||
jira: createIntegrationReducer('jira', JiraConfig),
|
||||
github: createIntegrationReducer('github', GithubConfig),
|
||||
issues: createIntegrationReducer('issues', IssueTracker),
|
||||
slack,
|
||||
integrations,
|
||||
};
|
||||
|
||||
export * from './actions';
|
||||
|
|
|
|||
40
frontend/app/duck/integrations/integrations.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Map } from 'immutable';
|
||||
import withRequestState from 'Duck/requestStateCreator';
|
||||
import { fetchListType } from '../funcTools/types';
|
||||
|
||||
const FETCH_LIST = fetchListType('integrations/FETCH_LIST');
|
||||
const SET_SITE_ID = 'integrations/SET_SITE_ID';
|
||||
const initialState = Map({
|
||||
list: [],
|
||||
siteId: null,
|
||||
});
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_LIST.success:
|
||||
return state.set('list', action.data);
|
||||
case SET_SITE_ID:
|
||||
return state.set('siteId', action.siteId);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default withRequestState(
|
||||
{
|
||||
fetchRequest: FETCH_LIST,
|
||||
},
|
||||
reducer
|
||||
);
|
||||
|
||||
export function fetchIntegrationList(siteID) {
|
||||
return {
|
||||
types: FETCH_LIST.array,
|
||||
call: (client) => client.get(`/${siteID}/integrations`),
|
||||
};
|
||||
}
|
||||
|
||||
export function setSiteId(siteId) {
|
||||
return {
|
||||
type: SET_SITE_ID,
|
||||
siteId,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,48 +1,52 @@
|
|||
import { List, Map } from 'immutable';
|
||||
import { createRequestReducer } from '../funcTools/request';
|
||||
import { fetchListType, saveType, removeType, editType, initType } from '../funcTools/types';
|
||||
import { fetchListType, saveType, removeType, editType, initType, fetchType } from '../funcTools/types';
|
||||
import { createItemInListUpdater } from '../funcTools/tools';
|
||||
|
||||
const idKey = 'siteId';
|
||||
const itemInListUpdater = createItemInListUpdater(idKey);
|
||||
|
||||
export const createIntegrationReducer = (name, Config) => {
|
||||
const FETCH_LIST = fetchListType(name);
|
||||
const SAVE = saveType(name);
|
||||
const REMOVE = removeType(name);
|
||||
const EDIT = editType(name);
|
||||
const INIT = initType(name);
|
||||
const FETCH_LIST = fetchListType(name);
|
||||
const SAVE = saveType(name);
|
||||
const REMOVE = removeType(name);
|
||||
const EDIT = editType(name);
|
||||
const INIT = initType(name);
|
||||
const FETCH = fetchType(name);
|
||||
|
||||
const initialState = Map({
|
||||
instance: Config(),
|
||||
list: List(),
|
||||
fetched: false,
|
||||
issuesFetched: false
|
||||
});
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_LIST.success:
|
||||
return state.set('list', Array.isArray(action.data) ?
|
||||
List(action.data).map(Config) : List([new Config(action.data)])).set(action.name + 'Fetched', true);
|
||||
case SAVE.success:
|
||||
const config = Config(action.data);
|
||||
return state
|
||||
.update('list', itemInListUpdater(config))
|
||||
.set('instance', config);
|
||||
case REMOVE.success:
|
||||
return state
|
||||
.update('list', list => list.filter(site => site.siteId !== action.siteId))
|
||||
.set('instance', Config())
|
||||
case EDIT:
|
||||
return state.mergeIn([ 'instance' ], action.instance);
|
||||
case INIT:
|
||||
return state.set('instance', Config(action.instance));
|
||||
}
|
||||
return state;
|
||||
};
|
||||
return createRequestReducer({
|
||||
fetchRequest: FETCH_LIST,
|
||||
saveRequest: SAVE,
|
||||
removeRequest: REMOVE,
|
||||
}, reducer);
|
||||
}
|
||||
const initialState = Map({
|
||||
instance: Config(),
|
||||
list: List(),
|
||||
fetched: false,
|
||||
issuesFetched: false,
|
||||
});
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_LIST.success:
|
||||
return state
|
||||
.set('list', Array.isArray(action.data) ? List(action.data).map(Config) : List([new Config(action.data)]))
|
||||
.set(action.name + 'Fetched', true);
|
||||
case FETCH.success:
|
||||
return state.set('instance', Config(action.data));
|
||||
case SAVE.success:
|
||||
const config = Config(action.data);
|
||||
return state.update('list', itemInListUpdater(config)).set('instance', config);
|
||||
case REMOVE.success:
|
||||
return state.update('list', (list) => list.filter((site) => site.siteId !== action.siteId)).set('instance', Config());
|
||||
case EDIT:
|
||||
return state.mergeIn(['instance'], action.instance);
|
||||
case INIT:
|
||||
return state.set('instance', Config(action.instance));
|
||||
}
|
||||
return state;
|
||||
};
|
||||
return createRequestReducer(
|
||||
{
|
||||
// fetchRequest: FETCH_LIST,
|
||||
fetchRequest: FETCH,
|
||||
saveRequest: SAVE,
|
||||
removeRequest: REMOVE,
|
||||
},
|
||||
reducer
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,77 +13,76 @@ const idKey = 'webhookId';
|
|||
const itemInListUpdater = createItemInListUpdater(idKey);
|
||||
|
||||
const initialState = Map({
|
||||
instance: Config(),
|
||||
list: List(),
|
||||
instance: Config(),
|
||||
list: List(),
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case FETCH_LIST.SUCCESS:
|
||||
return state.set('list', List(action.data).map(Config));
|
||||
case UPDATE.SUCCESS:
|
||||
case SAVE.SUCCESS:
|
||||
const config = Config(action.data);
|
||||
return state
|
||||
.update('list', itemInListUpdater(config))
|
||||
.set('instance', config);
|
||||
case REMOVE.SUCCESS:
|
||||
return state
|
||||
.update('list', list => list.filter(item => item.webhookId !== action.id))
|
||||
.set('instance', Config())
|
||||
case EDIT:
|
||||
return state.mergeIn([ 'instance' ], action.instance);
|
||||
case INIT:
|
||||
return state.set('instance', Config(action.instance));
|
||||
}
|
||||
return state;
|
||||
switch (action.type) {
|
||||
case FETCH_LIST.SUCCESS:
|
||||
return state.set('list', List(action.data).map(Config));
|
||||
case UPDATE.SUCCESS:
|
||||
case SAVE.SUCCESS:
|
||||
const config = Config(action.data);
|
||||
return state.update('list', itemInListUpdater(config)).set('instance', config);
|
||||
case REMOVE.SUCCESS:
|
||||
return state.update('list', (list) => list.filter((item) => item.webhookId !== action.id)).set('instance', Config());
|
||||
case EDIT:
|
||||
return state.mergeIn(['instance'], action.instance);
|
||||
case INIT:
|
||||
return state.set('instance', Config(action.instance));
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default withRequestState({
|
||||
fetchRequest: FETCH_LIST,
|
||||
saveRequest: SAVE,
|
||||
removeRequest: REMOVE,
|
||||
}, reducer);
|
||||
export default withRequestState(
|
||||
{
|
||||
fetchRequest: FETCH_LIST,
|
||||
saveRequest: SAVE,
|
||||
removeRequest: REMOVE,
|
||||
},
|
||||
reducer
|
||||
);
|
||||
|
||||
export function fetchList() {
|
||||
return {
|
||||
types: FETCH_LIST.toArray(),
|
||||
call: client => client.get('/integrations/slack/channels'),
|
||||
};
|
||||
return {
|
||||
types: FETCH_LIST.toArray(),
|
||||
call: (client) => client.get('/integrations/slack/channels'),
|
||||
};
|
||||
}
|
||||
|
||||
export function save(instance) {
|
||||
return {
|
||||
types: SAVE.toArray(),
|
||||
call: client => client.post(`/integrations/slack`, instance.toData()),
|
||||
};
|
||||
return {
|
||||
types: SAVE.toArray(),
|
||||
call: (client) => client.post(`/integrations/slack`, instance.toData()),
|
||||
};
|
||||
}
|
||||
|
||||
export function update(instance) {
|
||||
return {
|
||||
types: UPDATE.toArray(),
|
||||
call: client => client.put(`/integrations/slack/${instance.webhookId}`, instance.toData()),
|
||||
};
|
||||
return {
|
||||
types: UPDATE.toArray(),
|
||||
call: (client) => client.put(`/integrations/slack/${instance.webhookId}`, instance.toData()),
|
||||
};
|
||||
}
|
||||
|
||||
export function edit(instance) {
|
||||
return {
|
||||
type: EDIT,
|
||||
instance,
|
||||
};
|
||||
return {
|
||||
type: EDIT,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
export function init(instance) {
|
||||
return {
|
||||
type: INIT,
|
||||
instance,
|
||||
};
|
||||
return {
|
||||
type: INIT,
|
||||
instance,
|
||||
};
|
||||
}
|
||||
|
||||
export function remove(id) {
|
||||
return {
|
||||
types: REMOVE.toArray(),
|
||||
call: client => client.delete(`/integrations/slack/${id}`),
|
||||
id,
|
||||
};
|
||||
}
|
||||
return {
|
||||
types: REMOVE.toArray(),
|
||||
call: (client) => client.delete(`/integrations/slack/${id}`),
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
<svg width="2500" height="1719" viewBox="0 0 256 176" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M57.838 170.017c.151 1.663-.051 3.789-.14 5.436h56.864c.053-1.654.091-3.311.091-4.974 0-39.942-15.768-76.266-44.011-104.51C56.704 52.032 40.885 41.31 23.246 33.898L0 86.328c33.989 15.82 54.211 43.783 57.838 83.689zm69.197-1.644c.108 2.371-.062 4.732-.167 7.08h58.177c.077-2.355.13-4.714.13-7.08 0-28.826-5.66-56.82-16.82-83.207-10.767-25.456-26.169-48.306-45.778-67.915a216.421 216.421 0 0 0-15.686-14.218l-37.68 44.315c37.293 33.313 55.304 65.858 57.824 121.025zM235.263 64.39C226.595 41.785 213.935 19.521 198.727 0l-46.95 34.442c27.495 35.099 44.442 79.71 46.058 127.612.152 4.502-.164 8.969-.457 13.399h58.252c.226-4.448.447-8.916.344-13.399-.805-34.945-8.23-65.12-20.71-97.665z" fill="#3676A1"/></svg>
|
||||
<svg width="58" height="80" viewBox="0 0 58 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28.9431 55.1235C30.7258 55.1235 32.1709 53.6784 32.1709 51.8957C32.1709 50.113 30.7258 48.6678 28.9431 48.6678C27.1604 48.6678 25.7153 50.113 25.7153 51.8957C25.7153 53.6784 27.1604 55.1235 28.9431 55.1235Z" fill="#303F9F"/>
|
||||
<path d="M28.9431 78.9612C21.7674 78.9532 14.8878 76.0991 9.81374 71.0251C4.7397 65.9511 1.8856 59.0715 1.87762 51.8957V38.4743C1.87762 37.9402 2.08961 37.428 2.46701 37.0502C2.8444 36.6724 3.35635 36.4598 3.89038 36.4592H13.4904L13.4579 5.38672L5.90313 10.036V27.7287C5.90313 28.2626 5.69107 28.7745 5.31361 29.152C4.93615 29.5294 4.42419 29.7415 3.89038 29.7415C3.35656 29.7415 2.84461 29.5294 2.46715 29.152C2.08968 28.7745 1.87762 28.2626 1.87762 27.7287V9.80643C1.87915 9.18865 2.03815 8.58147 2.33962 8.04224C2.64108 7.50301 3.07505 7.04955 3.60052 6.72469L11.9692 1.57455C12.5174 1.23696 13.1458 1.05179 13.7895 1.03816C14.4331 1.02454 15.0688 1.18295 15.6308 1.49703C16.1928 1.81112 16.6608 2.26951 16.9865 2.82488C17.3121 3.38025 17.4837 4.01247 17.4834 4.65629L17.5182 36.4592H28.9431C31.9963 36.4587 34.981 37.3637 37.5198 39.0596C40.0587 40.7555 42.0376 43.1662 43.2063 45.9868C44.375 48.8074 44.681 51.9113 44.0856 54.9058C43.4903 57.9003 42.0203 60.6511 39.8615 62.8102C37.7028 64.9692 34.9523 66.4396 31.9578 67.0355C28.9634 67.6313 25.8595 67.3257 23.0387 66.1574C20.2179 64.9891 17.8069 63.0106 16.1106 60.472C14.4143 57.9334 13.5089 54.9489 13.5089 51.8957L13.495 40.487H5.90313V51.8957C5.90313 56.4526 7.2544 60.9071 9.78607 64.696C12.3177 68.485 15.9161 71.4381 20.1261 73.1819C24.3361 74.9257 28.9687 75.382 33.438 74.493C37.9073 73.604 42.0127 71.4097 45.2349 68.1874C48.4571 64.9652 50.6514 60.8599 51.5404 56.3906C52.4294 51.9213 51.9732 47.2887 50.2293 43.0787C48.4855 38.8687 45.5324 35.2703 41.7435 32.7386C37.9546 30.207 33.5 28.8557 28.9431 28.8557H25.451C24.9171 28.8557 24.4052 28.6436 24.0277 28.2662C23.6503 27.8887 23.4382 27.3768 23.4382 26.843C23.4382 26.3091 23.6503 25.7972 24.0277 25.4197C24.4052 25.0423 24.9171 24.8302 25.451 24.8302H28.9431C36.1214 24.8302 43.0056 27.6817 48.0813 32.7575C53.1571 37.8333 56.0086 44.7175 56.0086 51.8957C56.0086 59.0739 53.1571 65.9581 48.0813 71.0339C43.0056 76.1097 36.1214 78.9612 28.9431 78.9612V78.9612ZM17.5228 40.487V51.8934C17.5224 54.1499 18.1911 56.3559 19.4444 58.2324C20.6978 60.1088 22.4794 61.5715 24.564 62.4353C26.6486 63.2992 28.9426 63.5254 31.1558 63.0855C33.3691 62.6455 35.4021 61.5591 36.9979 59.9637C38.5937 58.3683 39.6805 56.3354 40.1208 54.1223C40.5612 51.9092 40.3354 49.6151 39.472 47.5303C38.6086 45.4455 37.1463 43.6636 35.2701 42.4099C33.3939 41.1562 31.1881 40.487 28.9315 40.487H17.5228Z" fill="#303F9F"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 835 B After Width: | Height: | Size: 2.7 KiB |
|
|
@ -1 +1,12 @@
|
|||
<svg id="CMYK_-_square" data-name="CMYK - square" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 681.02 551.55"><defs><style>.cls-1{fill:#0097a0;}.cls-2{fill:#5bc6cc;}.cls-3{fill:#231f20;}</style></defs><title>NewRelic-logo-square</title><g id="outlines"><path class="cls-1" d="M692.8,220.54C660.86,73.7,484.77-12.68,299.47,27.61s-309.63,192-277.7,338.83,208,233.22,393.32,192.93S724.72,367.37,692.8,220.54ZM344.87,476.79c-103.41,0-187.2-83.82-187.2-187.22s83.8-187.19,187.2-187.19,187.2,83.81,187.2,187.19S448.25,476.79,344.87,476.79Z" transform="translate(-16.78 -17.71)"/><path class="cls-2" d="M391.53,57.56c-132.32,0-239.61,107.28-239.61,239.6S259.21,536.78,391.53,536.78,631.15,429.49,631.15,297.16,523.85,57.56,391.53,57.56ZM344.87,473.78c-101.75,0-184.19-82.47-184.19-184.21S243.12,105.4,344.87,105.4,529,187.85,529,289.57,446.58,473.78,344.87,473.78Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M278.93,271.2l-20.19-42.33c-4.82-10-9.77-21.36-11.46-26.7l-.39.39c.65,7.55.78,17.06.91,25l.52,43.63H233.61V181.08h16.93l21.88,44a164.17,164.17,0,0,1,9.25,23.18l.39-.39c-.39-4.56-1.3-17.45-1.3-25.66l-.26-41.15h14.2V271.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M321.51,242.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.73a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.27,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H346C346,220.93,341.31,214.55,334.15,214.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M437,271.46H423.61l-8.07-30.34c-2.08-7.81-4.3-18-4.3-18H411s-1,6.51-4.3,18.62l-7.94,29.69H385.32l-18-65.25,14.2-2,7.16,31.91c1.82,8.2,3.39,17.32,3.39,17.32h.39a178.91,178.91,0,0,1,3.78-17.71l8.47-30.47h14.07L426.22,235c2.74,10.68,4.17,18.75,4.17,18.75h.39s1.56-10,3.26-17.71l6.77-30.74h14.85Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M267.62,387.2l-7.81-13.94c-6.25-11.07-10.42-17.32-15.37-22.27a7.64,7.64,0,0,0-5.86-2.73V387.2H223.86V297.08h27.48c20.19,0,29.3,11.72,29.3,25.79,0,12.89-8.33,24.75-22.4,24.75,3.26,1.69,9.25,10.42,13.93,18l13.28,21.62Zm-20.84-78h-8.21v28.52h7.68c7.81,0,12-1,14.72-3.78,2.47-2.47,4-6.25,4-10.94C265,313.88,260.06,309.19,246.78,309.19Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M305.12,358.16v1c0,9.12,3.39,18.75,16.28,18.75,6.12,0,11.46-2.21,16.41-6.51l5.6,8.72a35.59,35.59,0,0,1-23.7,8.73c-18.62,0-30.34-13.41-30.34-34.51,0-11.59,2.47-19.28,8.21-25.79,5.34-6.12,11.85-8.86,20.19-8.86a25.45,25.45,0,0,1,18.1,6.77c5.73,5.21,8.6,13.28,8.6,28.65v3Zm12.63-27.61c-8.07,0-12.5,6.38-12.5,17.06H329.6C329.6,336.93,324.92,330.55,317.75,330.55Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M371.28,388.63c-14.46,0-14.46-13-14.46-18.62V313.88a106.72,106.72,0,0,0-1.3-19.27l14.72-3.26c1,4,1.17,9.51,1.17,18.1v55.87c0,8.86.39,10.29,1.43,11.85a4,4,0,0,0,4.69,1l2.34,8.86A22.44,22.44,0,0,1,371.28,388.63Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M396.15,311.53A9.34,9.34,0,0,1,386.9,302a9.44,9.44,0,1,1,9.25,9.51ZM389,387.2V322.34l14.46-2.6V387.2Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M444.46,388.89c-18,0-28-12.63-28-33.86,0-24,14.33-35.42,29-35.42,7.16,0,12.37,1.69,18.23,7.16l-7.16,9.51c-3.91-3.52-7.29-5.08-11.07-5.08a11.2,11.2,0,0,0-10.42,6.64c-2,4-2.73,10.16-2.73,18.36,0,9,1.43,14.72,4.43,18A11.58,11.58,0,0,0,445.5,378c4.56,0,9-2.21,13.28-6.51l6.77,8.72C459.57,386.16,453.32,388.89,444.46,388.89Z" transform="translate(-16.78 -17.71)"/><path class="cls-3" d="M477.78,388.64A9.67,9.67,0,1,1,487.4,379,9.63,9.63,0,0,1,477.78,388.64Zm0-17.42a7.78,7.78,0,1,0,7.44,7.75A7.55,7.55,0,0,0,477.78,371.22Zm1.9,13.1c-.42-.73-.6-1-1-1.79-1.07-2-1.4-2.5-1.79-2.65a.72.72,0,0,0-.34-.08v4.52H474.4V373.48h4a3,3,0,0,1,3.2,3.17,2.78,2.78,0,0,1-2.42,3,2.47,2.47,0,0,1,.44.47c.62.78,2.6,4.21,2.6,4.21Zm-1.14-8.94a4.35,4.35,0,0,0-1.22-.16h-.78v2.94h.73c.94,0,1.35-.1,1.64-.36a1.53,1.53,0,0,0,.42-1.09A1.28,1.28,0,0,0,478.53,375.38Z" transform="translate(-16.78 -17.71)"/></g></svg>
|
||||
<svg width="71" height="81" viewBox="0 0 71 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_2090)">
|
||||
<path d="M57.0479 28.0403V52.9611L35.3182 65.424V81.0001L70.6399 60.7517V20.2498L57.0479 28.0403Z" fill="#00AC69"/>
|
||||
<path d="M35.3215 15.5812L57.0512 28.039L70.6433 20.2484L35.3215 0L0 20.2484L13.5868 28.039L35.3215 15.5812Z" fill="#1CE783"/>
|
||||
<path d="M21.7348 48.2938V73.2146L35.3215 81.0001V40.5032L-6.10352e-05 20.2498V35.8309L21.7348 48.2938Z" fill="black" fill-opacity="0.87"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_2090">
|
||||
<rect width="70.8739" height="81" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 633 B |
|
|
@ -1,20 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="rollbar-mark-color" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 304 240" style="enable-background:new 0 0 304 240;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#3A4757;}
|
||||
.st1{fill:#F7941D;}
|
||||
.st2{fill:#BFD730;}
|
||||
.st3{fill:#00BAD9;}
|
||||
</style>
|
||||
<title>rollbar-logo-color-vertical</title>
|
||||
<g id="icon">
|
||||
<path class="st0" d="M303.8,239.1V25.7c-0.5-13.6-0.9-34.3-31.4-21.9C221.7,22.4,170.6,40.2,120.3,60C82.2,75,40.5,91.4,19,171.6
|
||||
c-5.6,21-13.4,46.4-19,67.5h49.4c4.6-17,10.2-38.4,14.8-55.4c15.4-57.4,45.6-69.3,73.2-80.1C176.9,88,217,73.8,257,59.1v179.9
|
||||
H303.8z"/>
|
||||
<path class="st1" d="M119,124.5c-5,2.8-9.8,6.1-14.1,9.9c-14.9,13.3-23,32.2-28,51.1l-14.1,51.9H119V124.5z"/>
|
||||
<path class="st2" d="M180.1,99.7c-12.7,4.7-25.3,9.6-38,14.5c-3.4,1.3-6.7,2.6-10,4v119.2H180L180.1,99.7z"/>
|
||||
<path class="st3" d="M243.8,237.4v-161c-16.8,6.1-33.7,12.2-50.5,18.4v142.6H243.8z"/>
|
||||
<svg width="88" height="71" viewBox="0 0 88 71" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_27_2051)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M84.9393 2.09491C84.9382 1.95919 84.9245 1.82386 84.8988 1.69059C84.8988 1.65016 84.8785 1.61377 84.8664 1.57333C84.8543 1.5329 84.8138 1.39139 84.7815 1.30244L84.721 1.17306C84.6834 1.09588 84.64 1.02156 84.5916 0.950679C84.5632 0.906204 84.5351 0.86173 84.5027 0.817255L84.4459 0.740435C84.4136 0.704046 84.3773 0.675744 84.345 0.643397L84.264 0.546361L84.1993 0.501886C84.1401 0.451215 84.0781 0.403969 84.0134 0.360372L83.8556 0.259293C83.7862 0.222856 83.7146 0.190456 83.6415 0.162255L83.4717 0.0935209C83.3949 0.0692618 83.3139 0.0571321 83.2371 0.0409594L83.0633 0.00457045C82.9676 -0.00152348 82.8718 -0.00152348 82.7761 0.00457045H82.6144C81.531 0.101607 67.214 1.46821 51.122 8.64491C41.4586 12.9428 33.9786 19.5414 29.2724 27.5509L28.0596 28.0766C10.8314 35.7666 0.537354 50.7145 0.537354 68.0637V68.3549C0.537763 68.7543 0.652111 69.1453 0.866967 69.4819C1.08183 69.8185 1.38827 70.0867 1.75031 70.2553C2.02817 70.3826 2.33014 70.4488 2.63578 70.4493H59.8268C59.9381 70.4499 60.049 70.4404 60.1584 70.4209L60.3041 70.3847C60.3727 70.3644 60.4416 70.3523 60.5103 70.3281C60.5789 70.3039 60.6112 70.2795 60.6638 70.2553C60.7164 70.2311 60.7851 70.2069 60.8419 70.1746C60.9486 70.1114 61.0499 70.0397 61.1451 69.9602L84.1914 50.5286C84.427 50.3306 84.6158 50.0829 84.7444 49.8031C84.873 49.5235 84.9382 49.219 84.9353 48.9111V2.09491H84.9393ZM64.1005 61.9788L61.889 63.8427V22.501L80.7466 6.60713V47.9489L64.1005 61.9788ZM26.4503 51.0177H57.696V66.2567H8.36905L26.4503 51.0177ZM52.8241 12.4779C60.1892 9.23502 67.8935 6.82515 75.7937 5.29308L58.9375 19.505C51.1569 20.4543 43.5042 22.2563 36.1176 24.8784C40.2903 19.7839 45.9182 15.5588 52.8241 12.4779ZM32.1108 30.9068C40.3192 27.528 48.9123 25.1719 57.696 23.8918V46.825H27.825C28.0404 41.2644 29.5051 35.8237 32.1108 30.9068ZM26.3573 33.5835C24.5907 38.1675 23.6516 43.0286 23.5836 47.9408L4.98487 63.6365C6.39191 50.8844 13.9648 40.2183 26.3573 33.5835Z" fill="#3569F3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_27_2051">
|
||||
<rect width="86.7778" height="71" fill="white" transform="translate(0.537354)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -4,44 +4,47 @@ import Record from 'Types/Record';
|
|||
export const SECRET_ACCESS_KEY_LENGTH = 40;
|
||||
export const ACCESS_KEY_ID_LENGTH = 20;
|
||||
|
||||
export default Record({
|
||||
projectId: undefined,
|
||||
provider: 'github',
|
||||
token: ''
|
||||
}, {
|
||||
idKey: 'projectId',
|
||||
fromJS: ({ projectId, ...config }) => ({
|
||||
...config,
|
||||
projectId: projectId === undefined ? projectId : `${ projectId }`,
|
||||
}),
|
||||
methods: {
|
||||
validate() {
|
||||
// return this.jiraProjectId !== '' && this.username !== '' && this.token !== '' && validateURL(this.url);
|
||||
return this.token !== '';
|
||||
export default Record(
|
||||
{
|
||||
projectId: undefined,
|
||||
provider: 'github',
|
||||
token: '',
|
||||
},
|
||||
exists() {
|
||||
return this.projectId !== undefined;
|
||||
{
|
||||
idKey: 'projectId',
|
||||
fromJS: ({ projectId, ...config }) => ({
|
||||
...config,
|
||||
projectId: projectId === undefined ? projectId : `${projectId}`,
|
||||
}),
|
||||
methods: {
|
||||
validate() {
|
||||
// return this.jiraProjectId !== '' && this.username !== '' && this.token !== '' && validateURL(this.url);
|
||||
return this.token !== '';
|
||||
},
|
||||
exists() {
|
||||
return !!this.token;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export const regionLabels = {
|
||||
"us-east-1": "US East (N. Virginia)",
|
||||
"us-east-2": "US East (Ohio)",
|
||||
"us-west-1": "US West (N. California)",
|
||||
"us-west-2": "US West (Oregon)",
|
||||
"ap-east-1": "Asia Pacific (Hong Kong)",
|
||||
"ap-south-1": "Asia Pacific (Mumbai)",
|
||||
"ap-northeast-2": "Asia Pacific (Seoul)",
|
||||
"ap-southeast-1": "Asia Pacific (Singapore)",
|
||||
"ap-southeast-2": "Asia Pacific (Sydney)",
|
||||
"ap-northeast-1": "Asia Pacific (Tokyo)",
|
||||
"ca-central-1": "Canada (Central)",
|
||||
"eu-central-1": "EU (Frankfurt)",
|
||||
"eu-west-1": "EU (Ireland)",
|
||||
"eu-west-2": "EU (London)",
|
||||
"eu-west-3": "EU (Paris)",
|
||||
"eu-north-1": "EU (Stockholm)",
|
||||
"me-south-1": "Middle East (Bahrain)",
|
||||
"sa-east-1": "South America (São Paulo)"
|
||||
'us-east-1': 'US East (N. Virginia)',
|
||||
'us-east-2': 'US East (Ohio)',
|
||||
'us-west-1': 'US West (N. California)',
|
||||
'us-west-2': 'US West (Oregon)',
|
||||
'ap-east-1': 'Asia Pacific (Hong Kong)',
|
||||
'ap-south-1': 'Asia Pacific (Mumbai)',
|
||||
'ap-northeast-2': 'Asia Pacific (Seoul)',
|
||||
'ap-southeast-1': 'Asia Pacific (Singapore)',
|
||||
'ap-southeast-2': 'Asia Pacific (Sydney)',
|
||||
'ap-northeast-1': 'Asia Pacific (Tokyo)',
|
||||
'ca-central-1': 'Canada (Central)',
|
||||
'eu-central-1': 'EU (Frankfurt)',
|
||||
'eu-west-1': 'EU (Ireland)',
|
||||
'eu-west-2': 'EU (London)',
|
||||
'eu-west-3': 'EU (Paris)',
|
||||
'eu-north-1': 'EU (Stockholm)',
|
||||
'me-south-1': 'Middle East (Bahrain)',
|
||||
'sa-east-1': 'South America (São Paulo)',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,48 +4,51 @@ import { validateURL } from 'App/validate';
|
|||
export const SECRET_ACCESS_KEY_LENGTH = 40;
|
||||
export const ACCESS_KEY_ID_LENGTH = 20;
|
||||
|
||||
export default Record({
|
||||
projectId: undefined,
|
||||
username: '',
|
||||
token: '',
|
||||
url: '',
|
||||
// jiraProjectId: '',
|
||||
}, {
|
||||
idKey: 'projectId',
|
||||
fromJS: ({ projectId, ...config }) => ({
|
||||
...config,
|
||||
projectId: projectId === undefined ? projectId : `${ projectId }`,
|
||||
}),
|
||||
methods: {
|
||||
validateFetchProjects() {
|
||||
return this.username !== '' && this.token !== '' && validateURL(this.url);
|
||||
export default Record(
|
||||
{
|
||||
projectId: undefined,
|
||||
username: '',
|
||||
token: '',
|
||||
url: '',
|
||||
// jiraProjectId: '',
|
||||
},
|
||||
validate() {
|
||||
return this.username !== '' && this.token !== '' && validateURL(this.url);
|
||||
},
|
||||
exists() {
|
||||
return this.projectId !== undefined;
|
||||
{
|
||||
idKey: 'projectId',
|
||||
fromJS: ({ projectId, ...config }) => ({
|
||||
...config,
|
||||
projectId: projectId === undefined ? projectId : `${projectId}`,
|
||||
}),
|
||||
methods: {
|
||||
validateFetchProjects() {
|
||||
return this.username !== '' && this.token !== '' && validateURL(this.url);
|
||||
},
|
||||
validate() {
|
||||
return this.username !== '' && this.token !== '' && validateURL(this.url);
|
||||
},
|
||||
exists() {
|
||||
return !!this.token;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export const regionLabels = {
|
||||
"us-east-1": "US East (N. Virginia)",
|
||||
"us-east-2": "US East (Ohio)",
|
||||
"us-west-1": "US West (N. California)",
|
||||
"us-west-2": "US West (Oregon)",
|
||||
"ap-east-1": "Asia Pacific (Hong Kong)",
|
||||
"ap-south-1": "Asia Pacific (Mumbai)",
|
||||
"ap-northeast-2": "Asia Pacific (Seoul)",
|
||||
"ap-southeast-1": "Asia Pacific (Singapore)",
|
||||
"ap-southeast-2": "Asia Pacific (Sydney)",
|
||||
"ap-northeast-1": "Asia Pacific (Tokyo)",
|
||||
"ca-central-1": "Canada (Central)",
|
||||
"eu-central-1": "EU (Frankfurt)",
|
||||
"eu-west-1": "EU (Ireland)",
|
||||
"eu-west-2": "EU (London)",
|
||||
"eu-west-3": "EU (Paris)",
|
||||
"eu-north-1": "EU (Stockholm)",
|
||||
"me-south-1": "Middle East (Bahrain)",
|
||||
"sa-east-1": "South America (São Paulo)"
|
||||
'us-east-1': 'US East (N. Virginia)',
|
||||
'us-east-2': 'US East (Ohio)',
|
||||
'us-west-1': 'US West (N. California)',
|
||||
'us-west-2': 'US West (Oregon)',
|
||||
'ap-east-1': 'Asia Pacific (Hong Kong)',
|
||||
'ap-south-1': 'Asia Pacific (Mumbai)',
|
||||
'ap-northeast-2': 'Asia Pacific (Seoul)',
|
||||
'ap-southeast-1': 'Asia Pacific (Singapore)',
|
||||
'ap-southeast-2': 'Asia Pacific (Sydney)',
|
||||
'ap-northeast-1': 'Asia Pacific (Tokyo)',
|
||||
'ca-central-1': 'Canada (Central)',
|
||||
'eu-central-1': 'EU (Frankfurt)',
|
||||
'eu-west-1': 'EU (Ireland)',
|
||||
'eu-west-2': 'EU (London)',
|
||||
'eu-west-3': 'EU (Paris)',
|
||||
'eu-north-1': 'EU (Stockholm)',
|
||||
'me-south-1': 'Middle East (Bahrain)',
|
||||
'sa-east-1': 'South America (São Paulo)',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -343,3 +343,8 @@ export const setSessionFilter = (filter: any) => {
|
|||
export const compareJsonObjects = (obj1: any, obj2: any) => {
|
||||
return JSON.stringify(obj1) === JSON.stringify(obj2);
|
||||
};
|
||||
|
||||
export const getInitials = (name: any) => {
|
||||
const names = name.split(' ');
|
||||
return names.slice(0, 2).map((n: any) => n[0]).join('');
|
||||
}
|
||||
|
|
@ -97,7 +97,7 @@ export default class Assist {
|
|||
observer && observer.observe(titleNode, { subtree: true, characterData: true, childList: true, })
|
||||
})
|
||||
app.attachStopCallback(() => {
|
||||
if (this.assistDemandedRestart) { return }
|
||||
if (this.assistDemandedRestart) { return }
|
||||
this.clean()
|
||||
observer && observer.disconnect()
|
||||
})
|
||||
|
|
@ -134,10 +134,10 @@ export default class Assist {
|
|||
query: {
|
||||
'peerId': peerID,
|
||||
'identity': 'session',
|
||||
'sessionInfo': JSON.stringify({
|
||||
'sessionInfo': JSON.stringify({
|
||||
pageTitle: document.title,
|
||||
active: true,
|
||||
...this.app.getSessionInfo(),
|
||||
...this.app.getSessionInfo(),
|
||||
}),
|
||||
},
|
||||
transports: ['websocket',],
|
||||
|
|
@ -249,7 +249,7 @@ export default class Assist {
|
|||
peer.on('error', e => app.debug.warn('Peer error: ', e.type, e))
|
||||
peer.on('disconnected', () => peer.reconnect())
|
||||
peer.on('call', (call) => {
|
||||
app.debug.log('Call: ', call)
|
||||
app.debug.log('Call: ', call)
|
||||
if (this.callingState !== CallingState.False) {
|
||||
call.close()
|
||||
//this.notifyCallEnd() // TODO: strictly connect calling peer with agent socket.id
|
||||
|
|
|
|||