diff --git a/ui/server/server.js b/ui/server/server.js index cd9dad0b1..b9bc5ff51 100644 --- a/ui/server/server.js +++ b/ui/server/server.js @@ -205,6 +205,10 @@ app.get('/:projectId/annotation/:id', (req, res) => { res = addHeaders(res); }); +app.get('/permission-settings', (req, res) => { + res.render("permission-settings", params); +}); + app.get('/token', (req, res) => { res.render('token', params); }); diff --git a/ui/server/views/includes/permission-settings/single-view.html b/ui/server/views/includes/permission-settings/single-view.html new file mode 100644 index 000000000..d9f6b01a5 --- /dev/null +++ b/ui/server/views/includes/permission-settings/single-view.html @@ -0,0 +1,345 @@ + + + + + + + + + diff --git a/ui/server/views/includes/permission-settings/table-view.html b/ui/server/views/includes/permission-settings/table-view.html new file mode 100644 index 000000000..d15e2c6e9 --- /dev/null +++ b/ui/server/views/includes/permission-settings/table-view.html @@ -0,0 +1,248 @@ + + + + + + + diff --git a/ui/server/views/permission-settings.html b/ui/server/views/permission-settings.html new file mode 100644 index 000000000..0465e792b --- /dev/null +++ b/ui/server/views/permission-settings.html @@ -0,0 +1,98 @@ +{% extends "tator-base.html" %} {% block head %} +Tator | Permission Settings + +{% endblock head %} {% block body %} {% include +"includes/permission-settings/table-view.html" %} {% include +"includes/permission-settings/single-view.html" %} + + +{% include "includes/components/side-nav-link.html" %} + + +{% endblock body %} diff --git a/ui/src/css/components/button.css b/ui/src/css/components/button.css index 89a761ea1..89e71146d 100644 --- a/ui/src/css/components/button.css +++ b/ui/src/css/components/button.css @@ -219,3 +219,8 @@ .background-purple { background-color: var(--color-purple) !important; } + +.btn.selected { + color: var(--color-white); + background-color: var(--color-purple); +} diff --git a/ui/src/css/components/permission-settings.css b/ui/src/css/components/permission-settings.css new file mode 100644 index 000000000..dab8ae0d9 --- /dev/null +++ b/ui/src/css/components/permission-settings.css @@ -0,0 +1,159 @@ +.permission-table { + background-color: #00070d; + width: 100%; +} +.permission-table tr.selected, +.permission-table tr:hover { + background-color: #1e2129; + color: #ffffff; + fill: #a2afcd; +} +.permission-table th { + text-align: center; + padding-top: 6px; + padding-bottom: 6px; + border: 1px solid #a2afcd; + background-color: #696cff; + overflow-wrap: break-word; +} +.permission-table td { + vertical-align: middle; + text-align: center; + padding-top: 3px; + padding-bottom: 3px; + border: 1px solid #a2afcd; + overflow-wrap: break-word; +} +.permission-table td > *, +.permission-table th > * { + display: flex; + justify-content: center; + align-items: center; +} + +.table-cell-padding { + padding-left: var(--spacing-3); + padding-right: var(--spacing-3); +} + +.long-text-td { + position: relative; + cursor: pointer; +} +.long-text-td span { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.long-text-td::after { + content: attr(data-tooltip); + visibility: hidden; + opacity: 0; + + transition: opacity 0.1s ease-in-out; + background-color: var(--color-purple-200); + color: black; + padding: 10px; + border-radius: 4px; + position: absolute; + top: 50%; + white-space: normal; + max-width: 50%; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); + z-index: 1; +} +.long-text-td:hover::after { + visibility: visible; + opacity: 1; +} + +.card-list { + border: 1px solid var(--color-charcoal--light); + padding: var(--spacing-3); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: var(--spacing-2); + max-height: 330px; + overflow-y: auto; +} +.group-member-card, +.user-group-card { + border: 1px solid var(--color-charcoal--light); + border-radius: 5%; +} +.group-member-card.to-be-added, +.user-group-card.to-be-added { + border: 1px dashed var(--color-parakeet-green); +} +.group-member-card.to-be-deleted, +.user-group-card.to-be-deleted { + border: 1px dashed var(--color-coral); +} +.group-member-card--username, +.group-member-card--email, +.user-group-card--id, +.user-group-card--name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; /* Show "..." for overflowing text */ +} + +.calculator-table-div { + max-height: 500px; + overflow-y: auto; +} +.calculator-table { + font-size: 12px; +} +.calculator-table th { + vertical-align: middle; + position: sticky; + top: 0; + z-index: 1; +} +.calculator-table thead tr { + height: 30px; +} +/* Second header row to make header's 2 rows are both sticky */ +.calculator-table thead tr:nth-child(2) th { + top: 30px; +} + +.calculator-table tr.ord-row { + font-weight: bold; + color: var(--color-purple-400); +} +.calculator-table tr.final-row { + font-weight: bold; + color: var(--color-purple); +} + +.ord-row td:nth-child(1), +.final-row td:nth-child(1) { + text-align: left; + padding-left: 3px; +} + +/* Webkit scrollbar styling for .calculator-table-div */ +.calculator-table-div::-webkit-scrollbar { + width: 10px; +} +.calculator-table-div::-webkit-scrollbar-track { + background: #e0e0e0; +} +.calculator-table-div::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 5px; + border: 2px solid #e0e0e0; +} +/* Firefox scrollbar styling for .calculator-table-div div */ +.calculator-table-div { + scrollbar-width: thin; + scrollbar-color: #888 #e0e0e0; +} + +.table-view-filter { + background-color: var(--color-charcoal--medium); + box-sizing: border-box; +} diff --git a/ui/src/css/styles.css b/ui/src/css/styles.css index 8e222a51b..53ad547b0 100644 --- a/ui/src/css/styles.css +++ b/ui/src/css/styles.css @@ -33,3 +33,4 @@ @import url("components/toggle-content.css"); @import url("components/tooltip.css"); @import url("components/entity-panel-image.css"); +@import url("components/permission-settings.css"); diff --git a/ui/src/css/utilities.css b/ui/src/css/utilities.css index 6363d3420..1b39876a6 100644 --- a/ui/src/css/utilities.css +++ b/ui/src/css/utilities.css @@ -34,6 +34,10 @@ justify-content: space-between; } +.flex-justify-around { + justify-content: space-around; +} + .flex-justify-right { justify-content: flex-end; } diff --git a/ui/src/js/components/nav-main.js b/ui/src/js/components/nav-main.js index 34c4a8dea..dd468139d 100644 --- a/ui/src/js/components/nav-main.js +++ b/ui/src/js/components/nav-main.js @@ -30,6 +30,12 @@ export class NavMain extends TatorElement { organizations.textContent = "Organizations"; this._primary.appendChild(organizations); + const permissions = document.createElement("a"); + permissions.setAttribute("class", "nav__link"); + permissions.setAttribute("href", "/permission-settings/"); + permissions.textContent = "Permissions"; + this._primary.appendChild(permissions); + this._changePassword = document.createElement("a"); this._changePassword.setAttribute("class", "nav__link"); this._changePassword.setAttribute("href", "/password-reset-request"); diff --git a/ui/src/js/permission-settings/components/permission-settings-button.js b/ui/src/js/permission-settings/components/permission-settings-button.js new file mode 100644 index 000000000..ac03a0dc5 --- /dev/null +++ b/ui/src/js/permission-settings/components/permission-settings-button.js @@ -0,0 +1,255 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { svgNamespace } from "../../components/tator-element.js"; + +/// Generic button implementation +export class PermissionSettingsButton extends TatorElement { + constructor() { + super(); + } + + init(titleText, svgPath, viewbox) { + if (viewbox == undefined) { + viewbox = "0 0 32 32"; + } + this._button = document.createElement("button"); + this._button.setAttribute( + "class", + "btn-clear py-2 px-0 text-gray hover-text-white d-flex flex-items-center" + ); + this._shadow.appendChild(this._button); + + const svg = document.createElementNS(svgNamespace, "svg"); + svg.setAttribute("viewBox", viewbox); + svg.setAttribute("height", "1em"); + svg.setAttribute("width", "1em"); + svg.setAttribute("fill", "none"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + this._button.appendChild(svg); + + const title = document.createElementNS(svgNamespace, "title"); + title.textContent = titleText; + svg.appendChild(title); + + const path = document.createElementNS(svgNamespace, "path"); + path.setAttribute("d", svgPath); + svg.appendChild(path); + + this._disabled = false; + + this.addEventListener("click", (evt) => { + if (this._disabled) { + console.log("1"); + evt.stopImmediatePropagation(); + return false; + } + }); + } + + initWithSvg(titleText, svgElement) { + this._button = document.createElement("button"); + this._button.setAttribute( + "class", + "annotation__shape btn-clear py-3 px-3 d-flex rounded-2 text-gray hover-text-white" + ); + this._shadow.appendChild(this._button); + + this._button.appendChild(svgElement); + const title = document.createElementNS(svgNamespace, "title"); + title.textContent = titleText; + svgElement.appendChild(title); + } + + static get observedAttributes() { + return ["class", "disabled", "href"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "class": + if (this.classList.contains("is-selected")) { + this._button.classList.add("is-selected"); + } else { + this._button.classList.remove("is-selected"); + } + break; + case "disabled": + if (newValue === null) { + this._button.removeAttribute("disabled"); + this._disabled = false; + } else { + this._button.setAttribute("disabled", ""); + this._disabled = true; + } + break; + case "href": + this._button.setAttribute("href", newValue); + break; + } + } +} + +/// Specific implementations built on svg + name +class EditLineButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Edit Line", + "M373.2 16.97C395.1-4.901 430.5-4.901 452.4 16.97L495 59.6C516.9 81.47 516.9 116.9 495 138.8L182.3 451.6C170.9 462.9 156.9 471.2 141.5 475.8L20.52 511.3C14.9 512.1 8.827 511.5 4.687 507.3C.5466 503.2-1.002 497.1 .6506 491.5L36.23 370.5C40.76 355.1 49.09 341.1 60.44 329.7L373.2 16.97zM429.8 39.6C420.4 30.22 405.2 30.22 395.8 39.6L341 94.4L417.6 170.1L472.4 116.2C481.8 106.8 481.8 91.6 472.4 82.23L429.8 39.6zM109.6 402.4L173.4 415.2L394.1 193.6L318.4 117L96.84 338.6L109.6 402.4zM70.51 370.2C69.08 373.2 67.88 376.3 66.93 379.5L39.63 472.4L132.4 445.1C135.7 444.1 138.8 442.9 141.8 441.5L92.86 431.7C86.53 430.4 81.58 425.5 80.31 419.1L70.51 370.2z", + "0 0 512 512" + ); + } +} + +class GrantAllButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Grant All Permission", + "M536 96c-22.09 0-40 17.91-40 40c0 8.998 3.521 16.89 8.537 23.57l-89.63 71.7c-5.955 4.764-12.99 7.019-19.94 7.019c-11.61 0-22.98-6.298-28.68-17.7l-57.6-115.2C320 98.34 327.1 86.34 327.1 72C327.1 49.91 310.1 32 288 32S247.1 49.91 247.1 72c0 14.34 7.963 26.34 19.3 33.4L209.7 220.6C204 231.1 192.6 238.3 181 238.3c-6.945 0-13.98-2.255-19.94-7.019l-89.63-71.7C76.48 152.9 79.1 144.1 79.1 136c0-22.09-17.91-40-40-40s-40 17.91-40 40s17.91 40 40 40c.248 0 .457-.1164 .7051-.1203l50.52 277.8C93.99 468.9 107.2 480 122.7 480h330.6c15.46 0 28.72-11.06 31.48-26.27l50.52-277.9C535.5 175.9 535.8 176 536 176C558.1 176 576 158.1 576 136S558.1 96 536 96zM439.9 432H136.1l-33.89-186.4l28.94 23.15c14.14 11.31 31.87 17.54 49.92 17.54c30.53 0 57.97-16.95 71.61-44.23L288 171.3l35.37 70.73c13.64 27.28 41.08 44.23 71.61 44.23c18.06 0 35.79-6.229 49.92-17.54l28.94-23.15L439.9 432z", + "0 0 576 512" + ); + } +} +class RemovePermissionButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Remove This Policy", + "M384 208C401.7 208 416 222.3 416 240V272C416 289.7 401.7 304 384 304H128C110.3 304 96 289.7 96 272V240C96 222.3 110.3 208 128 208H384zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z", + "0 0 512 512" + ); + } +} +class ChangeBackButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Back to Original", + "M40 16C53.25 16 64 26.75 64 40v102.1C103.7 75.57 176.3 32.11 256.1 32.11C379.6 32.11 480 132.5 480 256s-100.4 223.9-223.9 223.9c-52.31 0-103.3-18.33-143.5-51.77c-10.19-8.5-11.56-23.62-3.062-33.81c8.5-10.22 23.66-11.56 33.81-3.062C174.9 417.5 214.9 432 256 432c97.03 0 176-78.97 176-176S353 80 256 80c-66.54 0-126.8 38.28-156.5 96H200C213.3 176 224 186.8 224 200S213.3 224 200 224h-160C26.75 224 16 213.3 16 200v-160C16 26.75 26.75 16 40 16z", + "0 0 512 512" + ); + } +} +class HasPermissionButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Remove this permission", + "M440.1 103C450.3 112.4 450.3 127.6 440.1 136.1L176.1 400.1C167.6 410.3 152.4 410.3 143 400.1L7.029 264.1C-2.343 255.6-2.343 240.4 7.029 231C16.4 221.7 31.6 221.7 40.97 231L160 350.1L407 103C416.4 93.66 431.6 93.66 440.1 103V103z", + "0 0 448 512" + ); + } +} +class NoPermissionButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Grant this permission", + "M312.1 375c9.369 9.369 9.369 24.57 0 33.94s-24.57 9.369-33.94 0L160 289.9l-119 119c-9.369 9.369-24.57 9.369-33.94 0s-9.369-24.57 0-33.94L126.1 256L7.027 136.1c-9.369-9.369-9.369-24.57 0-33.94s24.57-9.369 33.94 0L160 222.1l119-119c9.369-9.369 24.57-9.369 33.94 0s9.369 24.57 0 33.94L193.9 256L312.1 375z", + "0 0 320 512" + ); + } +} +class SortButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Sort", + "M0 96C0 78.33 14.33 64 32 64H416C433.7 64 448 78.33 448 96C448 113.7 433.7 128 416 128H32C14.33 128 0 113.7 0 96zM0 256C0 238.3 14.33 224 32 224H288C305.7 224 320 238.3 320 256C320 273.7 305.7 288 288 288H32C14.33 288 0 273.7 0 256zM160 448H32C14.33 448 0 433.7 0 416C0 398.3 14.33 384 32 384H160C177.7 384 192 398.3 192 416C192 433.7 177.7 448 160 448z", + "0 0 448 512" + ); + } +} +class SortAscendingButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Go Descending", + "M544 416h-223.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H544c17.67 0 32-14.33 32-32S561.7 416 544 416zM320 96h32c17.67 0 31.1-14.33 31.1-32s-14.33-32-31.1-32h-32c-17.67 0-32 14.33-32 32S302.3 96 320 96zM320 224H416c17.67 0 32-14.33 32-32s-14.33-32-32-32h-95.1c-17.67 0-32 14.33-32 32S302.3 224 320 224zM320 352H480c17.67 0 32-14.33 32-32s-14.33-32-32-32h-159.1c-17.67 0-32 14.33-32 32S302.3 352 320 352zM151.6 41.95c-12.12-13.26-35.06-13.26-47.19 0l-87.1 96.09C4.475 151.1 5.35 171.4 18.38 183.3c6.141 5.629 13.89 8.414 21.61 8.414c8.672 0 17.3-3.504 23.61-10.39L96 145.9v302C96 465.7 110.3 480 128 480s32-14.33 32-32.03V145.9L192.4 181.3C204.4 194.3 224.6 195.3 237.6 183.3c13.03-11.95 13.9-32.22 1.969-45.27L151.6 41.95z", + "0 0 576 512" + ); + } +} +class SortDescendingButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Go Ascending", + "M320 224H416c17.67 0 32-14.33 32-32s-14.33-32-32-32h-95.1c-17.67 0-32 14.33-32 32S302.3 224 320 224zM320 352H480c17.67 0 32-14.33 32-32s-14.33-32-32-32h-159.1c-17.67 0-32 14.33-32 32S302.3 352 320 352zM320 96h32c17.67 0 31.1-14.33 31.1-32s-14.33-32-31.1-32h-32c-17.67 0-32 14.33-32 32S302.3 96 320 96zM544 416h-223.1c-17.67 0-32 14.33-32 32s14.33 32 32 32H544c17.67 0 32-14.33 32-32S561.7 416 544 416zM192.4 330.7L160 366.1V64.03C160 46.33 145.7 32 128 32S96 46.33 96 64.03v302L63.6 330.7c-6.312-6.883-14.94-10.38-23.61-10.38c-7.719 0-15.47 2.781-21.61 8.414c-13.03 11.95-13.9 32.22-1.969 45.27l87.1 96.09c12.12 13.26 35.06 13.26 47.19 0l87.1-96.09c11.94-13.05 11.06-33.31-1.969-45.27C224.6 316.8 204.4 317.7 192.4 330.7z", + "0 0 576 512" + ); + } +} +class XMarkButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Remove", + "M393.4 41.37C405.9 28.88 426.1 28.88 438.6 41.37C451.1 53.87 451.1 74.13 438.6 86.63L269.3 255.1L438.6 425.4C451.1 437.9 451.1 458.1 438.6 470.6C426.1 483.1 405.9 483.1 393.4 470.6L223.1 301.3L54.63 470.6C42.13 483.1 21.87 483.1 9.372 470.6C-3.124 458.1-3.124 437.9 9.372 425.4L178.7 255.1L9.372 86.63C-3.124 74.13-3.124 53.87 9.372 41.37C21.87 28.88 42.13 28.88 54.63 41.37L223.1 210.7L393.4 41.37z", + "0 0 448 512" + ); + this._button.classList.remove("py-2"); + } +} +class FilterAddButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Remove", + "M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z", + "0 0 448 512" + ); + } +} +class QuestionMarkButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "", + "M144 416c-17.67 0-32 14.33-32 32s14.33 32.01 32 32.01s32-14.34 32-32.01S161.7 416 144 416zM211.2 32H104C46.66 32 0 78.66 0 136v16C0 165.3 10.75 176 24 176S48 165.3 48 152v-16c0-30.88 25.12-56 56-56h107.2C244.7 80 272 107.3 272 140.8c0 22.66-12.44 43.27-32.5 53.81L167 232.8C137.1 248 120 277.9 120 310.6V328c0 13.25 10.75 24.01 24 24.01S168 341.3 168 328V310.6c0-14.89 8.188-28.47 21.38-35.41l72.47-38.14C297.7 218.2 320 181.3 320 140.8C320 80.81 271.2 32 211.2 32z", + "0 0 320 512" + ); + } +} +class PermissionCalculatorButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Effective Permission Calculator", + "M192 344c13.26 0 24-10.75 24-24S205.3 296 192 296S168 306.7 168 320S178.7 344 192 344zM192 256c13.26 0 24-10.75 24-24S205.3 208 192 208S168 218.7 168 232S178.7 256 192 256zM280 344c13.26 0 24-10.75 24-24s-10.74-24-24-24S256 306.7 256 320S266.7 344 280 344zM280 256c13.26 0 24-10.75 24-24S293.3 208 280 208S256 218.7 256 232S266.7 256 280 256zM280 432c13.26 0 24-10.75 24-24S293.3 384 280 384S256 394.7 256 408S266.7 432 280 432zM104 432h80c13.26 0 24-10.75 24-24S197.3 384 184 384h-80c-13.26 0-24 10.75-24 24S90.74 432 104 432zM320 0H64C28.65 0 0 28.65 0 64v384c0 35.35 28.65 64 64 64h256c35.35 0 64-28.65 64-64V64C384 28.65 355.3 0 320 0zM336 448c0 8.836-7.164 16-16 16H64c-8.836 0-16-7.164-16-16V176h288V448zM336 128h-288V64c0-8.838 7.164-16 16-16h256c8.836 0 16 7.162 16 16V128zM104 256C117.3 256 128 245.3 128 232S117.3 208 104 208S80 218.7 80 232S90.74 256 104 256zM104 344C117.3 344 128 333.3 128 320S117.3 296 104 296S80 306.7 80 320S90.74 344 104 344z", + "0 0 384 512" + ); + } +} +class DisallowPermissionButton extends PermissionSettingsButton { + constructor() { + super(); + this.init( + "Disallow All Permission", + "M384 208C401.7 208 416 222.3 416 240V272C416 289.7 401.7 304 384 304H128C110.3 304 96 289.7 96 272V240C96 222.3 110.3 208 128 208H384zM512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256zM256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48z", + "0 0 512 512" + ); + } +} + +customElements.define("permission-settings-button", PermissionSettingsButton); +customElements.define("edit-line-button", EditLineButton); +customElements.define("grant-all-button", GrantAllButton); +customElements.define("remove-permission-button", RemovePermissionButton); +customElements.define("change-back-button", ChangeBackButton); +customElements.define("has-permission-button", HasPermissionButton); +customElements.define("no-permission-button", NoPermissionButton); +customElements.define("sort-button", SortButton); +customElements.define("sort-ascending-button", SortAscendingButton); +customElements.define("sort-descending-button", SortDescendingButton); +customElements.define("x-mark-button", XMarkButton); +customElements.define("filter-add-button", FilterAddButton); +customElements.define("question-mark-button", QuestionMarkButton); +customElements.define( + "permission-calculator-button", + PermissionCalculatorButton +); +customElements.define("disallow-permission-button", DisallowPermissionButton); diff --git a/ui/src/js/permission-settings/components/permission-settings-nav-link.js b/ui/src/js/permission-settings/components/permission-settings-nav-link.js new file mode 100644 index 000000000..c181284cb --- /dev/null +++ b/ui/src/js/permission-settings/components/permission-settings-nav-link.js @@ -0,0 +1,94 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class PermissionSettingsNavLink extends TatorElement { + constructor() { + super(); + + // Main Div wrapper + const template = document.getElementById("settings-nav-link").content; + this._shadow.appendChild(template.cloneNode(true)); + + // Template handles for page + // Remove add button and sub items + this._subNavGroup = this._shadow.getElementById("sub-nav"); + this._subNavGroup.removeChild( + this._shadow.getElementById("sub-nav--section") + ); + this._headingGroup = this._shadow.getElementById("sub-nav--heading-group"); + this._headingGroup.removeChild( + this._shadow.getElementById("sub-nav--plus-link") + ); + this._headingButton = this._shadow.getElementById( + "sub-nav--heading-button" + ); + this._headingIcon = this._shadow.getElementById("sub-nav--icon"); + this._subNavLabel = this._shadow.getElementById("sub-nav--label"); + } + + static get observedAttributes() { + return ["type"]; + } + + attributeChangedCallback(prop, oldValue, newValue) { + if (prop === "type") { + this._type = newValue; + this.setAttribute("id", `nav-for-${this._type}`); + this._headingButton.setAttribute("type", this._type); + } + } + + connectedCallback() { + this._headingGroup.addEventListener("click", this._goTo.bind(this)); + + store.subscribe( + (state) => state.selectedType, + this.toggleHighlight.bind(this) + ); + } + + /** + * + * @param {string} newSelection + * @param {string} oldSelection + */ + async toggleHighlight(newSelectedType, oldSelectedType) { + const affectsMe = + this._type == newSelectedType.typeName || + this._type == oldSelectedType.typeName; + + if (affectsMe) { + if ( + oldSelectedType.typeName === this._type && + oldSelectedType.typeName !== newSelectedType.typeName + ) { + this.unhighlightHeading(); + } else { + this.highlightHeading(); + } + } + } + + highlightHeading() { + if (this._headingGroup) { + this._headingGroup.setAttribute("selected", "true"); + } else { + console.warn("No nav heading found to higlight in settings nav."); + } + } + + unhighlightHeading() { + if (this._headingGroup) { + this._headingGroup.setAttribute("selected", "false"); + } + } + + _goTo() { + window.location.hash = this._type + "-All"; + } +} + +customElements.define( + "permission-settings-nav-link", + PermissionSettingsNavLink +); diff --git a/ui/src/js/permission-settings/components/table-view-actions.js b/ui/src/js/permission-settings/components/table-view-actions.js new file mode 100644 index 000000000..76fdf75a8 --- /dev/null +++ b/ui/src/js/permission-settings/components/table-view-actions.js @@ -0,0 +1,82 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class TableViewActions extends TatorElement { + constructor() { + super(); + + // Main Div wrapper + const template = document.getElementById("table-view-actions").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._tableActionsDiv = this._shadow.getElementById( + "table-view-actions--for-table" + ); + this._itemsActionsDiv = this._shadow.getElementById( + "table-view-actions--for-items" + ); + + this._viewByGroup = this._shadow.getElementById("view-by-group"); + this._viewByUser = this._shadow.getElementById("view-by-user"); + this._filter = this._shadow.getElementById("filter"); + this._calculator = this._shadow.getElementById("calculator"); + + this._newGroup = this._shadow.getElementById("new-group"); + this._delete = this._shadow.getElementById("delete"); + this._newPolicy = this._shadow.getElementById("new-policy"); + + this._buttons = [ + this._viewByGroup, + this._viewByUser, + this._filter, + this._calculator, + this._newGroup, + this._delete, + this._newPolicy, + ]; + + this._filterAppliedSignal = this._shadow.getElementById( + "filter-applied-signal" + ); + + this.modal = document.createElement("modal-dialog"); + this._shadow.appendChild(this.modal); + + this._filterWindow = document.createElement("table-view-filter"); + this._filterWindow.setAttribute("hidden", ""); + this._shadow.appendChild(this._filterWindow); + } + + connectedCallback() { + this._delete.setAttribute("disabled", ""); + + this._buttons.forEach((btn) => { + btn.style.display = "none"; + }); + this._buttonsForTable.forEach((btn) => { + btn.style.display = ""; + }); + this._buttonsForItems.forEach((btn) => { + btn.style.display = ""; + }); + + this._filterWindow.setAttribute("type", this.type); + this._filter.addEventListener("click", this._toggleFilterWindow.bind(this)); + + this._init(); + } + + _toggleFilterWindow() { + if (this._filterWindow.getAttribute("hidden") === null) { + this._filterWindow.setAttribute("hidden", ""); + } else { + this._filterWindow.removeAttribute("hidden"); + } + } + + _init() { + // Override by child class + } +} + +customElements.define("table-view-actions", TableViewActions); diff --git a/ui/src/js/permission-settings/components/table-view-filter.js b/ui/src/js/permission-settings/components/table-view-filter.js new file mode 100644 index 000000000..f61053027 --- /dev/null +++ b/ui/src/js/permission-settings/components/table-view-filter.js @@ -0,0 +1,83 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class TableViewFilter extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("table-view-filter").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._addConditionButton = this._shadow.getElementById( + "add-condition-button" + ); + this._conditionGroup = this._shadow.getElementById("condition-group"); + + this._search = this._shadow.getElementById("table-view-filter--search"); + this._cancel = this._shadow.getElementById("table-view-filter--cancel"); + } + + connectedCallback() { + this._search.addEventListener("click", this._getConditionValues.bind(this)); + this._cancel.addEventListener("click", this._toggleFilterWindow.bind(this)); + } + + static get observedAttributes() { + return ["type"]; + } + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "type": + this.type = newValue; + break; + } + } + + _toggleFilterWindow() { + if (this.getAttribute("hidden") === null) { + this.setAttribute("hidden", ""); + } else { + this.removeAttribute("hidden"); + } + } + + _getConditionValues() { + const values = Array.from(this._conditionGroup.children).map((condition) => + condition._processConditionValue() + ); + + if (values.findIndex((val) => !val) > -1) { + return; + } + + this.setAttribute("hidden", ""); + + if (this.type === "Group") { + const { groupSearchParams } = store.getState(); + + const newGroupSearchParams = { + Group: { + ...groupSearchParams.Group, + filter: values, + }, + User: { + ...groupSearchParams.User, + filter: values, + }, + }; + + store.getState().setGroupSearchParams(newGroupSearchParams); + } else if (this.type === "Policy") { + const { policySearchParams } = store.getState(); + + const newPolicySearchParams = { + ...policySearchParams, + filter: values, + }; + + store.getState().setPolicySearchParams(newPolicySearchParams); + } + } +} + +customElements.define("table-view-filter", TableViewFilter); diff --git a/ui/src/js/permission-settings/components/table-view-table.js b/ui/src/js/permission-settings/components/table-view-table.js new file mode 100644 index 000000000..2219e18d2 --- /dev/null +++ b/ui/src/js/permission-settings/components/table-view-table.js @@ -0,0 +1,43 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class TableViewTable extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("table-view-table").content; + this._shadow.appendChild(template.cloneNode(true)); + + // NO DATA + this._tableNoData = this._shadow.getElementById( + "permission-table--no-data" + ); + + // PAGE POSITION + this._pagePosition = this._shadow.getElementById( + "permission-table-page-position" + ); + this._totalItemCount = this._shadow.getElementById( + "page-position--total-item" + ); + this._currentPage = this._shadow.getElementById( + "page-position--current-page" + ); + this._totalPageCount = this._shadow.getElementById( + "page-position--total-page" + ); + + // TABLE + this._table = this._shadow.getElementById("permission-table"); + this._colgroup = this._shadow.getElementById("permission-table--colgroup"); + this._tableHead = this._shadow.getElementById("permission-table--head"); + this._tableBody = this._shadow.getElementById("permission-table--body"); + + // PAGINATION + this._paginatorDiv = this._shadow.getElementById( + "permission-table-paginator-div" + ); + } +} + +customElements.define("table-view-table", TableViewTable); diff --git a/ui/src/js/permission-settings/components/table-view.js b/ui/src/js/permission-settings/components/table-view.js new file mode 100644 index 000000000..df0032d5f --- /dev/null +++ b/ui/src/js/permission-settings/components/table-view.js @@ -0,0 +1,76 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { LoadingSpinner } from "../../components/loading-spinner.js"; +import { store } from "../store.js"; + +export class PermissionSettingsTableView extends TatorElement { + constructor() { + super(); + + // Main Div wrapper + const template = document.getElementById("table-view").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._actionsDiv = this._shadow.getElementById("table-view-actions"); + this._tableDiv = this._shadow.getElementById("table-view-table"); + + // // loading spinner + this.loading = new LoadingSpinner(); + this._shadow.appendChild(this.loading.getImg()); + + // this is outside the template and references by all parts of page to sync the dimmer + this.modal = document.createElement("modal-dialog"); + this._shadow.appendChild(this.modal); + } + + connectedCallback() { + // state.Policy may not be initialized, so show the spinner at first + this.showDimmer(); + this.loading.showSpinner(); + + this._type = this.getAttribute("type"); + + // Create actions component + this._actions = document.createElement( + `${this._type.toLowerCase()}-table-view-actions` + ); + this._actionsDiv.appendChild(this._actions); + + // Create table component + this._table = document.createElement( + `${this._type.toLowerCase()}-table-view-table` + ); + this._tableDiv.appendChild(this._table); + + // Once we know what type, listen to changes + store.subscribe((state) => state[this._type], this._newData.bind(this)); + } + + _newData(dataObj) { + if (!dataObj.init) return; + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + + // Only after knowing how many items are there can we init the paginator + this._table._initPaginator(); + } + + /** + * Modal for this page, and handler + * @returns sets page attribute that changes dimmer + */ + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } + + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } +} + +customElements.define( + "permission-settings-table-view", + PermissionSettingsTableView +); diff --git a/ui/src/js/permission-settings/permission-settings.js b/ui/src/js/permission-settings/permission-settings.js new file mode 100644 index 000000000..e7e9e09f7 --- /dev/null +++ b/ui/src/js/permission-settings/permission-settings.js @@ -0,0 +1,148 @@ +import { TatorPage } from "../components/tator-page.js"; +import { LoadingSpinner } from "../components/loading-spinner.js"; +import { store } from "./store.js"; +import { hasPermission } from "../util/has-permission.js"; + +export class PermissionSettings extends TatorPage { + constructor() { + super(); + + // // loading spinner + this.loading = new LoadingSpinner(); + this._shadow.appendChild(this.loading.getImg()); + + const template = document.getElementById("permission-settings").content; + this._shadow.appendChild(template.cloneNode(true)); + + this.main = this._shadow.getElementById("permission-settings--main"); + this.settingsNav = this._shadow.getElementById("settings-nav--nav"); + this.itemsContainer = this._shadow.getElementById( + "settings-nav--item-container" + ); + + this._groupTableView = this._shadow.getElementById("group-tabel-view"); + this._policyTableView = this._shadow.getElementById("policy-tabel-view"); + this._groupSingleView = this._shadow.getElementById("group-single-view"); + this._policyCalculatorView = this._shadow.getElementById( + "policy-calculator-view" + ); + this._policySingleView = this._shadow.getElementById("policy-single-view"); + this._views = { + GroupAll: this._groupTableView, + PolicyAll: this._policyTableView, + GroupSingle: this._groupSingleView, + PolicyCalculator: this._policyCalculatorView, + PolicySingle: this._policySingleView, + }; + + this.modal = this._shadow.getElementById("permission-settings--modal"); + this.modal.addEventListener("open", this.showDimmer.bind(this)); + this.modal.addEventListener("close", this.hideDimmer.bind(this)); + } + + connectedCallback() { + store.subscribe((state) => state.user, this._setUser.bind(this)); + + store.subscribe( + (state) => state.selectedType, + this._updateSelectedType.bind(this) + ); + + // Init + this._init(); + } + + async _init() { + await store.getState().initHeader(); + + this._getUserData(); + + // Figure out if something else needs to be shown + this.moveToCurrentHash(); + + // this handles back button, and some pushes to this to trigger selection change + window.addEventListener("hashchange", this.moveToCurrentHash.bind(this)); + } + + // Get current user, user's groups, user's organizations + async _getUserData() { + // Current User's Organization List + await store.getState().getOrganizationList(); + // Current User's Group List + await store.getState().getCurrentUserGroupList(); + // All Group data + store.getState().setGroupData(); + // Policy data that are associated to this user, this user's groups, this user's organizations + store.getState().setPolicyData(); + + console.log("😇 ~ _init ~ store.getState():", store.getState()); + } + + /** + * @param {string} val + */ + set selectedHash(val) { + if (val.split("-").length > 1) { + this._selectedHash = val; + const split = val.split("-"); + this._selectedType = split[0].replace("#", ""); + this._selectedObjectId = split[1]; + } else if (val === "") { + // No hash is home for permission-settings, which is group table + this._selectedHash = `#Group`; + this._selectedType = "Group"; + this._selectedObjectId = "All"; + } else { + // Error handle + this._selectedHash = null; + this._selectedType = null; + this._selectedObjectId = null; + } + + // console.log("DEBUG: Hash setup.... " + this._selectedHash); + store.getState().setSelectedType({ + typeName: this._selectedType, + typeId: this._selectedObjectId, + }); + } + + /* Sets the selection based on a hash change, or hash on load */ + moveToCurrentHash() { + this.selectedHash = window.location.hash; + } + + _updateSelectedType(newSelectedType, oldSelectedType) { + const oldKey = this._getViewKey(oldSelectedType); + const newKey = this._getViewKey(newSelectedType); + + this._views[oldKey].hidden = true; + this._views[newKey].hidden = false; + } + + _getViewKey(selectedType) { + let id = ""; + if (selectedType.typeId === "All") { + id = "All"; + } else if (selectedType.typeId.startsWith("Cal")) { + id = "Calculator"; + } else { + id = "Single"; + } + + return `${selectedType.typeName}${id}`; + } + + /** + * Modal for this page, and handler + * @returns sets page attribute that changes dimmer + */ + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } + + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } +} + +customElements.define("permission-settings", PermissionSettings); diff --git a/ui/src/js/permission-settings/single-view-components/group-settings-card.js b/ui/src/js/permission-settings/single-view-components/group-settings-card.js new file mode 100644 index 000000000..d93de57f0 --- /dev/null +++ b/ui/src/js/permission-settings/single-view-components/group-settings-card.js @@ -0,0 +1,102 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class GroupMemberCard extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("group-member-card").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._div = this._shadow.getElementById("group-member-card-div"); + this._username = this._shadow.getElementById("group-member-card--username"); + this._email = this._shadow.getElementById("group-member-card--email"); + } + + connectedCallback() {} + + static get observedAttributes() { + return ["username", "email", "type"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "username": + this._username.innerText = newValue; + break; + case "email": + this._email.innerText = newValue; + break; + case "type": + if (newValue === "to-be-added") { + this._div.classList.remove("to-be-deleted"); + this._div.classList.add("to-be-added"); + } else if (newValue === "to-be-deleted") { + this._div.classList.remove("to-be-added"); + this._div.classList.add("to-be-deleted"); + } + } + } + + /** + * @param {object} val + */ + set data(val) { + this._data = val; + } +} +export class UserGroupCard extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("user-group-card").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._div = this._shadow.getElementById("user-group-card-div"); + this._id = this._shadow.getElementById("user-group-card--id"); + this._name = this._shadow.getElementById("user-group-card--name"); + + this._remove = this._shadow.getElementById("remove-group-button"); + } + + connectedCallback() { + this._remove.addEventListener("click", this._removeGroup.bind(this)); + } + + static get observedAttributes() { + return ["id", "name", "type"]; + } + + attributeChangedCallback(name, oldValue, newValue) { + switch (name) { + case "id": + this._id.innerText = newValue; + break; + case "name": + this._name.innerText = newValue; + break; + case "type": + if (newValue === "to-be-added") { + this._div.classList.remove("to-be-deleted"); + this._div.classList.add("to-be-added"); + } else if (newValue === "to-be-deleted") { + this._div.classList.remove("to-be-added"); + this._div.classList.add("to-be-deleted"); + } + } + } + + _removeGroup() { + this.dispatchEvent(new CustomEvent("remove")); + } + + /** + * @param {object} val + */ + set data(val) { + this._data = val; + } +} + +customElements.define("group-member-card", GroupMemberCard); +customElements.define("user-group-card", UserGroupCard); diff --git a/ui/src/js/permission-settings/single-view-components/group-single-view.js b/ui/src/js/permission-settings/single-view-components/group-single-view.js new file mode 100644 index 000000000..ee28daf3b --- /dev/null +++ b/ui/src/js/permission-settings/single-view-components/group-single-view.js @@ -0,0 +1,660 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { LoadingSpinner } from "../../components/loading-spinner.js"; +import { store } from "../store.js"; + +export class GroupSingleView extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("group-single-view").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._title = this._shadow.getElementById("title"); + + this._noData = this._shadow.getElementById("no-data"); + this._form = this._shadow.getElementById("group-form"); + this._formInputForGroup = this._shadow.getElementById( + "form-input-for-group" + ); + + this._orgIdInput = this._shadow.getElementById("organization-id-input"); + this._groupNameInput = this._shadow.getElementById("group-name"); + + this._itemCount = this._shadow.getElementById("item-count"); + this._cardListDiv = this._shadow.getElementById("card-list"); + this._groupInputWarning = this._shadow.getElementById( + "group-input-warning" + ); + + this._usernameInput = this._shadow.getElementById("username-list-input"); + this._usernameInput._input.setAttribute( + "placeholder", + "Ensure that each username or email is on a separate line." + ); + this._usernameInputWarning = this._shadow.getElementById( + "username-list-warning" + ); + this._usernameAdd = this._shadow.getElementById("username-list-add"); + this._usernameDelete = this._shadow.getElementById("username-list-delete"); + + this._saveCancel = this._shadow.getElementById( + "group-single-view--save-cancel-section" + ); + this._save = this._shadow.getElementById("group-single-view-save"); + + // // loading spinner + this.loading = new LoadingSpinner(); + this._shadow.appendChild(this.loading.getImg()); + + this.modal = document.createElement("modal-dialog"); + this._shadow.appendChild(this.modal); + } + + connectedCallback() { + this._initUserInput(); + + this._usernameInput.addEventListener("change", () => { + const value = this._usernameInput.getValue(); + this._usernameInputString = value; + }); + + this._usernameAdd.addEventListener("click", this._addMembers.bind(this)); + this._usernameDelete.addEventListener( + "click", + this._deleteMembers.bind(this) + ); + + this._save.addEventListener("click", this._saveForm.bind(this)); + + store.subscribe( + (state) => state.selectedType, + this._updateSelectedType.bind(this) + ); + + store.subscribe((state) => state.Group, this._setData.bind(this)); + + store.subscribe( + (state) => state.status, + this.handleStatusChange.bind(this) + ); + } + + /** + * @param {string} val + */ + set id(val) { + this._id = val; + + // New group + if (val === "New") { + this._title.innerText = "New Group"; + this._orgIdInput.removeAttribute("hidden"); + this._groupNameInput.removeAttribute("hidden"); + this._formInputForGroup.style.display = ""; + this._usernameDelete.style.display = "none"; + } + // Edit group + else if (!val.includes("user")) { + this.showDimmer(); + this.loading.showSpinner(); + + this._title.innerText = "Edit Group"; + this._orgIdInput.setAttribute("hidden", ""); + this._groupNameInput.removeAttribute("hidden"); + this._formInputForGroup.style.display = ""; + this._usernameDelete.style.display = ""; + } + // Edit user's groups + else if (val.includes("user")) { + this.showDimmer(); + this.loading.showSpinner(); + + this._title.innerText = "Edit Groups of User"; + this._orgIdInput.setAttribute("hidden", ""); + this._groupNameInput.setAttribute("hidden", ""); + this._formInputForGroup.style.display = "none"; + } + + this._cardListDiv.innerHTML = ""; + this._setData(); + } + + _setData() { + if (!this._show) return; + + // New group + if (this._id === "New") { + const { organizationList } = store.getState(); + const orgIdInputChoices = organizationList.map((org, index) => { + const choice = { + label: `ID: ${org.id} - ${org.name}`, + value: org.id, + }; + if (index === 0) { + choice.selected = true; + } + return choice; + }); + this._orgIdInput.choices = orgIdInputChoices; + this._initByData("group", {}); + } + // Edit group + else if (!this._id.includes("user")) { + const { Group } = store.getState(); + if (!Group.init) { + return; + } else { + this._initByData("group", Group.map.get(+this._id)); + } + } + // Edit user's groups + else if (this._id.includes("user")) { + const { Group } = store.getState(); + if (!Group.init) { + return; + } else { + const userId = +this._id.replace("user", ""); + const groupIds = Group.userIdGroupIdMap.get(userId); + this._initByData("user", { id: userId, groups: groupIds }); + } + } + } + + /** + * @param {string} type -- define the page is for user's groups or group's users + * @param {object} val + */ + _initByData(type, val) { + if (type === "group") { + if (val) { + this._noData.setAttribute("hidden", ""); + this._form.removeAttribute("hidden"); + this._saveCancel.style.display = ""; + this._data = val; + + // This _userData is just used for storing users' data, to reduce number of rest calls + this._userData = new Map(); + + // New group + if (Object.keys(val).length === 0) { + this._groupNameInput.setValue(""); + this._itemCount.innerText = "0 Members"; + } + // Has group data (members count can be zero) + else { + this._groupNameInput.setValue(val.name); + this._itemCount.innerText = `${val.members.length} Member${ + val.members.length === 1 ? "" : "s" + }`; + } + + this._userToBeAdded = new Map(); + this._userToBeDeleted = new Map(); + this._renderMemberCards(); + } + // group id is invalid or no group data + else { + this._noData.removeAttribute("hidden"); + this._form.setAttribute("hidden", ""); + this._saveCancel.style.display = "none"; + this._noData.innerText = `There is no member data for Group ${this._id}.`; + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + } else if (type === "user") { + if (val.id) { + this._initAddGroupInput(); + + this._noData.setAttribute("hidden", ""); + this._form.removeAttribute("hidden"); + this._saveCancel.style.display = ""; + this._data = val; + + if (val.groups) { + this._itemCount.innerText = `${val.groups.length} Group${ + val.groups.length === 1 ? "" : "s" + }`; + } else { + this._data.groups = []; + this._itemCount.innerText = `0 Groups`; + } + + this._groupToBeAdded = new Map(); + this._groupToBeDeleted = new Map(); + this._renderGroupCards(); + } + // user id is invalid or no user data + else { + this._noData.removeAttribute("hidden"); + this._form.setAttribute("hidden", ""); + this._saveCancel.style.display = "none"; + this._noData.innerText = `There is no group data for User ${this._id.replace( + "user", + "" + )}.`; + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + } + } + + _renderGroupCards() { + const cards = []; + + const { map } = store.getState().Group; + + if (this._data.groups && this._data.groups.length) { + for (const groupId of this._data.groups) { + const groupData = map.get(groupId); + if (groupData) { + const card = document.createElement("user-group-card"); + card.setAttribute("id", groupData.id); + card.setAttribute("name", groupData.name); + if (this._groupToBeDeleted.has(groupId)) { + card.setAttribute("type", "to-be-deleted"); + } + card.addEventListener("remove", () => { + this._processToBeDeletedGroup(groupId, groupData); + }); + cards.push(card); + } + } + } + for (let [groupId, groupData] of this._groupToBeAdded) { + if ( + this._data?.groups && + this._data?.groups?.length && + this._data.groups.includes(groupId) + ) { + continue; + } + const card = document.createElement("user-group-card"); + card.setAttribute("id", groupData.id); + card.setAttribute("name", groupData.name); + card.setAttribute("type", "to-be-added"); + card.addEventListener("remove", () => { + this._processToBeDeletedGroup(groupId, groupData); + }); + cards.push(card); + } + // Input card + cards.push(this._addGroupInput); + + this._cardListDiv.replaceChildren(...cards); + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + + async _renderMemberCards() { + const cards = []; + + if (this._data?.members && this._data?.members?.length) { + for (const memberId of this._data.members) { + if (!this._userData.has(memberId)) { + const data = await store.getState().findUserById(memberId); + this._userData.set(memberId, data); + } + const userData = this._userData.get(memberId); + if (userData) { + const card = document.createElement("group-member-card"); + card.setAttribute("username", userData.username); + card.setAttribute("email", userData.email); + card.setAttribute("data-id", userData.id); + if (this._userToBeDeleted.has(memberId)) { + card.setAttribute("type", "to-be-deleted"); + } + cards.push(card); + } + } + } + for (let [userId, userData] of this._userToBeAdded) { + if ( + this._data?.members && + this._data?.members?.length && + this._data.members.includes(userId) + ) { + continue; + } + const card = document.createElement("group-member-card"); + card.setAttribute("username", userData.username); + card.setAttribute("email", userData.email); + card.setAttribute("data-id", userData.id); + card.setAttribute("type", "to-be-added"); + cards.push(card); + } + + this._cardListDiv.replaceChildren(...cards); + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + + _updateSelectedType(newSelectedType, oldSelectedType) { + if ( + newSelectedType.typeName !== "Group" || + newSelectedType.typeId === "All" + ) { + this._show = false; + return; + } + + this._show = true; + this.id = newSelectedType.typeId; + } + + _initUserInput() { + this._userToBeAdded = new Map(); + this._userToBeDeleted = new Map(); + } + + async _addMembers(evt) { + console.log("😇 ~ _addMembers ~ evt:", evt); + + evt.preventDefault(); + const trimmedInput = this._usernameInputString.trim(); + if (!trimmedInput) { + return; + } + const inputList = trimmedInput.split("\n"); + const result = await store.getState().findUsers(inputList); + console.log("😇 ~ _addMembers ~ result:", result); + this._processToBeAddedUsers(result); + } + + _processToBeAddedUsers({ found, notFound }) { + if (notFound.length) { + const warningStr = `Cannot find user information for these: ${notFound.join( + ", " + )}.`; + this._usernameInputShowWarning(warningStr); + } + + if (found.size) { + for (let [userId, user] of found) { + if (this._userToBeDeleted.has(userId)) { + this._userToBeDeleted.delete(userId); + } + // If not an original member, then add to _userToBeAdded + if (!this._data.members || !this._data.members.includes(userId)) { + this._userToBeAdded.set(userId, user); + } + } + this._renderMemberCards(); + } + } + + _deleteMembers(evt) { + evt.preventDefault(); + const trimmedInput = this._usernameInputString.trim(); + if (!trimmedInput) { + return; + } + const inputList = trimmedInput.split("\n"); + + const notFound = []; + // Find elements directly + inputList.forEach((input) => { + const el = this._cardListDiv.querySelector( + `[${input.indexOf("@") > -1 ? "email" : "username"}="${input}"]` + ); + if (el) { + const type = el.getAttribute("type"); + const id = +el.dataset.id; + if (type === "to-be-added") { + this._userToBeAdded.delete(id); + } else { + const userData = this._userData.get(id); + this._userToBeDeleted.set(id, userData); + } + } else { + notFound.push(input); + } + }); + + if (notFound.length) { + const warningStr = `Cannot find user information for these: ${notFound.join( + ", " + )}.`; + this._usernameInputShowWarning(warningStr); + } + + this._renderMemberCards(); + } + + async _saveForm(evt) { + evt.preventDefault(); + + // New group + if (this._id === "New") { + const newGroupName = this._groupNameInput.getValue(); + const orgId = this._orgIdInput.getValue(); + if (!orgId) { + this.modal._error( + "Please specify an organization that the new group is in." + ); + return; + } + if (!newGroupName) { + this.modal._error("Please specify a group name."); + return; + } + if (!this._userToBeAdded.size) { + this.modal._error("There is no member added."); + return; + } + + const info = await store.getState().createGroup(orgId, { + name: newGroupName, + initial_members: Array.from(this._userToBeAdded.keys()), + }); + + this.handleResponse(info); + } + // Edit group + else if (!this._id.includes("user")) { + const newGroupName = this._groupNameInput.getValue(); + if ( + newGroupName === this._data.name && + !this._userToBeAdded.size && + !this._userToBeDeleted.size + ) { + this.modal._success("Nothing new to save!"); + return; + } + + const info = await store.getState().updateGroup(this._data.id, { + name: newGroupName, + add_members: Array.from(this._userToBeAdded.keys()), + remove_members: Array.from(this._userToBeDeleted.keys()), + }); + + this.handleResponse(info); + } + // Edit user's groups + else if (this._id.includes("user")) { + if (!this._groupToBeAdded.size && !this._groupToBeDeleted.size) { + this.modal._success("Nothing new to save!"); + return; + } + + const responses = []; + for (const [groupId, group] of this._groupToBeAdded) { + const info = await store.getState().updateGroup(groupId, { + add_members: [this._data.id], + }); + responses.push(info); + } + for (const [groupId, group] of this._groupToBeDeleted) { + const info = await store.getState().updateGroup(groupId, { + remove_members: [this._data.id], + }); + responses.push(info); + } + + this.handleResponseList(responses); + } + } + + handleResponse(info) { + let message = info.data?.message ? info.data.message : ""; + if (info.response?.ok) { + store.getState().setGroupData(); + return this.modal._success(message); + } else { + if (info.response?.status && info.response?.statusText) { + return this.modal._error( + `${info.response.status} ${info.response.statusText}

${message}` + ); + } else { + return this.modal._error(`Error: Could not process request.`); + } + } + } + + handleResponseList(responses) { + if (responses && Array.isArray(responses)) { + let sCount = 0; + let eCount = 0; + let errors = ""; + + for (let object of responses) { + if (object.response?.ok) { + sCount++; + } else { + eCount++; + const message = object?.data?.message || ""; + errors += `

${message}`; + } + } + + if (sCount > 0 && eCount === 0) { + this.modal._success( + `Successfully updated association with ${sCount} group${ + sCount == 1 ? "" : "s" + }.` + ); + store.getState().setGroupData(); + } else if (sCount > 0 && eCount > 0) { + this.modal._complete( + `Successfully updated association with ${sCount} group${ + sCount == 1 ? "" : "s" + }.

+ Error updating association with ${eCount} group${ + eCount == 1 ? "" : "s" + }.

+ Error message${eCount == 1 ? "" : "s"}:

${errors}` + ); + store.getState().setGroupData(); + } else { + return this.modal._error( + `Error deleting ${eCount} group${eCount == 1 ? "" : "s"}.

+ Error message${eCount == 1 ? "" : "s"}:

${errors}` + ); + } + } + } + + handleStatusChange(status) { + if (status.name == "pending") { + this.showDimmer(); + this.loading.showSpinner(); + } else { + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + } + + _usernameInputShowWarning(str) { + this._usernameInputWarning.innerText = str; + this._usernameInputWarning.classList.remove("hidden"); + setTimeout(() => { + this._usernameInputWarning.classList.add("hidden"); + }, 4000); + } + + _groupInputShowWarning(str) { + this._groupInputWarning.innerText = str; + this._groupInputWarning.classList.remove("hidden"); + setTimeout(() => { + this._groupInputWarning.classList.add("hidden"); + }, 4000); + } + + _initAddGroupInput() { + this._addGroupInput = document.createElement("input"); + this._addGroupInput.setAttribute("class", "form-control"); + this._addGroupInput.setAttribute("type", "number"); + this._addGroupInput.style.height = "100%"; + this._addGroupInput.setAttribute("placeholder", "Hit Enter to add an ID"); + this._addGroupInput.value = null; + + this._addGroupInput.addEventListener("keydown", this._addGroup.bind(this)); + } + + _addGroup(evt) { + if (evt.key === "Enter") { + evt.preventDefault(); + const id = +this._addGroupInput.value; + const { map } = store.getState().Group; + const group = map.get(id); + if (!group) { + this._groupInputShowWarning(`Can't find group by ID ${id}.`); + } else { + this._processToBeAddedGroup(id, group); + this._addGroupInput.value = null; + this._addGroupInput.focus(); + } + } + } + + _processToBeAddedGroup(groupId, group) { + if (this._groupToBeDeleted.has(groupId)) { + this._groupToBeDeleted.delete(groupId); + } + // If not an original group, then add to _groupToBeAdded + if (!this._data.groups.includes(groupId)) { + this._groupToBeAdded.set(groupId, group); + } + + this._renderGroupCards(); + } + + _processToBeDeletedGroup(groupId, group) { + if (this._groupToBeAdded.has(groupId)) { + this._groupToBeAdded.delete(groupId); + } + // If is an original group, then add to _groupToBeDeleted + if (this._data.groups.includes(groupId)) { + this._groupToBeDeleted.set(groupId, group); + } + + this._renderGroupCards(); + } + + /** + * Modal for this page, and handler + * @returns sets page attribute that changes dimmer + */ + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } + + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } +} + +customElements.define("group-single-view", GroupSingleView); diff --git a/ui/src/js/permission-settings/single-view-components/policy-calculator-view.js b/ui/src/js/permission-settings/single-view-components/policy-calculator-view.js new file mode 100644 index 000000000..c01d79465 --- /dev/null +++ b/ui/src/js/permission-settings/single-view-components/policy-calculator-view.js @@ -0,0 +1,966 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { + POLICY_ENTITY_NAME, + POLICY_TARGET_NAME, + store, + fetchWithHttpInfo, +} from "../store.js"; +import { LoadingSpinner } from "../../components/loading-spinner.js"; + +const CALCULATOR_COLUMN = [ + "Policy Entity", + "Policy Target", + "Exist", + "Read", + "Create", + "Modify", + "Delete", + "Execute", + "Upload", + "ACL", + "Actions", +]; +const CALCULATOR_COLGROUP = ` + + + + + + + + + + + +`; + +const ENTITY_TYPE_CHOICES = [ + { label: "User", value: "user" }, + { label: "Group", value: "group" }, + { label: "Organization", value: "organization" }, +]; +const TARGET_TYPE_CHOICES = [ + { label: "Project", value: "project" }, + // { label: "Media", value: "media" }, + // { label: "File", value: "file" }, + { label: "Section", value: "section" }, + { label: "Algorithm", value: "algorithm" }, + { label: "Version", value: "version" }, + { label: "Organization", value: "target_organization" }, + { label: "Group", value: "target_group" }, + // { label: "Bucket", value: "bucket" }, + // { label: "Hosted Template", value: "hosted_template" }, +]; + +export class PolicyCalculatorView extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("policy-calculator-view").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._noData = this._shadow.getElementById("no-data"); + this._noPermission = this._shadow.getElementById("no-permission"); + + this._entityTypeInput = this._shadow.getElementById("entity-type-input"); + this._entityIdInput = this._shadow.getElementById("entity-id-input"); + this._targetTypeInput = this._shadow.getElementById("target-type-input"); + this._targetIdInput = this._shadow.getElementById("target-id-input"); + this._inputs = [ + this._entityTypeInput, + this._entityIdInput, + this._targetTypeInput, + this._targetIdInput, + ]; + this._inputsDiv = this._shadow.getElementById("inputs-div"); + + this._table = this._shadow.getElementById("calculator-table"); + this._colgroup = this._shadow.getElementById("calculator-table--colgroup"); + this._tableHead = this._shadow.getElementById("calculator-table--head"); + this._tableBody = this._shadow.getElementById("calculator-table--body"); + + this._form = this._shadow.getElementById("calculator-table-form"); + this._saveReset = this._shadow.getElementById( + "calculator-table--save-reset-section" + ); + this._saveButton = this._shadow.getElementById("calculator-table-save"); + this._resetButton = this._shadow.getElementById("calculator-table-reset"); + + // // loading spinner + this.loading = new LoadingSpinner(); + this._shadow.appendChild(this.loading.getImg()); + + this.modal = document.createElement("modal-dialog"); + this._shadow.appendChild(this.modal); + } + + connectedCallback() { + this._inputs.forEach((input) => { + input.addEventListener("change", this._inputChange.bind(this)); + }); + + store.subscribe( + (state) => state.selectedType, + this._updateSelectedType.bind(this) + ); + + store.subscribe((state) => state.Policy, this._setData.bind(this)); + + this._saveButton.addEventListener("click", this._saveForm.bind(this)); + this._resetButton.addEventListener("click", this._resetTable.bind(this)); + } + + _updateSelectedType(newSelectedType, oldSelectedType) { + if ( + newSelectedType.typeName !== "Policy" || + newSelectedType.typeId === "All" || + !newSelectedType.typeId.startsWith("Cal") + ) { + this._show = false; + return; + } + + this.showDimmer(); + this.loading.showSpinner(); + + this._show = true; + this.id = newSelectedType.typeId; + } + + /** + * @param {string} val + */ + set id(val) { + this._id = val; + + // Calculator (with no entity and target info preset) + if (this._id === "Cal") { + // this._initInputs(true); + } + // Calculator (with entity and target info preset) + else { + // this._initInputs(false); + } + this._noPermission.setAttribute("hidden", ""); + + this._setData(); + } + + _setData() { + if (!this._show) return; + const { Policy } = store.getState(); + if (!Policy.init) return; + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + + // Calculator (with no entity and target info preset) + if (this._id === "Cal") { + this._initInputs(true); + this._initByData(); + } + // Calculator (with entity and target info preset) + else { + this._initInputs(false); + this._initByData(Policy.processedMap.get(+this._id.replace("Cal", ""))); + } + } + + /** + * @param {object} val + */ + _initByData(val) { + console.log("😇 ~ _initByData ~ val:", val, this._id); + + this._noData.setAttribute("hidden", ""); + this._noPermission.setAttribute("hidden", ""); + this._saveReset.style.display = "none"; + this._tableHead.innerHTML = ""; + this._tableBody.innerHTML = ""; + + if (val) { + this.showDimmer(); + this.loading.showSpinner(); + + this._entityTypeInput.setValue(val.entityType); + this._entityIdInput.setValue(val.entityId); + this._targetTypeInput.setValue(val.targetType); + this._targetIdInput.setValue(val.targetId); + + this._requestedEntityName = `${ + POLICY_ENTITY_NAME[this._entityTypeInput.getValue()] + } ${this._entityIdInput.getValue()}`; + this._requestedTargetName = `${ + POLICY_TARGET_NAME[this._targetTypeInput.getValue()] + } ${this._targetIdInput.getValue()}`; + + this._getDataByInputs(); + } else { + // Calculator (with no entity and target info preset) + if (this._id === "Cal") { + this._entityIdInput.setValue(null); + this._targetIdInput.setValue(null); + } + // Calculator (with entity and target info preset) + else { + this._noData.removeAttribute("hidden"); + this._noData.innerText = `There is no data for Policy ${this._id.replace( + "Cal", + "" + )}.`; + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + } + } + + async _getDataByInputs() { + // Fetch Policy + try { + await this._getCalculatorTargets(); + } catch (error) { + console.error(error); + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + this._noData.removeAttribute("hidden"); + this._noData.innerText = error; + return; + } + + this._getCalculatorEntities(); + const calculatorPolicies = await store + .getState() + .getPoliciesByTargets(this.targets); + this._getTableBodyData(calculatorPolicies); + + this._renderCalculatorTable(); + + this._checkPermissionOnOperatePolicy(); + } + + _renderCalculatorTable() { + // Head + this._renderCalculatorTableHead(); + + // Body -- Create Row + Array.from(this._singleRowData.entries()).forEach( + ([targetName, policies]) => { + Array.from(policies.keys()).forEach((entityName) => { + // single policy row + const tr = document.createElement("tr"); + tr.id = `${targetName}--${entityName}`; + this._tableBody.appendChild(tr); + }); + + // OR'd row + const tr = document.createElement("tr"); + tr.id = `${targetName}--ord`; + this._tableBody.appendChild(tr); + } + ); + //// final effective permission Row + const tr = document.createElement("tr"); + tr.id = `final-row`; + this._tableBody.appendChild(tr); + + // Body -- Fill in data + Array.from(this._singleRowData.entries()).forEach( + ([targetName, policies]) => { + Array.from(policies.keys()).forEach((entityName) => { + this._renderCalculatorTableBodySingleRow(targetName, entityName); + }); + this._renderCalculatorTableBodyOrdRow(targetName); + } + ); + this._renderCalculatorTableBodyFinalRow(); + + this._saveReset.style.display = ""; + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + + _renderCalculatorTableBodySingleRow(targetName, entityName) { + const id = `${targetName}--${entityName}`; + const tr = document.createElement("tr"); + tr.id = id; + + const tdEntity = document.createElement("td"); + tdEntity.innerText = entityName; + tr.appendChild(tdEntity); + + const isFirstRow = + Array.from(this._singleRowData.get(targetName).keys()).indexOf( + entityName + ) === 0; + if (isFirstRow) { + const entityCount = this._singleRowData.get(targetName).size; + const tdTarget = document.createElement("td"); + tdTarget.innerText = targetName; + tdTarget.setAttribute("rowspan", entityCount); + tr.appendChild(tdTarget); + } + + const { permissionBits } = this._singleRowData + .get(targetName) + .get(entityName); + const noACL = permissionBits === "0-------"; + // .split("").reverse(): reverse the string, bc the rightmost is "Exist", the left most is "ACL". But in the table it is the opposite + permissionBits + .split("") + .reverse() + .forEach((char, index) => { + const td = document.createElement("td"); + if (char === "0") { + if (!noACL) { + const xmark = document.createElement("no-permission-button"); + xmark.setAttribute("data-id", `${id}--${index}`); + xmark.addEventListener("click", this._changeRowCell.bind(this)); + td.appendChild(xmark); + } + } else if (char === "1") { + const check = document.createElement("has-permission-button"); + check.setAttribute("data-id", `${id}--${index}`); + check.addEventListener("click", this._changeRowCell.bind(this)); + td.appendChild(check); + } + tr.appendChild(td); + }); + + const tdActions = document.createElement("td"); + const div = document.createElement("div"); + div.classList.add("d-flex", "flex-row", "flex-justify-center"); + div.style.gap = "5px"; + tdActions.appendChild(div); + tr.appendChild(tdActions); + + const back = document.createElement("change-back-button"); + back.setAttribute("data-id", `${targetName}--${entityName}`); + back.addEventListener("click", this._changeRowBack.bind(this)); + div.appendChild(back); + if (permissionBits === "--------") { + const grant = document.createElement("grant-all-button"); + grant.setAttribute("data-id", `${targetName}--${entityName}`); + grant.addEventListener("click", this._grantRow.bind(this)); + div.appendChild(grant); + } else { + const remove = document.createElement("remove-permission-button"); + remove.setAttribute("data-id", `${targetName}--${entityName}`); + remove.addEventListener("click", this._removeRow.bind(this)); + div.appendChild(remove); + } + // if (noACL) { + // for (const child of div.children) { + // child.setAttribute("disabled", ""); + // } + // } + + const trOld = this._shadow.getElementById(id); + this._tableBody.replaceChild(tr, trOld); + } + + _renderCalculatorTableBodyOrdRow(targetName) { + const id = `${targetName}--ord`; + const tr = document.createElement("tr"); + tr.id = id; + tr.classList.add("ord-row"); + + const td = document.createElement("td"); + td.innerText = `${this._requestedEntityName}'s effective permission against ${this._requestedTargetName} (via policies set against ${targetName}).`; + td.setAttribute("colspan", 2); + tr.appendChild(td); + + const ordPermission = this._ordRowData.get(targetName); + + // .split("").reverse(): reverse the string, bc the rightmost is "Exist", the left most is "ACL". But in the table it is the opposite + ordPermission + .split("") + .reverse() + .forEach((char) => { + const td = document.createElement("td"); + if (char === "0") { + const xmark = document.createElement("no-permission-button"); + xmark.setAttribute("disabled", ""); + td.appendChild(xmark); + } else if (char === "1") { + const check = document.createElement("has-permission-button"); + check.setAttribute("disabled", ""); + td.appendChild(check); + } else if (char === "-") { + const question = document.createElement("question-mark-button"); + question.setAttribute("disabled", ""); + td.appendChild(question); + } + tr.appendChild(td); + }); + + const tdActions = document.createElement("td"); + tr.appendChild(tdActions); + + const trOld = this._shadow.getElementById(id); + this._tableBody.replaceChild(tr, trOld); + } + + _renderCalculatorTableBodyFinalRow() { + const id = `final-row`; + const tr = document.createElement("tr"); + tr.id = id; + tr.classList.add("final-row"); + + const td = document.createElement("td"); + td.innerText = `${this._requestedEntityName}'s final effective permission against ${this._requestedTargetName}`; + td.setAttribute("colspan", 2); + tr.appendChild(td); + + // .split("").reverse(): reverse the string, bc the rightmost is "Exist", the left most is "ACL". But in the table it is the opposite + this._finalPermission + .split("") + .reverse() + .forEach((char) => { + const td = document.createElement("td"); + if (char === "0") { + const xmark = document.createElement("no-permission-button"); + xmark.setAttribute("disabled", ""); + td.appendChild(xmark); + } else if (char === "1") { + const check = document.createElement("has-permission-button"); + check.setAttribute("disabled", ""); + td.appendChild(check); + } else if (char === "-") { + const question = document.createElement("question-mark-button"); + question.setAttribute("disabled", ""); + td.appendChild(question); + } + tr.appendChild(td); + }); + + const tdActions = document.createElement("td"); + tr.appendChild(tdActions); + + const trOld = this._shadow.getElementById(id); + this._tableBody.replaceChild(tr, trOld); + } + + _renderCalculatorTableHead() { + this._colgroup.innerHTML = CALCULATOR_COLGROUP; + + // Head row 1 + const tr1 = document.createElement("tr"); + + const th0 = document.createElement("th"); + th0.innerText = CALCULATOR_COLUMN[0]; + th0.setAttribute("rowspan", 2); + tr1.appendChild(th0); + + const th1 = document.createElement("th"); + th1.innerText = CALCULATOR_COLUMN[1]; + th1.setAttribute("rowspan", 2); + tr1.appendChild(th1); + + const ths = document.createElement("th"); + ths.innerText = `Each policy's permission on ${this._requestedTargetName} (with corresponding bit shift)`; + ths.setAttribute("colspan", 8); + tr1.appendChild(ths); + + const thLast = document.createElement("th"); + thLast.innerText = CALCULATOR_COLUMN.at(-1); + thLast.setAttribute("rowspan", 2); + tr1.appendChild(thLast); + + this._tableHead.appendChild(tr1); + + // Head row 2 + const tr2 = document.createElement("tr"); + CALCULATOR_COLUMN.slice(2, CALCULATOR_COLUMN.length - 1) + .map((val) => { + const th = document.createElement("th"); + th.innerText = val; + return th; + }) + .forEach((th) => { + tr2.appendChild(th); + }); + this._tableHead.appendChild(tr2); + } + + _getTableBodyData(policies) { + console.log("😇 ~ _getTableBodyData ~ policies:", policies); + + // Clear values + this._singleRowData = new Map(); + this._ordRowData = new Map(); + + this._setSinglePolicyRowData(policies); + [...this._singleRowData.keys()].forEach((targetName) => + this._setOrdRowData(targetName) + ); + this._setFinalRowData(); + + console.log( + "😇 ~ _getTableBodyData ~ this._singleRowData :", + this._singleRowData + ); + } + + _setSinglePolicyRowData(policies) { + this.targets.forEach((target) => { + const targetName = `${POLICY_TARGET_NAME[target[0]]} ${target[1]}`; + this._singleRowData.set(targetName, new Map()); + + // If current user has no ACL permission on this target + if ( + policies.findIndex( + (policy) => + policy.entityName === "ALL" && policy.targetName === targetName + ) > -1 + ) { + this.entities.forEach((entity) => { + const entityName = `${POLICY_ENTITY_NAME[entity[0]]} ${entity[1]}`; + + let binaryShiftedPermission = "0-------"; + const obj = { + policyId: null, + permissionBits: binaryShiftedPermission, + originalPermissionBits: binaryShiftedPermission, + entityType: entity[0], + entityId: entity[1], + targetType: target[0], + targetId: target[1], + permission: -1, + }; + + this._singleRowData.get(targetName).set(entityName, obj); + }); + } else { + this.entities.forEach((entity) => { + const entityName = `${POLICY_ENTITY_NAME[entity[0]]} ${entity[1]}`; + + let binaryShiftedPermission = "--------"; + let policyId = null; + let permission = null; + const policy = policies.find( + (policy) => + policy.entityName === entityName && + policy.targetName === targetName + ); + if (policy) { + policyId = policy.id; + permission = policy.permission; + const shiftedPermission = + BigInt(policy.permission) >> BigInt(target[2]); // In JavaScript, the >> (bitwise right shift) operator only works on 32-bit signed integers. Therefore, need help of BigInt() + binaryShiftedPermission = this._getRightmost8Bits( + shiftedPermission.toString(2) + ); + } + const obj = { + policyId, + permissionBits: binaryShiftedPermission, + originalPermissionBits: binaryShiftedPermission, + entityType: entity[0], + entityId: entity[1], + targetType: target[0], + targetId: target[1], + permission, + }; + + this._singleRowData.get(targetName).set(entityName, obj); + }); + } + }); + } + _setOrdRowData(targetName) { + let ordPermission = ""; + const permissionStrings = Array.from( + this._singleRowData.get(targetName).values() + ).map((obj) => obj.permissionBits); + + if (permissionStrings[0] === "0-------") { + ordPermission = "0-------"; + } else { + ordPermission = this._bitwiseOrBinaryStrings(permissionStrings); + } + + this._ordRowData.set(targetName, ordPermission); + } + _setFinalRowData() { + this._finalPermission = Array.from(this._ordRowData.values()).at(-1); + } + + _changeRowCell(evt) { + const id = evt.target.dataset.id; + if (id) { + const val = id.split("--"); + if (val.length === 3) { + const { permissionBits } = this._singleRowData.get(val[0]).get(val[1]); + // 7-index: bc in a binary permission string, the rightmost is "Exist", the left most is "ACL". But in the table it is the opposite + const index = 7 - parseInt(val[2]); + const bit = permissionBits[index]; + + const newPermissionBits = + permissionBits.slice(0, index) + + (bit === "1" ? "0" : "1") + + permissionBits.slice(index + 1); + const oldObj = this._singleRowData.get(val[0]).get(val[1]); + + this._singleRowData + .get(val[0]) + .set(val[1], { ...oldObj, permissionBits: newPermissionBits }); + this._setOrdRowData(val[0]); + this._setFinalRowData(); + + this._renderCalculatorTableBodySingleRow(val[0], val[1]); + this._renderCalculatorTableBodyOrdRow(val[0]); + this._renderCalculatorTableBodyFinalRow(); + } + } + } + + _changeRowBack(evt) { + const id = evt.target.dataset.id; + if (id) { + const val = id.split("--"); + if (val.length === 2) { + const newPermissionBits = this._singleRowData + .get(val[0]) + .get(val[1]).originalPermissionBits; + + const oldObj = this._singleRowData.get(val[0]).get(val[1]); + this._singleRowData + .get(val[0]) + .set(val[1], { ...oldObj, permissionBits: newPermissionBits }); + this._setOrdRowData(val[0]); + this._setFinalRowData(); + + this._renderCalculatorTableBodySingleRow(val[0], val[1]); + this._renderCalculatorTableBodyOrdRow(val[0]); + this._renderCalculatorTableBodyFinalRow(); + } + } + } + + _grantRow(evt) { + const id = evt.target.dataset.id; + if (id) { + const val = id.split("--"); + if (val.length === 2) { + const newPermissionBits = "11111111"; + + const oldObj = this._singleRowData.get(val[0]).get(val[1]); + this._singleRowData + .get(val[0]) + .set(val[1], { ...oldObj, permissionBits: newPermissionBits }); + this._setOrdRowData(val[0]); + this._setFinalRowData(); + + this._renderCalculatorTableBodySingleRow(val[0], val[1]); + this._renderCalculatorTableBodyOrdRow(val[0]); + this._renderCalculatorTableBodyFinalRow(); + } + } + } + + _removeRow(evt) { + const id = evt.target.dataset.id; + if (id) { + const val = id.split("--"); + if (val.length === 2) { + const newPermissionBits = "--------"; + + const oldObj = this._singleRowData.get(val[0]).get(val[1]); + this._singleRowData + .get(val[0]) + .set(val[1], { ...oldObj, permissionBits: newPermissionBits }); + this._setOrdRowData(val[0]); + this._setFinalRowData(); + + this._renderCalculatorTableBodySingleRow(val[0], val[1]); + this._renderCalculatorTableBodyOrdRow(val[0]); + this._renderCalculatorTableBodyFinalRow(); + } + } + } + + _checkPermissionOnOperatePolicy() { + this._noPermission.innerText = ""; + for (const [targetName, policies] of this._singleRowData) { + const noPermission = Array.from(policies.values()).some( + (policy) => policy.permission === -1 + ); + if (noPermission) { + this._noPermission.removeAttribute("hidden"); + this._noPermission.innerText += `You don't have permission to fetch policy data of ${targetName}.\n`; + const actionButtons = this._tableBody.querySelectorAll( + `[data-id^="${targetName}"]` + ); + actionButtons.forEach((btn) => btn.setAttribute("disabled", "")); + } + } + } + + _getCalculatorEntities() { + const requestedEntityType = this._entityTypeInput.getValue(); + const requestedEntityId = this._entityIdInput.getValue(); + + const { + user, + groupList, + organizationList, + Group: { userIdGroupIdMap }, + } = store.getState(); + + this.entities = []; + + if (requestedEntityType === "organization") { + this.entities.push(["organization", requestedEntityId]); + } else if (requestedEntityType === "group") { + // const group = groupList.find((gr) => gr.id === requestedEntityId); + // entities.push(["organization", group.organization__id]); + this.entities.push(["group", requestedEntityId]); + } else if (requestedEntityType === "user") { + organizationList.forEach((org) => { + this.entities.push(["organization", org.id]); + }); + if (userIdGroupIdMap.has(requestedEntityId)) { + userIdGroupIdMap.get(requestedEntityId).forEach((groupId) => { + this.entities.push(["group", groupId]); + }); + } + this.entities.push(["user", requestedEntityId]); + } + } + + async _getCalculatorTargets() { + const targetType = this._targetTypeInput.getValue(); + const targetId = this._targetIdInput.getValue(); + this.targets = []; + + if (targetType === "project") { + const response = await fetchWithHttpInfo(`/rest/Project/${targetId}`); + + if (response.response?.ok) { + const project = response.data; + this.targets.push(["project", project.id, 0]); + } else { + throw new Error(response.data?.message || "Could not fetch data."); + } + } else if (targetType === "section") { + const response = await fetchWithHttpInfo(`/rest/Section/${targetId}`); + + if (response.response?.ok) { + const section = response.data; + this.targets.push(["project", section.project, 8]); + this.targets.push(["section", section.id, 0]); + } else { + throw new Error(response.data?.message || "Could not fetch data."); + } + } else if (targetType === "version") { + const response = await fetchWithHttpInfo(`/rest/Version/${targetId}`); + + if (response.response?.ok) { + const version = response.data; + this.targets.push(["project", version.project, 8]); + this.targets.push(["version", version.id, 0]); + } else { + throw new Error(response.data?.message || "Could not fetch data."); + } + } else if (targetType === "target_group") { + const response = await fetchWithHttpInfo(`/rest/Group/${targetId}`); + + if (response.response?.ok) { + const group = response.data; + this.targets.push(["target_organization", group.organization, 24]); + this.targets.push(["target_group", group.id, 0]); + } else { + throw new Error(response.data?.message || "Could not fetch data."); + } + } else if (targetType === "target_organization") { + const response = await fetchWithHttpInfo( + `/rest/Organization/${targetId}` + ); + + if (response.response?.ok) { + const organization = response.data; + this.targets.push(["target_organization", organization.id, 0]); + } else { + throw new Error(response.data?.message || "Could not fetch data."); + } + } else if (targetType === "algorithm") { + const response = await fetchWithHttpInfo(`/rest/Algorithm/${targetId}`); + + if (response.response?.ok) { + const algorithm = response.data; + this.targets.push(["project", algorithm.project, 8]); + this.targets.push(["algorithm", algorithm.id, 0]); + } else { + throw new Error(response.data?.message || "Could not fetch data."); + } + } + } + + _saveForm(evt) { + evt.preventDefault(); + + if (this._id === "New") { + } else { + const policyToBeCreated = []; + const policyToBeDeleted = []; + const policyToBeEdited = []; + for (const [targetName, policies] of this._singleRowData) { + for (const [entityName, policy] of policies) { + if (policy.permissionBits !== policy.originalPermissionBits) { + if (policy.originalPermissionBits === "--------") { + policyToBeCreated.push(policy); + } else if (policy.permissionBits === "--------") { + policyToBeDeleted.push(policy); + } else { + policyToBeEdited.push(policy); + } + } + } + } + + this._calculateChanges( + policyToBeCreated, + policyToBeDeleted, + policyToBeEdited + ); + } + } + + _calculateChanges(policyToBeCreated, policyToBeDeleted, policyToBeEdited) { + const toBeCreated = []; + const toBeDeleted = []; + const toBeEdited = []; + policyToBeEdited.forEach((policy) => { + const target = this.targets.find( + (target) => target[0] === policy.targetType + ); + + const newBinaryPermissionOnTarget = this._getRightmost8Bits( + BigInt(`0b${policy.permissionBits}`).toString(2) + ); + + const binaryPermission = this._padToAnyBits( + 8, + BigInt(policy.permission).toString(2) + ); + + const newBinaryPermission = `${binaryPermission.slice( + 0, + -target[2] - 8 + )}${newBinaryPermissionOnTarget}${ + target[2] === 0 ? "" : `${binaryPermission.slice(-target[2])}` + }`; + + const newPermission = parseInt(newBinaryPermission, 2); + console.log(newPermission); + }); + } + + _setUpWarningSavingMsg() { + this._warningDeleteMessage = ` + Pressing confirm will create these policies:

+ ${1} +
+

+ Do you want to continue? + `; + return this._warningDeleteMessage; + } + + _resetTable() { + for (const [targetName, policies] of this._singleRowData) { + for (const [entityName, policy] of policies) { + if (policy.permissionBits !== policy.originalPermissionBits) { + const { originalPermissionBits } = policy; + this._singleRowData.get(targetName).set(entityName, { + ...policy, + permissionBits: originalPermissionBits, + }); + this._renderCalculatorTableBodySingleRow(targetName, entityName); + } + } + this._setOrdRowData(targetName); + this._renderCalculatorTableBodyOrdRow(targetName); + } + this._setFinalRowData(); + this._renderCalculatorTableBodyFinalRow(); + } + + _initInputs(canEdit) { + this._inputsDiv.style.display = ""; + this._entityTypeInput.choices = ENTITY_TYPE_CHOICES; + this._targetTypeInput.choices = TARGET_TYPE_CHOICES; + + if (canEdit) { + this._entityTypeInput.permission = "Can Edit"; + this._entityIdInput.permission = "Can Edit"; + this._targetTypeInput.permission = "Can Edit"; + this._targetIdInput.permission = "Can Edit"; + } else { + this._entityTypeInput.permission = "View Only"; + this._entityIdInput.permission = "View Only"; + this._targetTypeInput.permission = "View Only"; + this._targetIdInput.permission = "View Only"; + } + } + + _inputChange() { + const values = this._inputs.map((input) => { + return [input.dataset.key, input.getValue()]; + }); + + if (values.some((value) => !value[1])) return; + + this._initByData(Object.fromEntries(values)); + } + + _bitwiseOrBinaryStrings(binaryStrings) { + const validBinaryStrings = binaryStrings.filter((str) => str !== ""); + + if (validBinaryStrings.length === 0) return ""; + + let result = parseInt(validBinaryStrings[0], 2); + + for (let i = 1; i < validBinaryStrings.length; i++) { + result |= parseInt(validBinaryStrings[i], 2); + } + + return result.toString(2).padStart(8, "0"); + } + + _getRightmost8Bits(binaryStr) { + // If the binary string is shorter than 8 bits, pad it with leading zeros + const paddedBinaryStr = binaryStr.padStart(8, "0"); + + // Return the rightmost 8 bits + return paddedBinaryStr.slice(-8); + } + + _padToAnyBits(targetLength, binaryStr) { + return binaryStr.padStart(targetLength, "0"); + } + + /** + * Modal for this page, and handler + * @returns sets page attribute that changes dimmer + */ + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } + + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } +} + +customElements.define("policy-calculator-view", PolicyCalculatorView); diff --git a/ui/src/js/permission-settings/single-view-components/policy-single-view.js b/ui/src/js/permission-settings/single-view-components/policy-single-view.js new file mode 100644 index 000000000..cca3f2041 --- /dev/null +++ b/ui/src/js/permission-settings/single-view-components/policy-single-view.js @@ -0,0 +1,1029 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { + POLICY_ENTITY_NAME, + POLICY_TARGET_NAME, + store, + fetchWithHttpInfo, +} from "../store.js"; +import { LoadingSpinner } from "../../components/loading-spinner.js"; + +const EDIT_COLUMN = [ + "Level\n(Self-level to Descendant-level)", + "Exist", + "Read", + "Create", + "Modify", + "Delete", + "Execute", + "Upload", + "ACL", + "Shortcuts", +]; +const EDIT_COLGROUP = ` + + + + + + + + + + +`; + +const BYTE_COUNT = { + project: 5, + media: null, + localization: null, + state: null, + file: null, + section: 3, + algorithm: 1, + version: 2, + target_organization: 5, + target_group: 1, + job_cluster: null, + bucket: null, + hosted_template: 1, +}; + +const ENTITY_TYPE_CHOICES = [ + { label: "User", value: "user" }, + { label: "Group", value: "group" }, + { label: "Organization", value: "organization" }, +]; +const TARGET_TYPE_CHOICES = [ + { label: "Project", value: "project" }, + { label: "Media", value: "media" }, + { label: "Localization", value: "localization" }, + { label: "State", value: "state" }, + { label: "File", value: "file" }, + { label: "Section", value: "section" }, + { label: "Algorithm", value: "algorithm" }, + { label: "Version", value: "version" }, + { label: "Organization", value: "target_organization" }, + { label: "Group", value: "target_group" }, + { label: "Job Cluster", value: "job_cluster" }, + { label: "Bucket", value: "bucket" }, + { label: "Hosted Template", value: "hosted_template" }, +]; +const QUICK_FILL_CHOICES = [ + { label: "Not Selected", value: "" }, + { label: "Full Control", value: "Full Control" }, + { label: "Admin", value: "Admin" }, + { label: "Editor", value: "Editor" }, + { label: "Annotator", value: "Annotator" }, + { label: "Verifier", value: "Verifier" }, + { label: "Viewer", value: "Viewer" }, + { label: "No Access", value: "No Access" }, +]; +const PARENT_TARGET = { + section: ["section", "project"], + project: ["project"], + version: ["version", "project"], +}; + +export class PolicySingleView extends TatorElement { + constructor() { + super(); + + const template = document.getElementById("policy-single-view").content; + this._shadow.appendChild(template.cloneNode(true)); + + this._title = this._shadow.getElementById("title"); + + this._noData = this._shadow.getElementById("no-data"); + this._noPermission = this._shadow.getElementById("no-permission"); + + this._permissionInputDiv = this._shadow.getElementById( + "permission-input-div" + ); + this._quickFillInput = this._shadow.getElementById("quick-fill-input"); + this._permissionInput = this._shadow.getElementById("permission-input"); + + this._entityTypeInput = this._shadow.getElementById("entity-type-input"); + this._entityIdInput = this._shadow.getElementById("entity-id-input"); + this._targetTypeInput = this._shadow.getElementById("target-type-input"); + this._targetIdInput = this._shadow.getElementById("target-id-input"); + this._inputs = [ + this._entityTypeInput, + this._entityIdInput, + this._targetTypeInput, + this._targetIdInput, + ]; + this._inputsDiv = this._shadow.getElementById("inputs-div"); + + this._tableDiv = this._shadow.getElementById("edit-table-div"); + this._table = this._shadow.getElementById("edit-table"); + this._colgroup = this._shadow.getElementById("edit-table--colgroup"); + this._tableHead = this._shadow.getElementById("edit-table--head"); + this._tableBody = this._shadow.getElementById("edit-table--body"); + + this._form = this._shadow.getElementById("edit-table-form"); + this._saveReset = this._shadow.getElementById( + "edit-table--save-reset-section" + ); + this._saveButton = this._shadow.getElementById("edit-table-save"); + this._resetButton = this._shadow.getElementById("edit-table-reset"); + this._deleteButton = this._shadow.getElementById("edit-table-delete"); + + // // loading spinner + this.loading = new LoadingSpinner(); + this._shadow.appendChild(this.loading.getImg()); + + this.modal = document.createElement("modal-dialog"); + this._shadow.appendChild(this.modal); + } + + connectedCallback() { + this._permissionMask = new PermissionMask(); + + this._initInputs(); + + this._inputs.forEach((input) => { + input.addEventListener("change", this._inputChange.bind(this)); + }); + + store.subscribe( + (state) => state.selectedType, + this._updateSelectedType.bind(this) + ); + + store.subscribe((state) => state.Policy, this._setData.bind(this)); + + this._saveButton.addEventListener("click", this._saveForm.bind(this)); + this._resetButton.addEventListener("click", this._resetTable.bind(this)); + this._deleteButton.addEventListener( + "click", + this._openDeletePolicyModal.bind(this) + ); + + this._permissionInput.addEventListener( + "change", + this._changePermissionInput.bind(this) + ); + + this._quickFillInput.addEventListener( + "change", + this._changeQuickFillInput.bind(this) + ); + } + + _updateSelectedType(newSelectedType, oldSelectedType) { + if ( + newSelectedType.typeName !== "Policy" || + newSelectedType.typeId === "All" || + newSelectedType.typeId.startsWith("Cal") + ) { + this._show = false; + return; + } + + // state.Policy may not be initialized, so show the spinner at first + this.showDimmer(); + this.loading.showSpinner(); + + this._show = true; + this.id = newSelectedType.typeId; + } + + /** + * @param {string} val + */ + set id(val) { + this._id = val; + + // Styles reset at the very beginning + this._noData.setAttribute("hidden", ""); + this._noPermission.setAttribute("hidden", ""); + this._inputsDiv.style.display = ""; + this._permissionInputDiv.style.display = ""; + this._permissionInput.permission = "Can Edit"; + this._permissionInput.setValue(null); + this._quickFillInput.permission = "Can Edit"; + this._quickFillInput.setValue(""); + this._saveReset.style.display = ""; + this._tableDiv.setAttribute("hidden", ""); + + // New policy + if (this._id === "New") { + this._resetButton.style.display = ""; + this._deleteButton.style.display = "none"; + this._title.innerText = "New Policy"; + + this._inputs.forEach((input) => (input.permission = "Can Edit")); + } + // Edit policy + else { + this._resetButton.style.display = ""; + this._deleteButton.style.display = ""; + this._title.innerText = "Edit Policy"; + + this._inputs.forEach((input) => (input.permission = "View Only")); + } + + this._setData(); + } + + _setData() { + if (!this._show) return; + const { Policy } = store.getState(); + if (!Policy.init) return; + + // state.Policy initialized, remove the spinner + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + + // New policy + if (this._id === "New") { + this._initByInputs(); + } + // Edit policy + else { + this._initByInputs(Policy.processedMap.get(+this._id)); + } + } + + /** + * @param {object} val + */ + // val is undefined, or an object that has entityType, entityId, targetType, targetId to fetch data + async _initByInputs(val) { + console.log("😇 ~ _initByInputs ~ val:", val, this._id); + + if (val) { + this.showDimmer(); + this.loading.showSpinner(); + + this._setInputValues(val); + this._storeInputValues(); + this._data = await this._getDataByInputs(); + this._processPermission(); + } else { + // New policy + if (this._id === "New") { + this._entityIdInput.setValue(null); + this._targetIdInput.setValue(null); + } + // Edit policy + else { + // didn't find in states, then manually fetch + this._data = await this._getDataById(); + this._setInputValues(this._data); + this._storeInputValues(); + this._processPermission(); + } + } + } + + _storeInputValues() { + console.log("_storeInputValues"); + this._entityType = this._entityTypeInput.getValue(); + this._entityId = this._entityIdInput.getValue(); + this._targetType = this._targetTypeInput.getValue(); + this._targetId = this._targetIdInput.getValue(); + + this._requestedEntityName = `${POLICY_ENTITY_NAME[this._entityType]} ${ + this._entityId + }`; + this._requestedTargetName = `${POLICY_TARGET_NAME[this._targetType]} ${ + this._targetId + }`; + console.log(this._entityType); + } + _setInputValues(val) { + console.log("_setInputValues", val); + this._entityTypeInput.setValue(val.entityType); + this._entityIdInput.setValue(val.entityId); + this._targetTypeInput.setValue(val.targetType); + this._targetIdInput.setValue(val.targetId); + } + + // when state.Policy has this._id + // Or when user wants to create a new policy and have filled in all the inputs + async _getDataByInputs() { + const { + Policy: { processedData }, + } = store.getState(); + console.log(this._entityType, processedData, this._targetType); + const data = processedData.find((policy) => { + return ( + policy.entityType === this._entityType && + policy.entityId === this._entityId && + policy.targetType === this._targetType && + policy.targetId === this._targetId + ); + }); + + if (data) { + return data; + } else { + const policies = await store + .getState() + .getPoliciesByTargets([[this._targetType, this._targetId]]); + + // No policy exists + if (!policies.length) { + return { + id: null, + permission: null, + entityType: this._entityType, + entityId: this._entityId, + entityName: this._requestedEntityName, + targetType: this._targetType, + targetId: this._targetId, + targetName: this._requestedTargetName, + }; + } + // User has no permission on the target + if (policies[0].permission === -1) { + return { id: null, permission: -1 }; + } + + const policy = policies.find( + (policy) => policy.entityName === this._requestedEntityName + ); + // already has this policy + if (policy) { + return policy; + } + // No policy exists + else { + return { + id: null, + permission: null, + entityType: this._entityType, + entityId: this._entityId, + entityName: this._requestedEntityName, + targetType: this._targetType, + targetId: this._targetId, + targetName: this._requestedTargetName, + }; + } + } + } + + // When state.Policy has NO this._id + async _getDataById() { + // Fetch Policy + try { + const data = await store.getState().getPolicyById(+this._id); + return data; + } catch (error) { + console.error(error); + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + this._noData.removeAttribute("hidden"); + this._noData.innerText = error; + return; + } + } + + _processPermission() { + console.log(this._data); + + // When user wants to create a new policy + if (this._id === "New") { + // policy not exists + if (!this._data.permission) { + this._quickFillInput.permission = "Can Edit"; + this._permissionInput.permission = "Can Edit"; + this._quickFillInput.resetChoices(); + this._quickFillInput.choices = this._getQuickFillChoices(); + + this._permissionInputValue = {}; + + // clear table + this._tableDiv.removeAttribute("hidden"); + this._tableHead.innerHTML = ""; + this._tableBody.innerHTML = ""; + // Head + this._renderEditTableHead(); + // Body + this._resetTable(); + + this._saveReset.style.display = ""; + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + // policy not permitted to fetch + else if (this._data.permission === -1) { + this._quickFillInput.permission = "View Only"; + this._permissionInput.permission = "View Only"; + this._noPermission.removeAttribute("hidden"); + this._noPermission.innerText = `You don't have permission to create new policy towards ${this._requestedTargetName}.`; + this._tableDiv.setAttribute("hidden", ""); + this._saveReset.style.display = "none"; + + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + // policy data fetched, should redirect to the edit policy view + else { + window.location.hash = `#Policy-${this._data.id}`; + } + } + // When user wants to edit the policy + else { + // policy not exists + if (!this._data.permission) { + this._inputsDiv.style.display = "none"; + this._permissionInputDiv.style.display = "none"; + this._noData.removeAttribute("hidden"); + this._noData.innerText = `There is no data for Policy ${this._id}.`; + this._tableDiv.setAttribute("hidden", ""); + this._saveReset.style.display = "none"; + } + // policy not permitted to fetch + else if (this._data.permission === -1) { + this._quickFillInput.permission = "View Only"; + this._permissionInput.permission = "View Only"; + this._noPermission.removeAttribute("hidden"); + this._noPermission.innerText = `You don't have permission to edit Policy ${this._id}.`; + this._tableDiv.setAttribute("hidden", ""); + this._saveReset.style.display = "none"; + } + // policy data fetched + else { + this._quickFillInput.resetChoices(); + this._quickFillInput.choices = this._getQuickFillChoices(); + this._inputsDiv.style.display = ""; + + this._permissionInputValue = {}; + + // clear table + this._tableDiv.removeAttribute("hidden"); + this._tableHead.innerHTML = ""; + this._tableBody.innerHTML = ""; + // Head + this._renderEditTableHead(); + // Body + this._resetTable(); + + this._saveReset.style.display = ""; + if (this.hasAttribute("has-open-modal")) { + this.hideDimmer(); + this.loading.hideSpinner(); + } + } + } + } + + _renderEditTableBody() { + this._tableBody.innerHTML = ""; + + this._permissionInput.setValue(this._permissionInputValue.decimal); + const chunks = this._splitIntoChunksOf8Chars( + this._permissionInputValue.binary + ); + const levelCount = chunks.length; + + // Note: reverse() modify the original array + chunks.reverse().forEach((byte, level) => { + const tr = document.createElement("tr"); + + // Level Column + const tdLevel = document.createElement("td"); + tdLevel.innerText = `Level ${level + 1}`; + tr.appendChild(tdLevel); + + if (level === 0) { + tdLevel.innerText += `\n(Self-level)`; + } + if (level === chunks.length - 1) { + tdLevel.innerText += `\n(Youngest Descendant-level)`; + } + + // Permission Columns + byte + .split("") + .reverse() + .forEach((char, index) => { + // id is the index of this char in binaryPermission + const id = (levelCount - level) * 8 - (index + 1); + const td = document.createElement("td"); + if (char === "0") { + const xmark = document.createElement("no-permission-button"); + xmark.setAttribute("data-id", id); + xmark.addEventListener( + "click", + this._changeEditTableCell.bind(this) + ); + td.appendChild(xmark); + } else if (char === "1") { + const check = document.createElement("has-permission-button"); + check.setAttribute("data-id", id); + check.addEventListener( + "click", + this._changeEditTableCell.bind(this) + ); + td.appendChild(check); + } + tr.appendChild(td); + }); + + // Shortcuts Column + const tdSc = document.createElement("td"); + const div = document.createElement("div"); + div.classList.add("d-flex", "flex-row", "flex-justify-center"); + div.style.gap = "5px"; + tdSc.appendChild(div); + + const grant = document.createElement("grant-all-button"); + grant.setAttribute("data-level", level); + grant.addEventListener("click", this._grantRow.bind(this)); + div.appendChild(grant); + const disallow = document.createElement("disallow-permission-button"); + disallow.setAttribute("title", "Disallow All Permission"); + disallow.setAttribute("data-level", level); + disallow.addEventListener("click", this._disallowRow.bind(this)); + div.appendChild(disallow); + + tr.appendChild(tdSc); + + this._tableBody.appendChild(tr); + }); + } + + _renderEditTableHead() { + this._colgroup.innerHTML = EDIT_COLGROUP; + + const tr = document.createElement("tr"); + EDIT_COLUMN.map((val) => { + const th = document.createElement("th"); + th.innerText = val; + return th; + }).forEach((th) => { + tr.appendChild(th); + }); + this._tableHead.appendChild(tr); + } + + _changeEditTableCell(evt) { + this._quickFillInput.setValue(""); + + const index = +evt.target.dataset.id; + if (isNaN(index)) return; + + const oldBinary = this._permissionInputValue.binary; + const oldChar = oldBinary.at(index); + const newChar = oldChar === "0" ? "1" : "0"; + let newBinary = + oldBinary.slice(0, index) + newChar + oldBinary.slice(index + 1); + const newDecimal = parseInt(newBinary, 2); + this._permissionInputValue.binary = newBinary; + this._permissionInputValue.decimal = newDecimal; + this._renderEditTableBody(); + } + + _grantRow(evt) { + const level = +evt.target.dataset.level; + if (isNaN(level)) return; + + this._quickFillInput.setValue(""); + const levelCount = this._permissionInputValue.levelCount; + const oldBinary = this._permissionInputValue.binary; + let newBinary = + oldBinary.slice(0, (levelCount - level - 1) * 8) + + "11111111" + + oldBinary.slice((levelCount - level) * 8); + const newDecimal = parseInt(newBinary, 2); + this._permissionInputValue.binary = newBinary; + this._permissionInputValue.decimal = newDecimal; + this._renderEditTableBody(); + } + _disallowRow(evt) { + const level = +evt.target.dataset.level; + if (isNaN(level)) return; + + this._quickFillInput.setValue(""); + const levelCount = this._permissionInputValue.levelCount; + const oldBinary = this._permissionInputValue.binary; + let newBinary = + oldBinary.slice(0, (levelCount - level - 1) * 8) + + "00000000" + + oldBinary.slice((levelCount - level) * 8); + const newDecimal = parseInt(newBinary, 2); + this._permissionInputValue.binary = newBinary; + this._permissionInputValue.decimal = newDecimal; + this._renderEditTableBody(); + } + + _changePermissionInput() { + this._quickFillInput.setValue(""); + + const newDecimal = this._permissionInput.getValue(); + this._permissionInputValue.decimal = newDecimal; + + const binary = newDecimal.toString(2); + // If binary's length is smaller than level count defined in BYTE_COUNT, then pad it with "0"s + this._permissionInputValue.binary = this._padToAnyBits( + this._permissionInputValue.levelCount * 8, + binary + ); + + this._renderEditTableBody(); + } + + _changeQuickFillInput() { + const quickFillValue = this._quickFillInput.getValue(); + if (quickFillValue === "") return; + + console.log( + this._data.targetType, + quickFillValue, + this._permissionMask.QUICK_FILL_PERMISSIONS + ); + + let newDecimal = + this._permissionMask.QUICK_FILL_PERMISSIONS[this._data.targetType]?.[ + quickFillValue + ]; + console.log("😇 ~ _changeQuickFillInput ~ newDecimal:", newDecimal); + if (typeof newDecimal === "undefined") { + const levelCount = this._permissionInputValue.levelCount; + const binary = "-".repeat(levelCount * 8); + + this._permissionInputValue.decimal = null; + this._permissionInputValue.binary = binary; + } else { + this._permissionInputValue.decimal = newDecimal; + + const binary = newDecimal.toString(2); + // If binary's level count is smaller than it defined in BYTE_COUNT, then pad it with "0"s + this._permissionInputValue.binary = this._padToAnyBits( + this._permissionInputValue.levelCount * 8, + binary + ); + } + + this._renderEditTableBody(); + } + + async _saveForm(evt) { + evt.preventDefault(); + + // Create a new policy + if (this._id === "New") { + try { + const body = { + [this._data.targetType]: this._data.targetId, + [this._data.entityType]: this._data.entityId, + permission: Number(this._permissionInputValue.decimal), + }; + const responseInfo = await store.getState().createPolicy(body); + + console.log("😇 ~ responseInfo ~ responseInfo:", responseInfo); + this.handleResponse(responseInfo); + } catch (err) { + this.modal._error(err); + } + } + // Edit a policy + else { + this._openUpdatePolicyModal(); + } + } + + _resetTable() { + const policy = this._data; + + if (this._id === "New") { + const levelCount = BYTE_COUNT[policy.targetType]; + const binary = "-".repeat(levelCount * 8); + const decimal = null; + + this._permissionInputValue.binary = binary; + this._permissionInputValue.decimal = decimal; + this._permissionInputValue.levelCount = levelCount; + + this._renderEditTableBody(); + } else { + this._permissionInputValue.decimal = policy.permission; + + const binary = policy.permission.toString(2); + // If binary's level count is smaller than it defined in BYTE_COUNT, then pad it with "0"s + const levelCount = Math.max( + Math.ceil(binary.length / 8), + BYTE_COUNT[policy.targetType] + ); + + this._permissionInputValue.binary = this._padToAnyBits( + levelCount * 8, + binary + ); + this._permissionInputValue.levelCount = levelCount; + this._renderEditTableBody(); + } + } + + _initInputs() { + this._entityTypeInput.choices = ENTITY_TYPE_CHOICES; + this._targetTypeInput.choices = TARGET_TYPE_CHOICES; + } + + _getQuickFillChoices() { + const keys = Object.keys( + this._permissionMask.QUICK_FILL_PERMISSIONS[this._targetType] + ); + const choices = [{ label: "Not Selected", value: "" }]; + keys.forEach((key) => { + choices.push({ label: key, value: key }); + }); + console.log(choices); + return choices; + } + + _inputChange() { + const values = this._inputs.map((input) => { + return [input.dataset.key, input.getValue()]; + }); + + if (values.some((value) => !value[1])) return; + + this._initByInputs(Object.fromEntries(values)); + } + + _openUpdatePolicyModal() { + const button = document.createElement("button"); + button.setAttribute("class", "btn btn-clear f1 text-semibold btn-red"); + + let confirmText = document.createTextNode("Confirm"); + button.appendChild(confirmText); + + button.addEventListener("click", this._updatePolicy.bind(this)); + this._setUpWarningMsg("PATCH"); + + this.modal._confirm({ + titleText: `Update Confirmation`, + mainText: this._modalWarningMessage, + buttonSave: button, + }); + } + + async _openDeletePolicyModal() { + const button = document.createElement("button"); + button.setAttribute("class", "btn btn-clear f1 text-semibold btn-red"); + + let confirmText = document.createTextNode("Confirm"); + button.appendChild(confirmText); + + button.addEventListener("click", this._deletePolicy.bind(this)); + this._setUpWarningMsg("DELETE"); + + this.modal._confirm({ + titleText: `Delete Confirmation`, + mainText: this._modalWarningMessage, + buttonSave: button, + }); + } + + _setUpWarningMsg(method) { + if (method === "DELETE") { + this._modalWarningMessage = ` + Pressing confirm will create this policy:


+ ID: ${this._data.id}, ${this._requestedEntityName} against ${this._requestedTargetName}.


+ Do you want to continue? + `; + } else if (method === "PATCH") { + this._modalWarningMessage = ` + Pressing confirm will update this policy:


+ ID: ${this._data.id}, ${this._requestedEntityName} against ${this._requestedTargetName}.

+ From ${this._data.permission} to ${this._permissionInputValue.decimal}.


+ Do you want to continue? + `; + } + } + + async _updatePolicy() { + const { id } = this._data; + + this.modal._modalCloseAndClear(); + try { + const responseInfo = await store + .getState() + .updatePolicy(id, Number(this._permissionInputValue.decimal)); + this.handleResponse(responseInfo); + } catch (err) { + this.modal._error(err); + } + } + + async _deletePolicy() { + const { id } = this._data; + + this.modal._modalCloseAndClear(); + try { + const responseInfo = await store.getState().deletePolicy(id); + this.handleResponse(responseInfo); + } catch (err) { + this.modal._error(err); + } + } + + handleResponse(info) { + let message = info.data?.message ? info.data.message : ""; + if (info.response?.ok) { + store.getState().setPolicyData(); + return this.modal._success(message); + } else { + if (info.response?.status) { + return this.modal._error( + `${info.response.status}

${message}` + ); + } else { + return this.modal._error(`Error: Could not process request.`); + } + } + } + + _bitwiseOrBinaryStrings(binaryStrings) { + const validBinaryStrings = binaryStrings.filter((str) => str !== ""); + + if (validBinaryStrings.length === 0) return ""; + + let result = parseInt(validBinaryStrings[0], 2); + + for (let i = 1; i < validBinaryStrings.length; i++) { + result |= parseInt(validBinaryStrings[i], 2); + } + + return result.toString(2).padStart(8, "0"); + } + + _getRightmost8Bits(binaryStr) { + // If the binary string is shorter than 8 bits, pad it with leading zeros + const paddedBinaryStr = binaryStr.padStart(8, "0"); + + // Return the rightmost 8 bits + return paddedBinaryStr.slice(-8); + } + + _padToAnyBits(targetLength, binaryStr) { + return binaryStr.padStart(targetLength, "0"); + } + + _splitIntoChunksOf8Chars(str) { + let chunks = []; + for (let i = 0; i < str.length; i += 8) { + chunks.push(str.substring(i, i + 8)); + } + return chunks; + } + + /** + * Modal for this page, and handler + * @returns sets page attribute that changes dimmer + */ + showDimmer() { + return this.setAttribute("has-open-modal", ""); + } + + hideDimmer() { + return this.removeAttribute("has-open-modal"); + } +} + +customElements.define("policy-single-view", PolicySingleView); + +class PermissionMask { + constructor() { + // These bits are repeated so the left-byte is for children objects. This allows + // a higher object to store the default permission for children objects by bitshifting by the + // level of abstraction. + // [0:7] Self-level objects (projects, algos, versions) + // [8:15] Children objects (project -> section* -> media -> metadata) + // [16:23] Grandchildren objects (project -> section -> media* -> metadata) + // [24:31] Great-grandchildren objects (project -> section -> media -> metadata*) + // If a permission points to a child object, that occupies [0:7] + // Permission objects exist against either projects, algos, versions or sections + + this.EXIST = BigInt(0x1); // Allows a row to be seen in a list, or individual GET + this.READ = BigInt(0x2); // Allows a references to be accessed, e.g. generate presigned URLs + this.CREATE = BigInt(0x4); // Allows a row to be created (e.g. POST) + this.MODIFY = BigInt(0x8); // Allows a row to be PATCHED (but not in-place, includes variant delete) + this.DELETE = BigInt(0x10); // Allows a row to be deleted (pruned for metadata) + this.EXECUTE = BigInt(0x20); // Allows an algorithm to be executed (applies to project-level or algorithm) + this.UPLOAD = BigInt(0x40); // Allows media to be uploaded + this.ACL = BigInt(0x80); // Allows ACL modification for a row, if not a creator + this.FULL_CONTROL = BigInt(0xff); // All bits and all future bits are set + + this.QUICK_FILL_PERMISSIONS = { + project: { + "Full Control": + (this.FULL_CONTROL << 32n) | + (this.FULL_CONTROL << 24n) | + (this.FULL_CONTROL << 16n) | + (this.FULL_CONTROL << 8n) | + this.FULL_CONTROL, + "No Access": 0, + }, + media: { + "No Access": 0, + }, + localization: { + "No Access": 0, + }, + state: { + "No Access": 0, + }, + file: { + "No Access": 0, + }, + section: { + "Full Control": + (this.FULL_CONTROL << 16n) | + (this.FULL_CONTROL << 8n) | + this.FULL_CONTROL, + Admin: + ((this.EXIST | + this.READ | + this.MODIFY | + this.CREATE | + this.DELETE | + this.ACL) << + 16n) | + ((this.EXIST | + this.READ | + this.MODIFY | + this.CREATE | + this.DELETE | + this.UPLOAD | + this.ACL) << + 8n) | + (this.EXIST | + this.READ | + this.MODIFY | + this.CREATE | + this.DELETE | + this.ACL), + Editor: + ((this.EXIST | this.READ | this.MODIFY | this.CREATE | this.DELETE) << + 16n) | + ((this.EXIST | + this.READ | + this.MODIFY | + this.CREATE | + this.DELETE | + this.UPLOAD) << + 8n) | + (this.EXIST | this.READ | this.MODIFY | this.CREATE | this.DELETE), + Annotator: + ((this.EXIST | this.READ | this.MODIFY | this.CREATE | this.DELETE) << + 16n) | + ((this.EXIST | this.READ) << 8n) | + (this.EXIST | this.READ), + Verifier: + ((this.EXIST | this.READ | this.MODIFY) << 16n) | + ((this.EXIST | this.READ) << 8n) | + (this.EXIST | this.READ), + Viewer: + ((this.EXIST | this.READ) << 16n) | + ((this.EXIST | this.READ) << 8n) | + (this.EXIST | this.READ), + "No Access": 0, + }, + algorithm: { + "Full Control": this.FULL_CONTROL, + "No Access": 0, + }, + version: { + "Full Control": (this.FULL_CONTROL << 8n) | this.FULL_CONTROL, + "No Access": 0, + }, + target_organization: { + "Full Control": + (this.FULL_CONTROL << 32n) | + (this.FULL_CONTROL << 24n) | + (this.FULL_CONTROL << 16n) | + (this.FULL_CONTROL << 8n) | + this.FULL_CONTROL, + "No Access": 0, + }, + target_group: { + "Full Control": this.FULL_CONTROL, + "No Access": 0, + }, + job_cluster: { + "No Access": 0, + }, + bucket: { + "No Access": 0, + }, + template: { + "Full Control": this.FULL_CONTROL, + "No Access": 0, + }, + }; + } +} diff --git a/ui/src/js/permission-settings/store.js b/ui/src/js/permission-settings/store.js new file mode 100644 index 000000000..b59dfb595 --- /dev/null +++ b/ui/src/js/permission-settings/store.js @@ -0,0 +1,1058 @@ +import create from "../../../node_modules/zustand/esm/vanilla.mjs"; +import { subscribeWithSelector } from "../../../node_modules/zustand/esm/middleware.js"; +import { fetchCredentials } from "../../../../scripts/packages/tator-js/src/utils/fetch-credentials.js"; +// import { data as policy, noPer } from "./test.js"; + +const listResources = { + section: "Sections", + project: "Projects", +}; +const detailResources = { + section: "Section", + project: "Project", +}; + +const POLICY_ENTITY_NAME = { + user: "User", + organization: "Organization", + group: "Group", +}; +const POLICY_TARGET_NAME = { + project: "Project", + media: "Media", + localization: "Localization", + state: "State", + file: "File", + section: "Section", + algorithm: "Algorithm", + version: "Version", + target_organization: "Organization", + target_group: "Group", + job_cluster: "Job Cluster", + bucket: "Bucket", + hosted_template: "Hosted Template", +}; +const POLICY_ENTITY_TYPE = Object.keys(POLICY_ENTITY_NAME); +const POLICY_TARGET_TYPE = Object.keys(POLICY_TARGET_NAME); + +export async function fetchWithHttpInfo(url, options, retry = false) { + const response = await fetchCredentials(url, options); + return { response: response, data: await response.json() }; +} + +const store = create( + subscribeWithSelector((set, get) => ({ + announcements: [], + + user: null, + groupList: [], + organizationList: [], + + selectedType: { + typeName: "Group", + typeId: "All", + }, + + User: { + init: false, + data: null, + map: null, + }, + + Group: { + init: false, + data: null, + map: null, + groupIdUserIdMap: null, + groupIdGroupNameMap: null, + userIdGroupIdMap: null, + }, + tabularGroup: { + Group: { + count: 0, + data: null, + }, + User: { + count: 0, + userIdGroupIdMap: null, + }, + }, + selectedGroupIds: [], + groupViewBy: "Group", + groupSearchParams: { + Group: { + filter: [], + sortBy: { + groupName: "", + groupId: "", + }, + pagination: {}, + }, + User: { + filter: [], + sortBy: { + userId: "", + }, + pagination: {}, + }, + }, + + Policy: { + init: false, + data: null, + processedData: null, + map: null, + processedMap: null, + noPermissionEntities: [], + }, + tabularPolicy: { + count: 0, + data: null, + }, + selectedPolicyIds: [], + policySearchParams: { + filter: [], + sortBy: { + entityName: "", + targetName: "", + }, + pagination: {}, + }, + + initHeader: async () => { + Promise.all([ + fetchCredentials(`/rest/User/GetCurrent`, {}, true).then((response) => + response.json() + ), + fetchCredentials("/rest/Announcements", {}, true).then((response) => + response.json() + ), + ]).then((values) => { + set({ + user: values[0], + announcements: values[1], + // isStaff: values[0].is_staff, + }); + }); + }, + + getCurrentUserGroupList: async () => { + let groupList = []; + + const { organizationList, user } = get(); + + for (const org of organizationList) { + const data = await fetchCredentials( + `/rest/Groups/${org.id}?user=${user.id}`, + {}, + true + ).then((response) => response.json()); + groupList.push(...data); + } + + set({ groupList }); + }, + + getOrganizationList: async () => { + const organizationList = await fetchCredentials( + `/rest/Organizations`, + {}, + true + ).then((response) => response.json()); + + set({ organizationList }); + }, + + setGroupData: async () => { + let data = []; + let map = new Map(); + let groupIdUserIdMap = new Map(); + let groupIdGroupNameMap = new Map(); + let userIdGroupIdMap = new Map(); + + const { organizationList } = get(); + + for (const org of organizationList) { + const groupList = await fetchCredentials( + `/rest/Groups/${org.id}`, + {}, + true + ).then((response) => response.json()); + data.push(...groupList); + } + + data.forEach((gr) => { + map.set(gr.id, gr); + groupIdUserIdMap.set(gr.id, gr.members); + groupIdGroupNameMap.set(gr.id, gr.name); + }); + + data.forEach((gr) => { + gr.members.forEach((userId) => { + if (!userIdGroupIdMap.has(userId)) { + userIdGroupIdMap.set(userId, []); + } + userIdGroupIdMap.get(userId).push(gr.id); + }); + }); + + set({ + Group: { + init: true, + data, + map, + groupIdUserIdMap, + groupIdGroupNameMap, + userIdGroupIdMap, + }, + }); + }, + + setTabularGroup: (groupViewBy) => { + if (groupViewBy === "Group") { + get().setViewByGroupData(); + } else if (groupViewBy === "User") { + get().setViewByUserData(); + } + }, + + setViewByGroupData: () => { + const { Group: viewByGroupSearchParams } = get().groupSearchParams; + + const { groupIdGroupNameMap, groupIdUserIdMap } = get().Group; + let filteredGroupIdGroupNameMap = new Map(groupIdGroupNameMap); + let filteredGroupIdUserIdMap = new Map(groupIdUserIdMap); + + // Filter + viewByGroupSearchParams.filter.forEach((con) => { + switch (con.category) { + case "groupName": + switch (con.modifier) { + case "includes": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!groupName.includes(con.value)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "equals": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupName !== con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "starts with": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!groupName.startsWith(con.value)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "ends with": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!groupName.endsWith(con.value)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "not equal": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupName === con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + } + break; + case "groupId": + switch (con.modifier) { + case "==": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupId !== con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "!=": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupId === con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case ">=": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupId < con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "<=": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupId > con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "in": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!con.value.includes(groupId)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + } + break; + case "userId": + switch (con.modifier) { + case "==": + for (let [groupId, userIds] of filteredGroupIdUserIdMap) { + if (!userIds.includes(con.value)) { + filteredGroupIdUserIdMap.delete(groupId); + } + } + break; + case "!=": + for (let [groupId, userIds] of filteredGroupIdUserIdMap) { + if (userIds.includes(con.value)) { + filteredGroupIdUserIdMap.delete(groupId); + } + } + break; + case ">=": + for (let [groupId, userIds] of filteredGroupIdUserIdMap) { + if (userIds.every((id) => id < con.value)) { + filteredGroupIdUserIdMap.delete(groupId); + } + } + break; + case "<=": + for (let [groupId, userIds] of filteredGroupIdUserIdMap) { + if (userIds.every((id) => id > con.value)) { + filteredGroupIdUserIdMap.delete(groupId); + } + } + break; + case "in": + for (let [groupId, userIds] of filteredGroupIdUserIdMap) { + if (userIds.every((id) => !con.value.includes(id))) { + filteredGroupIdUserIdMap.delete(groupId); + } + } + break; + } + break; + } + }); + // a group id must be in both filteredGroupIdGroupNameMap and filteredGroupIdUserIdMap + // So that it meets all filter conditions + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!filteredGroupIdUserIdMap.has(groupId)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + // Now filteredGroupIdGroupNameMap meets all filter conditions + const count = filteredGroupIdGroupNameMap.size; + + // Sort + let sortedGroupIdGroupNameMap = null; + + if (viewByGroupSearchParams.sortBy.groupName === "ascending") { + sortedGroupIdGroupNameMap = new Map( + [...filteredGroupIdGroupNameMap.entries()].sort((a, b) => + a[1].localeCompare(b[1]) + ) + ); + } else if (viewByGroupSearchParams.sortBy.groupName === "descending") { + sortedGroupIdGroupNameMap = new Map( + [...filteredGroupIdGroupNameMap.entries()].sort((a, b) => + b[1].localeCompare(a[1]) + ) + ); + } else if (viewByGroupSearchParams.sortBy.groupId === "ascending") { + sortedGroupIdGroupNameMap = new Map( + [...filteredGroupIdGroupNameMap.entries()].sort((a, b) => a[0] - b[0]) + ); + } else if (viewByGroupSearchParams.sortBy.groupId === "descending") { + sortedGroupIdGroupNameMap = new Map( + [...filteredGroupIdGroupNameMap.entries()].sort((a, b) => b[0] - a[0]) + ); + } else { + sortedGroupIdGroupNameMap = new Map([ + ...filteredGroupIdGroupNameMap.entries(), + ]); + } + + // Paginate + const paginatedEntries = new Map( + [...sortedGroupIdGroupNameMap.entries()].slice( + viewByGroupSearchParams.pagination.start, + viewByGroupSearchParams.pagination.stop + ) + ); + + // Set data + const { map } = get().Group; + const data = []; + for (let [groupId, groupName] of paginatedEntries) { + data.push(map.get(groupId)); + } + + set({ + tabularGroup: { + ...get().tabularGroup, + Group: { + count, + data, + }, + }, + }); + }, + + setViewByUserData: () => { + const { User: viewByUserSearchParams } = get().groupSearchParams; + + const { groupIdGroupNameMap, userIdGroupIdMap } = get().Group; + let filteredGroupIdGroupNameMap = new Map(groupIdGroupNameMap); + let filteredUserIdGroupIdMap = new Map(userIdGroupIdMap); + + // Filter + viewByUserSearchParams.filter.forEach((con) => { + switch (con.category) { + case "groupName": + switch (con.modifier) { + case "includes": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!groupName.includes(con.value)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "equals": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupName !== con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "starts with": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!groupName.startsWith(con.value)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "ends with": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (!groupName.endsWith(con.value)) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + case "not equal": + for (let [groupId, groupName] of filteredGroupIdGroupNameMap) { + if (groupName === con.value) { + filteredGroupIdGroupNameMap.delete(groupId); + } + } + break; + } + break; + case "groupId": + switch (con.modifier) { + case "==": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (!groupIds.includes(con.value)) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case "!=": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (groupIds.includes(con.value)) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case ">=": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (groupIds.every((id) => id < con.value)) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case "<=": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (groupIds.every((id) => id > con.value)) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case "in": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (groupIds.every((id) => !con.value.includes(id))) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + } + break; + case "userId": + switch (con.modifier) { + case "==": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (userId !== con.value) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case "!=": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (userId === con.value) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case ">=": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (userId < con.value) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case "<=": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (userId > con.value) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + case "in": + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + if (!con.value.includes(userId)) { + filteredUserIdGroupIdMap.delete(userId); + } + } + break; + } + break; + } + }); + // a group id must be in both filteredGroupIdGroupNameMap and filteredUserIdGroupIdMap + // So that it meets all filter conditions + const groupIdWithFilteredName = Array.from( + filteredGroupIdGroupNameMap.keys() + ); + for (let [userId, groupIds] of filteredUserIdGroupIdMap) { + const hasSameGroupId = groupIds.some((id) => + groupIdWithFilteredName.includes(id) + ); + if (!hasSameGroupId) { + filteredUserIdGroupIdMap.delete(userId); + } + } + // Now filteredUserIdGroupIdMap meets all filter conditions + const count = filteredUserIdGroupIdMap.size; + + // Sort + let sortedUserIdGroupIdMap = null; + if (viewByUserSearchParams.sortBy.userId === "ascending") { + sortedUserIdGroupIdMap = new Map( + [...filteredUserIdGroupIdMap.entries()].sort((a, b) => a[0] - b[0]) + ); + } else if (viewByUserSearchParams.sortBy.userId === "descending") { + sortedUserIdGroupIdMap = new Map( + [...filteredUserIdGroupIdMap.entries()].sort((a, b) => b[0] - a[0]) + ); + } else { + sortedUserIdGroupIdMap = new Map([ + ...filteredUserIdGroupIdMap.entries(), + ]); + } + + // Paginate + const paginatedEntries = new Map( + [...sortedUserIdGroupIdMap.entries()].slice( + viewByUserSearchParams.pagination.start, + viewByUserSearchParams.pagination.stop + ) + ); + + // Set data + set({ + tabularGroup: { + ...get().tabularGroup, + User: { + count, + userIdGroupIdMap: paginatedEntries, + }, + }, + }); + }, + + setPolicyData: async () => { + let data = []; + let processedData = []; + let map = new Map(); + let processedMap = new Map(); + let noPermissionEntities = []; + + const { user, groupList, organizationList } = get(); + + const userPolicyList = await fetchCredentials( + `/rest/RowProtections?user=${user.id}`, + {} + ).then((response) => response.json()); + data.push(...userPolicyList); + + for (const gr of groupList) { + const groupPolicyList = await fetchCredentials( + `/rest/RowProtections?group=${gr.id}`, + {} + ).then((response) => response.json()); + // When user is not allowed to get a permission item, the data sent back is not an array + if (!Array.isArray(groupPolicyList)) { + noPermissionEntities.push(["group", gr.id]); + continue; + } + data.push(...groupPolicyList); + } + + for (const org of organizationList) { + const organizationPolicyList = await fetchCredentials( + `/rest/RowProtections?organization=${org.id}`, + {} + ).then((response) => response.json()); + // When user is not allowed to get a permission item, the data sent back is not an array + if (!Array.isArray(organizationPolicyList)) { + noPermissionEntities.push(["group", gr.id]); + continue; + } + data.push(...organizationPolicyList); + } + // data = policy; + // noPermissionEntities = noPer; + + data.forEach((policy) => { + map.set(policy.id, policy); + }); + + processedData = get().processPolicyData(data); + processedData.forEach((pd) => { + processedMap.set(pd.id, pd); + }); + + set({ + Policy: { + init: true, + data, + processedData, + map, + processedMap, + noPermissionEntities, + }, + }); + }, + + setTabularPolicy: () => { + const { policySearchParams } = get(); + + const { processedMap } = get().Policy; + let filteredProcessedMap = new Map(processedMap); + + // Filter + policySearchParams.filter.forEach((con) => { + switch (con.category) { + case "entityType": + case "targetType": + switch (con.modifier) { + case "equals": + for (let [policyId, policy] of filteredProcessedMap) { + if (policy[con.category] !== con.value) { + filteredProcessedMap.delete(policyId); + } + } + break; + case "not equal": + for (let [policyId, policy] of filteredProcessedMap) { + if (policy[con.category] === con.value) { + filteredProcessedMap.delete(policyId); + } + } + break; + } + break; + case "entityId": + case "targetId": + switch (con.modifier) { + case "==": + for (let [policyId, policy] of filteredProcessedMap) { + if (policy[con.category] !== con.value) { + filteredProcessedMap.delete(policyId); + } + } + break; + case "!=": + for (let [policyId, policy] of filteredProcessedMap) { + if (policy[con.category] === con.value) { + filteredProcessedMap.delete(policyId); + } + } + break; + case ">=": + for (let [policyId, policy] of filteredProcessedMap) { + if (policy[con.category] < con.value) { + filteredProcessedMap.delete(policyId); + } + } + break; + case "<=": + for (let [policyId, policy] of filteredProcessedMap) { + if (policy[con.category] > con.value) { + filteredProcessedMap.delete(policyId); + } + } + break; + case "in": + for (let [policyId, policy] of filteredProcessedMap) { + if (!con.value.includes(policy[con.category])) { + filteredProcessedMap.delete(policyId); + } + } + break; + } + break; + } + }); + const count = filteredProcessedMap.size; + + // Sort + let sortedProcessedMap = null; + sortedProcessedMap = new Map( + [...filteredProcessedMap.entries()].sort((a, b) => { + const sortBy = policySearchParams.sortBy; + + if (sortBy.entityName === "ascending") { + return a[1].entityName.localeCompare(b[1].entityName); + } else if (sortBy.entityName === "descending") { + return b[1].entityName.localeCompare(a[1].entityName); + } else if (sortBy.targetName === "ascending") { + return a[1].targetName.localeCompare(b[1].targetName); + } else if (sortBy.targetName === "descending") { + return b[1].targetName.localeCompare(a[1].targetName); + } else { + return 0; + } + }) + ); + + // Paginate + const paginatedEntries = new Map( + [...sortedProcessedMap.entries()].slice( + policySearchParams.pagination.start, + policySearchParams.pagination.stop + ) + ); + + // Set data + const data = []; + for (let [policyId, policy] of paginatedEntries) { + data.push(policy); + } + + set({ + tabularPolicy: { + count, + data, + }, + }); + }, + + setUserData: async () => { + if (!get().Group.map || !get().Group.map.size) return; + + let data = []; + let map = new Map(); + + const { userIdGroupIdMap } = get().Group; + + if (get().User.map && get().User.map.size) { + map = new Map(get().User.map); + } + for (const userId of userIdGroupIdMap.keys()) { + const userData = await fetchCredentials( + `/rest/User/${userId}`, + {}, + true + ).then((response) => response.json()); + data.push(userData); + map.set(userId, userData); + } + + set({ + User: { + init: true, + data, + map, + }, + }); + }, + + getPoliciesByTargets: async (targets) => { + const policies = []; + for (const target of targets) { + try { + const info = await fetchWithHttpInfo( + `/rest/RowProtections?${target[0]}=${target[1]}` + ); + + if (info.response.ok) { + policies.push(...info.data); + } else if (!info.response.ok && info.response.status === 403) { + policies.push({ [target[0]]: target[1], permission: -1 }); + } + } catch (error) { + console.error(error); + } + } + + const processedPolicies = get().processPolicyData(policies); + + return processedPolicies; + }, + + getPolicyById: async (policyId) => { + try { + const info = await fetchWithHttpInfo(`/rest/RowProtection/${policyId}`); + + if (info.response.ok) { + const policy = info.data; + const processedPolicy = get().processPolicyData([policy])[0]; + return processedPolicy; + } else if (!info.response.ok && info.response.status === 403) { + return { + id: policyId, + permission: -1, + }; + } else { + return { + id: policyId, + permission: null, + }; + } + } catch (error) { + console.error(error); + } + }, + + processPolicyData: (data) => { + return data.map((policy) => { + let processedObj = null; + + const entityType = POLICY_ENTITY_TYPE.find( + (en) => policy[en] != undefined + ); + const targetType = POLICY_TARGET_TYPE.find( + (ta) => policy[ta] != undefined + ); + const entityName = `${POLICY_ENTITY_NAME[entityType]} ${policy[entityType]}`; + const targetName = `${POLICY_TARGET_NAME[targetType]} ${policy[targetType]}`; + + if (policy.permission === -1) { + processedObj = { + id: null, + entityName: "ALL", + targetName, + permission: policy.permission, + entityType: "ALL", + targetType, + entityId: "ALL", + targetId: policy[targetType], + }; + } else { + processedObj = { + id: policy.id, + entityName, + targetName, + permission: policy.permission, + entityType, + targetType, + entityId: policy[entityType], + targetId: policy[targetType], + }; + } + + return processedObj; + }); + }, + + findUsers: async (inputList) => { + const notFound = []; + const found = new Map(); + + for (let input of inputList) { + const users = await fetchCredentials( + `/rest/Users?${ + input.indexOf("@") > -1 ? "email" : "username" + }=${encodeURIComponent(input)}`, + {}, + true + ).then((response) => response.json()); + + if (users.length) { + for (const user of users) { + found.set(user.id, user); + } + } else { + notFound.push(input); + } + } + + return { found, notFound }; + }, + + findUserById: async (id) => { + const user = await fetchCredentials(`/rest/User/${id}`, {}, true).then( + (response) => response.json() + ); + + if (user.id && !user.message) { + return user; + } else { + return null; + } + }, + + createGroup: async (orgId, data) => { + try { + const fn = async (orgId, body) => { + return await fetchWithHttpInfo(`/rest/Groups/${orgId}`, { + method: "POST", + body: JSON.stringify(body), + }); + }; + const responseInfo = await fn(orgId, data); + + // This includes the reponse so error handling can happen in ui + return responseInfo; + } catch (err) { + set({ status: { ...get().status, name: "idle", msg: "" } }); + return err; + } + }, + + updateGroup: async (groupId, data) => { + try { + const fn = async (groupId, body) => { + return await fetchWithHttpInfo(`/rest/Group/${groupId}`, { + method: "PATCH", + body: JSON.stringify(body), + }); + }; + const responseInfo = await fn(groupId, data); + + // This includes the reponse so error handling can happen in ui + return responseInfo; + } catch (err) { + set({ status: { ...get().status, name: "idle", msg: "" } }); + return err; + } + }, + + deleteGroup: async (groupId) => { + try { + const fn = async (groupId) => { + return await fetchWithHttpInfo(`/rest/Group/${groupId}`, { + method: "DELETE", + }); + }; + const responseInfo = await fn(groupId); + + // This includes the reponse so error handling can happen in ui + return responseInfo; + } catch (err) { + set({ status: { ...get().status, name: "idle", msg: "" } }); + return err; + } + }, + + createPolicy: async (data) => { + try { + const fn = async (body) => { + return await fetchWithHttpInfo(`/rest/RowProtections`, { + method: "POST", + body: JSON.stringify(body), + }); + }; + const responseInfo = await fn(data); + + // This includes the reponse so error handling can happen in ui + return responseInfo; + } catch (err) { + throw new Error(err); + } + }, + + updatePolicy: async (policyId, newPermission) => { + try { + const fn = async (policyId, newPermission) => { + return await fetchWithHttpInfo(`/rest/RowProtection/${policyId}`, { + method: "PATCH", + body: JSON.stringify({ + permission: newPermission, + }), + }); + }; + const responseInfo = await fn(policyId, newPermission); + + // This includes the reponse so error handling can happen in ui + return responseInfo; + } catch (err) { + throw new Error(err); + } + }, + + deletePolicy: async (policyId) => { + try { + const fn = async (policyId) => { + return await fetchWithHttpInfo(`/rest/RowProtection/${policyId}`, { + method: "DELETE", + }); + }; + const responseInfo = await fn(policyId); + + // This includes the reponse so error handling can happen in ui + return responseInfo; + } catch (err) { + throw new Error(err); + } + }, + + setSelectedType: (selectedType) => { + set({ selectedType }); + }, + + setGroupViewBy: (groupViewBy) => { + set({ groupViewBy }); + }, + + setGroupSearchParams: (groupSearchParams) => { + set({ groupSearchParams }); + }, + + setSelectedGroupIds: (selectedGroupIds) => { + set({ selectedGroupIds }); + }, + + setSelectedPolicyIds: (selectedPolicyIds) => { + set({ selectedPolicyIds }); + }, + + setPolicySearchParams: (policySearchParams) => { + set({ policySearchParams }); + }, + })) +); + +export { POLICY_ENTITY_NAME, POLICY_TARGET_NAME, store }; diff --git a/ui/src/js/permission-settings/table-view-components/group-filter-condition.js b/ui/src/js/permission-settings/table-view-components/group-filter-condition.js new file mode 100644 index 000000000..e612ab8c4 --- /dev/null +++ b/ui/src/js/permission-settings/table-view-components/group-filter-condition.js @@ -0,0 +1,180 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +const CATEGORY_CHOICES = [ + { label: "Group Name", value: "groupName" }, + { label: "Group ID", value: "groupId" }, + { label: "User ID", value: "userId" }, +]; +const MODIFIER_CHOICES = { + NAME: [ + { label: "Includes", value: "includes" }, + { label: "Equals", value: "equals" }, + { label: "Starts with", value: "starts with" }, + { label: "Ends with", value: "ends with" }, + { label: "Does not equal", value: "not equal" }, + ], + ID: [ + { label: "Equals", value: "==" }, + { label: "Does Not Euqal", value: "!=" }, + { label: ">=", value: ">=" }, + { label: "<=", value: "<=" }, + { label: "Is one of (Comma-seperated)", value: "in" }, + ], +}; +const STRING_INPUT = [...MODIFIER_CHOICES.NAME.map((item) => item.value), "in"]; + +export class GroupFilterCondition extends TatorElement { + constructor() { + super(); + + this._div = document.createElement("div"); + this._div.setAttribute( + "class", + "d-flex flex-items-center flex-grow text-gray f2" + ); + this._div.setAttribute( + "style", + "border: 1px solid var(--color-charcoal--light)" + ); + this._shadow.appendChild(this._div); + } + + connectedCallback() { + this._initInputs(); + } + + _initInputs() { + this._categoryInput = document.createElement("enum-input"); + this._categoryInput.setAttribute("class", "col-4"); + this._categoryInput.setAttribute("name", "Category"); + this._categoryInput.setAttribute("style", "margin-left: 15px"); + + this._modifierInput = document.createElement("enum-input"); + this._modifierInput.setAttribute("class", "col-4"); + this._modifierInput.setAttribute("name", "Modifier"); + this._modifierInput.setAttribute("style", "margin-left: 15px"); + + this._valueInput = document.createElement("text-input"); + this._valueInput.setAttribute("class", "col-4"); + this._valueInput.setAttribute("name", "Value"); + this._valueInput.setAttribute("style", "margin-left: 15px"); + + this._deleteConditionButton = document.createElement( + "entity-delete-button" + ); + this._deleteConditionButton.setAttribute( + "style", + "margin-left: 15px; margin-right: 8px" + ); + this._deleteConditionButton.addEventListener( + "click", + this._deleteSelf.bind(this) + ); + + this._div.appendChild(this._categoryInput); + this._div.appendChild(this._modifierInput); + this._div.appendChild(this._valueInput); + this._div.appendChild(this._deleteConditionButton); + + this._categoryInput.choices = CATEGORY_CHOICES; + this._modifierInput.choices = MODIFIER_CHOICES.NAME; + this._valueInput.setAttribute("type", "string"); + + this._categoryInput.addEventListener( + "change", + this._userSelectedCategory.bind(this) + ); + this._modifierInput.addEventListener( + "change", + this._userSelectedModifier.bind(this) + ); + } + + _userSelectedCategory() { + // Remove existing choices + this._modifierInput.clear(); + this._valueInput.setValue(null); + + if (this._categoryInput.getValue() === "groupName") { + this._modifierInput.choices = MODIFIER_CHOICES.NAME; + this._valueInput.setAttribute("type", "string"); + this._valueInput._input.setAttribute("placeholder", ""); + } else { + this._modifierInput.choices = MODIFIER_CHOICES.ID; + this._valueInput.setAttribute("type", "int"); + } + } + _userSelectedModifier() { + // Remove existing choices + this._valueInput.setValue(null); + + if (STRING_INPUT.includes(this._modifierInput.getValue())) { + this._valueInput.setAttribute("type", "string"); + this._valueInput._input.setAttribute("placeholder", ""); + } else { + this._valueInput.setAttribute("type", "int"); + } + } + + _processConditionValue() { + this._valueInput._input.classList.remove("has-border"); + this._valueInput._input.classList.remove("is-invalid"); + + const categoryValue = this._categoryInput.getValue(); + const modifierValue = this._modifierInput.getValue(); + const valueValue = this._valueInput.getValue(); + + if (categoryValue === "groupName") { + if (valueValue) { + return { + category: categoryValue, + modifier: modifierValue, + value: valueValue, + }; + } else { + this._valueInput._input.classList.add("has-border"); + this._valueInput._input.classList.add("is-invalid"); + return null; + } + } + if (categoryValue === "groupId" || categoryValue === "userId") { + if (modifierValue === "in") { + const values = valueValue + .split(",") + .map((val) => parseInt(val)) + .filter((val) => !isNaN(val)); + + if (values.length) { + return { + category: categoryValue, + modifier: modifierValue, + value: values, + }; + } else { + this._valueInput._input.classList.add("has-border"); + this._valueInput._input.classList.add("is-invalid"); + return null; + } + } else { + if (valueValue) { + return { + category: categoryValue, + modifier: modifierValue, + value: valueValue, + }; + } else { + this._valueInput._input.classList.add("has-border"); + this._valueInput._input.classList.add("is-invalid"); + return null; + } + } + } + } + + _deleteSelf() { + this.remove(); + } +} + +customElements.define("group-filter-condition", GroupFilterCondition); diff --git a/ui/src/js/permission-settings/table-view-components/group-table-view-actions.js b/ui/src/js/permission-settings/table-view-components/group-table-view-actions.js new file mode 100644 index 000000000..d57191e4a --- /dev/null +++ b/ui/src/js/permission-settings/table-view-components/group-table-view-actions.js @@ -0,0 +1,179 @@ +import { TableViewActions } from "../components/table-view-actions.js"; +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class GroupTableViewActions extends TableViewActions { + constructor() { + super(); + this.type = "Group"; + + this._buttonsForTable = [this._viewByGroup, this._viewByUser, this._filter]; + this._buttonsForItems = [this._newGroup, this._delete]; + + this._viewByGroup.addEventListener("click", () => { + store.getState().setGroupViewBy("Group"); + }); + this._viewByUser.addEventListener("click", () => { + store.getState().setGroupViewBy("User"); + }); + } + + _init() { + this.styleViewByButtons(store.getState().groupViewBy); + + store.subscribe( + (state) => state.groupViewBy, + this.styleViewByButtons.bind(this) + ); + store.subscribe( + (state) => state.selectedGroupIds, + this._newSelectedGroupIds.bind(this) + ); + store.subscribe( + (state) => state.groupSearchParams, + this._newGroupSearchParams.bind(this) + ); + + this._delete.addEventListener( + "click", + this._openDeleteGroupsModal.bind(this) + ); + + this._filterWindow._addConditionButton.addEventListener( + "click", + this._addCondition.bind(this) + ); + } + + _newGroupSearchParams(groupSearchParams) { + const { groupViewBy } = store.getState(); + // Check if have filters applied + if (groupSearchParams[groupViewBy].filter.length) { + this._filterAppliedSignal.removeAttribute("hidden"); + } else { + this._filterAppliedSignal.setAttribute("hidden", ""); + } + } + + _addCondition() { + const condition = document.createElement("group-filter-condition"); + this._filterWindow._conditionGroup.appendChild(condition); + } + + styleViewByButtons(groupViewBy) { + if (groupViewBy === "Group") { + this._viewByGroup.classList.add("selected"); + this._viewByUser.classList.remove("selected"); + this._delete.style.display = ""; + } else if (groupViewBy === "User") { + this._viewByGroup.classList.remove("selected"); + this._viewByUser.classList.add("selected"); + this._delete.style.display = "none"; + } + } + + _newSelectedGroupIds(selectedGroupIds) { + if (selectedGroupIds.length === 0) { + this._delete.setAttribute("disabled", ""); + } else { + this._delete.removeAttribute("disabled"); + } + } + + setUpWarningDeleteMsg() { + const { + selectedGroupIds, + Group: { map }, + } = store.getState(); + + this._warningDeleteMessage = ` + Pressing confirm will delete these groups:

+ ${selectedGroupIds + .map((id) => { + return `ID: ${id}: ${map.get(id).name}`; + }) + .join("
")} +
+

+ Do you want to continue? + `; + return this._warningDeleteMessage; + } + + async _openDeleteGroupsModal() { + const button = document.createElement("button"); + button.setAttribute("class", "btn btn-clear f1 text-semibold btn-red"); + + let confirmText = document.createTextNode("Confirm"); + button.appendChild(confirmText); + + button.addEventListener("click", this._deleteGroups.bind(this)); + this.setUpWarningDeleteMsg(); + + this.modal._confirm({ + titleText: `Delete Confirmation`, + mainText: this._warningDeleteMessage, + buttonSave: button, + }); + } + + async _deleteGroups() { + const { selectedGroupIds } = store.getState(); + + this.modal._modalCloseAndClear(); + try { + const responses = []; + for (const id of selectedGroupIds) { + const respData = await store.getState().deleteGroup(id); + responses.push(respData); + } + console.log("😇 ~ _deleteGroups ~ responses:", responses); + this.handleResponseList(responses); + + // store.getState.setGroupData(); + } catch (err) { + this.modal._error(err); + } + } + + handleResponseList(responses) { + if (responses && Array.isArray(responses)) { + let sCount = 0; + let eCount = 0; + let errors = ""; + + for (let object of responses) { + if (object.response?.ok) { + sCount++; + } else { + eCount++; + const message = object?.data?.message || ""; + errors += `

${message}`; + } + } + + if (sCount > 0 && eCount === 0) { + this.modal._success( + `Successfully deleted ${sCount} group${sCount == 1 ? "" : "s"}.` + ); + store.getState().setGroupData(); + } else if (sCount > 0 && eCount > 0) { + this.modal._complete( + `Successfully deleted ${sCount} group${ + sCount == 1 ? "" : "s" + }.

+ Error deleting ${eCount} group${eCount == 1 ? "" : "s"}.

+ Error message${eCount == 1 ? "" : "s"}:

${errors}` + ); + store.getState().setGroupData(); + } else { + return this.modal._error( + `Error deleting ${eCount} group${eCount == 1 ? "" : "s"}.

+ Error message${eCount == 1 ? "" : "s"}:

${errors}` + ); + } + } + } +} + +customElements.define("group-table-view-actions", GroupTableViewActions); diff --git a/ui/src/js/permission-settings/table-view-components/group-table-view-table.js b/ui/src/js/permission-settings/table-view-components/group-table-view-table.js new file mode 100644 index 000000000..ccdefd6de --- /dev/null +++ b/ui/src/js/permission-settings/table-view-components/group-table-view-table.js @@ -0,0 +1,381 @@ +import { TableViewTable } from "../components/table-view-table.js"; +import { store } from "../store.js"; + +const COLUMN_BY_GROUP = [ + "Checkbox", + "Group ID", + "Group Name", + "User IDs", + "Actions", +]; +const COLUMN_BY_USER = ["User ID", "Group IDs", "Actions"]; +const COLGROUP_BY_GROUP = ` + + + + + +`; +const COLGROUP_BY_USER = ` + + + +`; + +const INITIAL_SEARCH_PARAMS = { + Group: { + filter: [], + sortBy: { + groupName: "", + groupId: "", + }, + pagination: { + start: 0, + stop: 10, + page: 1, + pageSize: 10, + }, + }, + User: { + filter: [], + sortBy: { + userId: "", + }, + pagination: { + start: 0, + stop: 10, + page: 1, + pageSize: 10, + }, + }, +}; + +export class GroupTableViewTable extends TableViewTable { + constructor() { + super(); + this._checkboxes = []; + } + + connectedCallback() { + store.subscribe( + (state) => state.groupViewBy, + this._changeGroupViewBy.bind(this) + ); + + store.subscribe( + (state) => state.groupSearchParams, + this._newGroupSearchParams.bind(this) + ); + + store.subscribe((state) => state.tabularGroup, this._newData.bind(this)); + } + + _initPaginator() { + this._paginator = document.createElement("entity-gallery-paginator"); + this._paginator.addEventListener("selectPage", this._changePage.bind(this)); + this._paginatorDiv.replaceChildren(this._paginator); + this._paginator.setupElements(); + + store.getState().setGroupSearchParams(INITIAL_SEARCH_PARAMS); + } + + _newGroupSearchParams() { + const { groupViewBy } = store.getState(); + // Count what data should be displayed + store.getState().setTabularGroup(groupViewBy); + } + + _changePage(evt) { + console.log("😇 ~ _changePage ~ evt:", evt); + + const { groupViewBy, groupSearchParams } = store.getState(); + + const newGroupSearchParams = { + ...groupSearchParams, + [groupViewBy]: { + ...groupSearchParams[groupViewBy], + pagination: { ...evt.detail }, + }, + }; + + store.getState().setGroupSearchParams(newGroupSearchParams); + } + + _newData(tabularGroup) { + const { groupViewBy, groupSearchParams } = store.getState(); + + // After we know the total number of data, then we can set up paginator + // Note: not total number of items fetched from DB, but total number of FILTERED items + const numData = tabularGroup[groupViewBy].count; + this._paginator.init(numData, groupSearchParams[groupViewBy].pagination); + + if ( + !tabularGroup?.[groupViewBy]?.data?.length && + !tabularGroup?.[groupViewBy]?.userIdGroupIdMap?.size + ) { + this._tableNoData.classList.remove("hidden"); + this._pagePosition.classList.add("hidden"); + this._table.classList.add("hidden"); + this._paginatorDiv.classList.add("hidden"); + } else { + this._tableNoData.classList.add("hidden"); + this._pagePosition.classList.remove("hidden"); + this._table.classList.remove("hidden"); + this._paginatorDiv.classList.remove("hidden"); + this._displayPagePosition(); + this._displayTable(); + } + } + + _displayPagePosition() { + const { groupViewBy } = store.getState(); + + if (groupViewBy !== "Group" && groupViewBy !== "User") return; + + const { count } = store.getState().tabularGroup[groupViewBy]; + const { page, pageSize } = + store.getState().groupSearchParams[groupViewBy].pagination; + const tatolPageCount = Math.ceil(count / pageSize); + + this._totalItemCount.innerText = count; + this._currentPage.innerText = page; + this._totalPageCount.innerText = tatolPageCount; + } + + _changeGroupViewBy(groupViewBy) { + if (groupViewBy !== "Group" && groupViewBy !== "User") return; + store.getState().setTabularGroup(groupViewBy); + } + + _displayTable(groupViewBy) { + if (!groupViewBy) { + groupViewBy = store.getState().groupViewBy; + } + + if (groupViewBy !== "Group" && groupViewBy !== "User") return; + + this._tableHead.innerHTML = ""; + this._tableBody.innerHTML = ""; + + if (groupViewBy === "Group") { + this._checkboxes = []; + this._colgroup.innerHTML = COLGROUP_BY_GROUP; + this._displayTableByGroup(); + store.getState().setSelectedGroupIds([]); + } else if (groupViewBy === "User") { + this._colgroup.innerHTML = COLGROUP_BY_USER; + this._displayTableByUser(); + } + } + + _displayTableByGroup() { + const { data } = store.getState().tabularGroup.Group; + + // Head + const tr = document.createElement("tr"); + COLUMN_BY_GROUP.map((val) => { + const th = document.createElement("th"); + if (val === "Checkbox") { + const check = document.createElement("checkbox-input"); + check.setAttribute("type", "number"); + check.addEventListener("change", (event) => { + this._changeAllCheckboxes(); + }); + + this._checkAll = check; + th.appendChild(check); + } else if (val === "Group ID" || val === "Group Name") { + this._setupTableHeadCellWithSort(val, th); + } else { + th.innerText = val; + } + return th; + }).forEach((th) => { + tr.appendChild(th); + }); + this._tableHead.appendChild(tr); + + // Body + data.forEach((gr) => { + const tr = document.createElement("tr"); + COLUMN_BY_GROUP.map((val) => { + const td = document.createElement("td"); + if (val === "Checkbox") { + const check = document.createElement("checkbox-input"); + check.setAttribute("type", "number"); + check.setValue({ id: gr.id, checked: false }); + + check.addEventListener("change", (event) => { + this._changeCheckboxes(); + }); + + this._checkboxes.push(check); + td.appendChild(check); + } else if (val === "Group ID") { + td.innerText = gr.id; + } else if (val === "Group Name") { + td.innerText = gr.name; + } else if (val === "User IDs") { + td.classList.add("table-cell-padding"); + td.innerText = gr.members; + } else if (val === "Actions") { + const edit = document.createElement("edit-line-button"); + edit.setAttribute("data-id", `Group-${gr.id}`); + edit.addEventListener("click", this._goTo.bind(this)); + td.appendChild(edit); + } + return td; + }).forEach((td) => { + tr.appendChild(td); + }); + this._tableBody.appendChild(tr); + }); + } + + _displayTableByUser() { + const { userIdGroupIdMap } = store.getState().tabularGroup.User; + + // Head + const tr = document.createElement("tr"); + COLUMN_BY_USER.map((val) => { + const th = document.createElement("th"); + if (val === "User ID") { + this._setupTableHeadCellWithSort(val, th); + } else { + th.innerText = val; + } + return th; + }).forEach((th) => { + tr.appendChild(th); + }); + this._tableHead.appendChild(tr); + // Body + for (let [userId, groupIds] of userIdGroupIdMap) { + const tr = document.createElement("tr"); + COLUMN_BY_USER.map((val) => { + const td = document.createElement("td"); + if (val === "User ID") { + td.innerText = userId; + } else if (val === "Group IDs") { + td.classList.add("table-cell-padding", "long-text-td"); + const span = document.createElement("span"); + span.innerText = groupIds; + td.appendChild(span); + td.setAttribute("data-tooltip", groupIds); + } else if (val === "Actions") { + const edit = document.createElement("edit-line-button"); + edit.setAttribute("data-id", `Group-user${userId}`); + edit.addEventListener("click", this._goTo.bind(this)); + td.appendChild(edit); + } + return td; + }).forEach((td) => { + tr.appendChild(td); + }); + this._tableBody.appendChild(tr); + } + } + + _setupTableHeadCellWithSort(text, th) { + const { groupViewBy, groupSearchParams } = store.getState(); + const { sortBy } = groupSearchParams[groupViewBy]; + + let sortByKey = ""; + if (text === "Group ID") sortByKey = "groupId"; + else if (text === "Group Name") sortByKey = "groupName"; + else sortByKey = "userId"; + + const div = document.createElement("div"); + div.classList.add("d-flex", "flex-row"); + div.style.gap = "5px"; + th.appendChild(div); + const span = document.createElement("span"); + span.innerText = text; + div.appendChild(span); + const sortDiv = document.createElement("div"); + sortDiv.classList.add("d-flex", "flex-row"); + div.appendChild(sortDiv); + const sort = document.createElement("sort-button"); + const sortAscending = document.createElement("sort-ascending-button"); + const sortDescending = document.createElement("sort-descending-button"); + + sort.setAttribute("data-id", `${sortByKey}--`); + sortAscending.setAttribute("data-id", `${sortByKey}--ascending`); + sortDescending.setAttribute("data-id", `${sortByKey}--descending`); + sort.addEventListener("click", this._clickSort.bind(this)); + sortAscending.addEventListener("click", this._clickSort.bind(this)); + sortDescending.addEventListener("click", this._clickSort.bind(this)); + + sortDiv.appendChild(sort); + sortDiv.appendChild(sortAscending); + sortDiv.appendChild(sortDescending); + const sorts = { + "": sort, + ascending: sortAscending, + descending: sortDescending, + }; + + Object.values(sorts).forEach((sort) => sort.setAttribute("hidden", "")); + sorts[sortBy[sortByKey]].removeAttribute("hidden"); + } + + _changeCheckboxes() { + const ids = this._checkboxes + .filter((check) => check.getChecked()) + .map((checked) => checked.getValue()); + + store.getState().setSelectedGroupIds(ids); + } + + _changeAllCheckboxes() { + const val = + store.getState().selectedGroupIds.length < this._checkboxes.length; + this._checkboxes.forEach((check) => { + check._checked = val; + }); + this._checkAll._checked = val; + + this._changeCheckboxes(); + } + + _clickSort(evt) { + const id = evt.target.dataset.id; + + if (id) { + const val = id.split("--"); + if (val.length === 2) { + let newSortVal = ""; + if (!val[1]) { + newSortVal = "ascending"; + } else if (val[1] === "ascending") { + newSortVal = "descending"; + } else if (val[1] === "descending") { + newSortVal = "ascending"; + } + + const { groupViewBy, groupSearchParams } = store.getState(); + const newSortBy = { ...INITIAL_SEARCH_PARAMS[groupViewBy].sortBy }; + newSortBy[val[0]] = newSortVal; + + const newGroupSearchParams = { + ...groupSearchParams, + [groupViewBy]: { + ...groupSearchParams[groupViewBy], + sortBy: { ...newSortBy }, + }, + }; + + store.getState().setGroupSearchParams(newGroupSearchParams); + } + } + } + + _goTo(evt) { + const id = evt.target.dataset.id; + if (id) { + window.location.hash = `#${id}`; + } + } +} + +customElements.define("group-table-view-table", GroupTableViewTable); diff --git a/ui/src/js/permission-settings/table-view-components/policy-filter-condition.js b/ui/src/js/permission-settings/table-view-components/policy-filter-condition.js new file mode 100644 index 000000000..0232e210a --- /dev/null +++ b/ui/src/js/permission-settings/table-view-components/policy-filter-condition.js @@ -0,0 +1,224 @@ +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +const CATEGORY_CHOICES = [ + { label: "Entity Type", value: "entityType" }, + { label: "Entity ID", value: "entityId" }, + { label: "Target Type", value: "targetType" }, + { label: "Target ID", value: "targetId" }, +]; +const MODIFIER_CHOICES = { + TYPE: [ + { label: "Equals", value: "equals" }, + { label: "Does Not Euqal", value: "not equal" }, + ], + ID: [ + { label: "Equals", value: "==" }, + { label: "Does Not Euqal", value: "!=" }, + { label: ">=", value: ">=" }, + { label: "<=", value: "<=" }, + { label: "Is one of (Comma-seperated)", value: "in" }, + ], +}; +const VALUE_CHOICES = { + ENTITY: [ + { label: "User", value: "user" }, + { label: "Group", value: "group" }, + { label: "Organization", value: "organization" }, + ], + TARGET: [ + { label: "Project", value: "project" }, + { label: "Media", value: "media" }, + { label: "Localization", value: "localization" }, + { label: "State", value: "state" }, + { label: "File", value: "file" }, + { label: "Section", value: "section" }, + { label: "Algorithm", value: "algorithm" }, + { label: "Version", value: "version" }, + { label: "Organization", value: "target_organization" }, + { label: "Group", value: "target_group" }, + { label: "Job Cluster", value: "job_cluster" }, + { label: "Bucket", value: "bucket" }, + { label: "Hosted Template", value: "hosted_template" }, + ], +}; + +export class PolicyFilterCondition extends TatorElement { + constructor() { + super(); + + this._div = document.createElement("div"); + this._div.setAttribute( + "class", + "d-flex flex-items-center flex-grow text-gray f2" + ); + this._div.setAttribute( + "style", + "border: 1px solid var(--color-charcoal--light)" + ); + this._shadow.appendChild(this._div); + } + + connectedCallback() { + this._initInputs(); + } + + _initInputs() { + this._categoryInput = document.createElement("enum-input"); + this._categoryInput.setAttribute("class", "col-4"); + this._categoryInput.setAttribute("name", "Category"); + this._categoryInput.setAttribute("style", "margin-left: 15px"); + + this._modifierInput = document.createElement("enum-input"); + this._modifierInput.setAttribute("class", "col-4"); + this._modifierInput.setAttribute("name", "Modifier"); + this._modifierInput.setAttribute("style", "margin-left: 15px"); + + this._typeInput = document.createElement("enum-input"); + this._typeInput.setAttribute("class", "col-4"); + this._typeInput.setAttribute("name", "Value"); + this._typeInput.setAttribute("style", "margin-left: 15px"); + + this._idInput = document.createElement("text-input"); + this._idInput.setAttribute("class", "col-4"); + this._idInput.setAttribute("name", "Value"); + this._idInput.setAttribute("style", "margin-left: 15px"); + this._idInput.setAttribute("hidden", ""); + + this._deleteConditionButton = document.createElement( + "entity-delete-button" + ); + this._deleteConditionButton.setAttribute( + "style", + "margin-left: 15px; margin-right: 8px" + ); + this._deleteConditionButton.addEventListener( + "click", + this._deleteSelf.bind(this) + ); + + this._div.appendChild(this._categoryInput); + this._div.appendChild(this._modifierInput); + this._div.appendChild(this._typeInput); + this._div.appendChild(this._idInput); + this._div.appendChild(this._deleteConditionButton); + + this._categoryInput.choices = CATEGORY_CHOICES; + this._modifierInput.choices = MODIFIER_CHOICES.TYPE; + this._typeInput.choices = VALUE_CHOICES.ENTITY; + + this._categoryInput.addEventListener( + "change", + this._userSelectedCategory.bind(this) + ); + this._modifierInput.addEventListener( + "change", + this._userSelectedModifier.bind(this) + ); + } + + _userSelectedCategory() { + // Remove existing choices + this._modifierInput.clear(); + this._typeInput.clear(); + this._idInput.setValue(null); + + if (this._categoryInput.getValue() === "entityType") { + this._modifierInput.choices = MODIFIER_CHOICES.TYPE; + this._typeInput.removeAttribute("hidden"); + this._typeInput.choices = VALUE_CHOICES.ENTITY; + this._idInput.setAttribute("hidden", ""); + } else if (this._categoryInput.getValue() === "targetType") { + this._modifierInput.choices = MODIFIER_CHOICES.TYPE; + this._typeInput.removeAttribute("hidden"); + this._typeInput.choices = VALUE_CHOICES.TARGET; + this._idInput.setAttribute("hidden", ""); + } else { + this._modifierInput.choices = MODIFIER_CHOICES.ID; + this._idInput.removeAttribute("hidden"); + this._idInput.setAttribute("type", "int"); + this._typeInput.setAttribute("hidden", ""); + } + } + _userSelectedModifier() { + // Remove existing choices + this._typeInput.clear(); + this._idInput.setValue(null); + + if ( + this._categoryInput.getValue() === "entityId" || + this._categoryInput.getValue() === "targetId" + ) { + if (this._modifierInput.getValue() === "in") { + this._idInput.setAttribute("type", "string"); + this._idInput._input.setAttribute("placeholder", ""); + } else { + this._idInput.setAttribute("type", "int"); + } + } + } + + _processConditionValue() { + this._typeInput._select.classList.remove("has-border"); + this._typeInput._select.classList.remove("is-invalid"); + this._idInput._input.classList.remove("has-border"); + this._idInput._input.classList.remove("is-invalid"); + + const categoryValue = this._categoryInput.getValue(); + const modifierValue = this._modifierInput.getValue(); + const typeValue = this._typeInput.getValue(); + const idValue = this._idInput.getValue(); + + if (categoryValue === "entityType" || categoryValue === "targetType") { + if (typeValue) { + return { + category: categoryValue, + modifier: modifierValue, + value: typeValue, + }; + } else { + this._typeInput._select.classList.add("has-border"); + this._typeInput._select.classList.add("is-invalid"); + return null; + } + } + if (categoryValue === "entityId" || categoryValue === "targetId") { + if (modifierValue === "in") { + const values = idValue + .split(",") + .map((val) => parseInt(val)) + .filter((val) => !isNaN(val)); + + if (values.length) { + return { + category: categoryValue, + modifier: modifierValue, + value: values, + }; + } else { + this._idInput._input.classList.add("has-border"); + this._idInput._input.classList.add("is-invalid"); + return null; + } + } else { + if (idValue) { + return { + category: categoryValue, + modifier: modifierValue, + value: idValue, + }; + } else { + this._idInput._input.classList.add("has-border"); + this._idInput._input.classList.add("is-invalid"); + return null; + } + } + } + } + + _deleteSelf() { + this.remove(); + } +} + +customElements.define("policy-filter-condition", PolicyFilterCondition); diff --git a/ui/src/js/permission-settings/table-view-components/policy-table-view-actions.js b/ui/src/js/permission-settings/table-view-components/policy-table-view-actions.js new file mode 100644 index 000000000..28a16a5f2 --- /dev/null +++ b/ui/src/js/permission-settings/table-view-components/policy-table-view-actions.js @@ -0,0 +1,173 @@ +import { TableViewActions } from "../components/table-view-actions.js"; +import { TatorElement } from "../../components/tator-element.js"; +import { store } from "../store.js"; + +export class PolicyTableViewActions extends TableViewActions { + constructor() { + super(); + this.type = "Policy"; + + this._buttonsForTable = [this._filter, this._calculator]; + this._buttonsForItems = [this._newPolicy, this._delete]; + } + + _init() { + store.subscribe( + (state) => state.selectedPolicyIds, + this._newSelectedPolicyIds.bind(this) + ); + store.subscribe( + (state) => state.policySearchParams, + this._newPolicySearchParams.bind(this) + ); + + this._delete.addEventListener( + "click", + this._openDeletePoliciesModal.bind(this) + ); + + this._filterWindow._addConditionButton.addEventListener( + "click", + this._addCondition.bind(this) + ); + } + + _newPolicySearchParams(policySearchParams) { + // Check if have filters applied + if (policySearchParams.filter.length) { + this._filterAppliedSignal.removeAttribute("hidden"); + } else { + this._filterAppliedSignal.setAttribute("hidden", ""); + } + } + + _addCondition() { + const condition = document.createElement("policy-filter-condition"); + this._filterWindow._conditionGroup.appendChild(condition); + } + + _newSelectedPolicyIds(selectedPolicyIds) { + if (selectedPolicyIds.length === 0) { + this._delete.setAttribute("disabled", ""); + } else { + this._delete.removeAttribute("disabled"); + } + } + + setUpWarningDeleteMsg() { + const { + selectedPolicyIds, + Policy: { processedMap }, + Group: { groupIdUserIdMap }, + } = store.getState(); + + // If a policy's entity is group, then deleting it can affect the group's members + const affectedUsers = new Map(); + selectedPolicyIds.forEach((id) => { + const policy = processedMap.get(id); + if (policy.entityType === "group") { + affectedUsers.set(id, groupIdUserIdMap.get(policy.entityId)); + } else { + affectedUsers.set(id, []); + } + }); + + this._warningDeleteMessage = ` + Pressing confirm will delete these policies:


+ ${selectedPolicyIds + .map((id) => { + return `ID: ${id}, ${processedMap.get(id).entityName} against ${ + processedMap.get(id).targetName + }, ${ + affectedUsers.get(id).length + ? ` This can affect User ${affectedUsers.get(id)}` + : "" + }`; + }) + .join("

")} +


+ Do you want to continue? + `; + return this._warningDeleteMessage; + } + + _openDeletePoliciesModal() { + const button = document.createElement("button"); + button.setAttribute("class", "btn btn-clear f1 text-semibold btn-red"); + + let confirmText = document.createTextNode("Confirm"); + button.appendChild(confirmText); + + button.addEventListener("click", this._deletePolicies.bind(this)); + this.setUpWarningDeleteMsg(); + + this.modal._confirm({ + titleText: `Delete Confirmation`, + mainText: this._warningDeleteMessage, + buttonSave: button, + }); + } + + async _deletePolicies() { + const { selectedPolicyIds } = store.getState(); + + this.modal._modalCloseAndClear(); + try { + const responses = []; + for (const id of selectedPolicyIds) { + const respData = await store.getState().deletePolicy(id); + responses.push(respData); + } + this.handleResponseList(responses); + } catch (err) { + this.modal._error(err); + } + } + + handleResponseList(responses) { + if (responses && Array.isArray(responses)) { + let sCount = 0; + let eCount = 0; + let errors = ""; + + for (let object of responses) { + if (object.response?.ok) { + sCount++; + } else { + eCount++; + const message = object?.data?.message || ""; + errors += `

${message}`; + } + } + + if (sCount > 0 && eCount === 0) { + this.modal._success( + `Successfully deleted ${sCount} ${ + sCount == 1 ? "policy" : "policies" + }.` + ); + store.getState().setPolicyData(); + } else if (sCount > 0 && eCount > 0) { + this.modal._complete( + `Successfully deleted ${sCount} ${ + sCount == 1 ? "policy" : "policies" + }.

+ Error deleting ${eCount} ${ + eCount == 1 ? "policy" : "policies" + }.

+ Error message${eCount == 1 ? "" : "s"}:

${errors}` + ); + store.getState().setPolicyData(); + } else { + return this.modal._error( + `Error deleting ${eCount} ${ + eCount == 1 ? "policy" : "policies" + }.

+ Error message${eCount == 1 ? "" : "s"}:

${errors}` + ); + } + } + } +} + +customElements.define("policy-table-view-actions", PolicyTableViewActions); diff --git a/ui/src/js/permission-settings/table-view-components/policy-table-view-table.js b/ui/src/js/permission-settings/table-view-components/policy-table-view-table.js new file mode 100644 index 000000000..c564426fd --- /dev/null +++ b/ui/src/js/permission-settings/table-view-components/policy-table-view-table.js @@ -0,0 +1,282 @@ +import { TableViewTable } from "../components/table-view-table.js"; +import { store } from "../store.js"; + +const COLUMN = ["Checkbox", "Entity", "Target", "Permission", "Actions"]; +const COLGROUP = ` + + + + + +`; + +const INITIAL_SEARCH_PARAMS = { + filter: [], + sortBy: { + entityName: "", + targetName: "", + }, + pagination: { + start: 0, + stop: 10, + page: 1, + pageSize: 10, + }, +}; + +export class PolicyTableViewTable extends TableViewTable { + constructor() { + super(); + this.type = "Policy"; + + this._checkboxes = []; + } + + connectedCallback() { + store.subscribe((state) => state.tabularPolicy, this._newData.bind(this)); + + store.subscribe( + (state) => state.policySearchParams, + this._newPolicySearchParams.bind(this) + ); + } + + _initPaginator() { + this._paginator = document.createElement("entity-gallery-paginator"); + this._paginator.addEventListener("selectPage", this._changePage.bind(this)); + this._paginatorDiv.replaceChildren(this._paginator); + this._paginator.setupElements(); + + store.getState().setPolicySearchParams(INITIAL_SEARCH_PARAMS); + } + + _newPolicySearchParams() { + // Count what data should be displayed + store.getState().setTabularPolicy(); + } + + _changePage(evt) { + console.log("😇 ~ _changePage ~ evt:", evt); + + const { policySearchParams } = store.getState(); + + const newPolicySearchParams = { + ...policySearchParams, + pagination: { ...evt.detail }, + }; + + store.getState().setPolicySearchParams(newPolicySearchParams); + } + + _newData(tabularPolicy) { + const { policySearchParams } = store.getState(); + + // After we know the total number of data, then we can set up paginator + // Note: not total number of items fetched from DB, but total number of FILTERED items + const numData = store.getState().tabularPolicy.count; + this._paginator.init(numData, policySearchParams.pagination); + + if (!tabularPolicy?.data?.length) { + this._tableNoData.classList.remove("hidden"); + this._pagePosition.classList.add("hidden"); + this._table.classList.add("hidden"); + this._paginatorDiv.classList.add("hidden"); + } else { + this._tableNoData.classList.add("hidden"); + this._pagePosition.classList.remove("hidden"); + this._table.classList.remove("hidden"); + this._paginatorDiv.classList.remove("hidden"); + this._displayPagePosition(); + this._displayTable(); + } + } + + _displayPagePosition() { + const { count } = store.getState().tabularPolicy; + const { page, pageSize } = store.getState().policySearchParams.pagination; + const tatolPageCount = Math.ceil(count / pageSize); + + this._totalItemCount.innerText = count; + this._currentPage.innerText = page; + this._totalPageCount.innerText = tatolPageCount; + } + + _displayTable() { + this._tableHead.innerHTML = ""; + this._tableBody.innerHTML = ""; + this._colgroup.innerHTML = COLGROUP; + this._checkboxes = []; + + const { data } = store.getState().tabularPolicy; + + // Head + const tr = document.createElement("tr"); + COLUMN.map((val) => { + const th = document.createElement("th"); + if (val === "Checkbox") { + const check = document.createElement("checkbox-input"); + check.setAttribute("type", "number"); + check.addEventListener("change", (event) => { + this._changeAllCheckboxes(); + }); + + this._checkAll = check; + th.appendChild(check); + } else if (val === "Entity") { + this._setupTableHeadCellWithSort(val, th); + } else if (val === "Target") { + this._setupTableHeadCellWithSort(val, th); + } else { + th.innerText = val; + } + return th; + }).forEach((th) => { + tr.appendChild(th); + }); + this._tableHead.appendChild(tr); + + // Body + data.forEach((policy) => { + const tr = document.createElement("tr"); + COLUMN.map((val) => { + const td = document.createElement("td"); + if (val === "Checkbox") { + const check = document.createElement("checkbox-input"); + check.setAttribute("type", "number"); + check.setValue({ id: policy.id, checked: false }); + + check.addEventListener("change", (event) => { + this._changeCheckboxes(); + }); + + this._checkboxes.push(check); + td.appendChild(check); + } else if (val === "Entity") { + td.innerText = policy.entityName; + } else if (val === "Target") { + td.innerText = policy.targetName; + } else if (val === "Permission") { + td.innerText = policy.permission; + } else { + const div = document.createElement("div"); + div.setAttribute("class", "d-flex flex-row"); + div.style.gap = "5px"; + td.appendChild(div); + + const calculator = document.createElement( + "permission-calculator-button" + ); + calculator.setAttribute("data-id", `Policy-Cal${policy.id}`); + calculator.addEventListener("click", this._goTo.bind(this)); + div.appendChild(calculator); + const edit = document.createElement("edit-line-button"); + edit.setAttribute("data-id", `Policy-${policy.id}`); + edit.addEventListener("click", this._goTo.bind(this)); + div.appendChild(edit); + } + return td; + }).forEach((td) => { + tr.appendChild(td); + }); + this._tableBody.appendChild(tr); + }); + + store.getState().setSelectedPolicyIds([]); + } + + _setupTableHeadCellWithSort(text, th) { + const { + policySearchParams: { sortBy }, + } = store.getState(); + const sortByKey = text === "Entity" ? "entityName" : "targetName"; + + const div = document.createElement("div"); + div.classList.add("d-flex", "flex-row"); + div.style.gap = "5px"; + th.appendChild(div); + const span = document.createElement("span"); + span.innerText = text; + div.appendChild(span); + const sortDiv = document.createElement("div"); + sortDiv.classList.add("d-flex", "flex-row"); + div.appendChild(sortDiv); + const sort = document.createElement("sort-button"); + const sortAscending = document.createElement("sort-ascending-button"); + const sortDescending = document.createElement("sort-descending-button"); + + sort.setAttribute("data-id", `${sortByKey}--`); + sortAscending.setAttribute("data-id", `${sortByKey}--ascending`); + sortDescending.setAttribute("data-id", `${sortByKey}--descending`); + sort.addEventListener("click", this._clickSort.bind(this)); + sortAscending.addEventListener("click", this._clickSort.bind(this)); + sortDescending.addEventListener("click", this._clickSort.bind(this)); + + sortDiv.appendChild(sort); + sortDiv.appendChild(sortAscending); + sortDiv.appendChild(sortDescending); + const sorts = { + "": sort, + ascending: sortAscending, + descending: sortDescending, + }; + Object.values(sorts).forEach((sort) => sort.setAttribute("hidden", "")); + sorts[sortBy[sortByKey]].removeAttribute("hidden"); + } + + _changeCheckboxes() { + const ids = this._checkboxes + .filter((check) => check.getChecked()) + .map((checked) => checked.getValue()); + + store.getState().setSelectedPolicyIds(ids); + } + + _changeAllCheckboxes() { + const val = + store.getState().selectedPolicyIds.length < this._checkboxes.length; + this._checkboxes.forEach((check) => { + check._checked = val; + }); + this._checkAll._checked = val; + + this._changeCheckboxes(); + } + + _clickSort(evt) { + const id = evt.target.dataset.id; + + if (id) { + const val = id.split("--"); + if (val.length === 2) { + let newSortVal = ""; + if (!val[1]) { + newSortVal = "ascending"; + } else if (val[1] === "ascending") { + newSortVal = "descending"; + } else if (val[1] === "descending") { + newSortVal = "ascending"; + } + + const { policySearchParams } = store.getState(); + const newSortBy = { ...INITIAL_SEARCH_PARAMS.sortBy }; + newSortBy[val[0]] = newSortVal; + + const newPolicySearchParams = { + ...policySearchParams, + sortBy: { ...newSortBy }, + }; + + store.getState().setPolicySearchParams(newPolicySearchParams); + } + } + } + + _goTo(evt) { + const id = evt.target.dataset.id; + if (id) { + window.location.hash = `#${id}`; + } + } +} + +customElements.define("policy-table-view-table", PolicyTableViewTable);