web: provide dual-list multiselect with pagination (#8004)

* web: revise css-import-maps to need only a single entry, rather than dual-entry

Given that the difference Vite/Storybook cares about is whether or not there's a
sigil at the end of the CSS string, it seemed silly to require devs to enter
both the raw and sigiled string; just do an in-line text-and-replace.

* web: provide a "select / select all" tool for the dual list multiselect

**This commit**

Provides one of several of the sub-controls needed to make the multi-list multi-select thing work.
This is the simplest control, and I decided to go with it first because it's all presentation; all
it does is show the buttons and send events from those buttons.

A Storybook component is provided to show how well it works.

* web: provide a "select / select all" tool for the dual list multiselect

**This commit**

This commit provides the following new features for dual list multiselect:

- The "available" pane, which has all of the entries that are available to be selected.  Items that
  are already selected will remain, but they're marked with a checkmark and can neither be selected
  or moved.
- The "selected" pane, which has *all* of the entries that have been selected.
- The Pagination control, which in this case only sends an event upstream.

**Plan**:

The plan is to have a master control that marries the available-pane, selected-pane,
select-controls, and pagination-controls into a single component that receives the list of
"currently visible" available entries and keeps the list of "currently selected" entries, as well as
a pass-through for the pagination value that allows it to hide the pagination control if there is
only one page.

A master component *above that* will provide the list of currently visible entries and, at need,
read the value of the master control object for the "selected" list. That component will mostly be
data-only; it's render will probably just be `<slot></slot>`; its duty will be only to map entries
to string keys Lit can use, and to provide the lists we want to provide and the pagination ranges we
want to show.

Some judicious use of grid will allow me size the controls properly with/without the pagination
control.

Status and Title are going to be in the master control.

A <slot> will be provided for Search, but I have no plans to integrate that into this control as of
yet.

There is already a planned fallback control; the multi-select experience on mobile is actually
excellent, and we should exploit that appropriately.

* web: provide a "select / select all" tool for the dual list multiselect

**This commit**

1. Re-arrange the contents of the folder so that the sub-components are in their own folder. This
   reduces the clutter and makes it easier to understand where to look for certain things.
2. Re-arranges the contents of the folder so that all the Storybook stories are in their own folder.
   Again, this reduces the clutter; it also helps the compiler understand what not to compile.
3. Strips down the "Available items pane" to a minimal amount of interactivity and annotates the
   passed-in properties as `readonly`, since the purpose of this component is to display those. The
   only internal state kept is the list of items marked-to-move.
4. Does the same thing with the "Selected items pane".
5. Added comments to help guide future maintainers.
6. Restructured the CSS, taking a _lot_ of it into our own hands. Patternfly continues to act as if
   all components are fully available all the time, and that's simply not true in a shadowDOM
   environment. By separating out the global CSS Custom Properties from the grid and style
   definitions of `pf-c-dual-list-selector`, I was able to construct a more simple and
   straightforward grid (with nested grids for the columns inside).
7. Added "Delete ALL Selected" to the controls
8. Added "double-click" as a "move this one NOW" feature.

* web: provide a "select / select all" tool for the dual list multiselect

**This commit**

- Fixes the bug whereby pagination would leave the 'some moves available' state visible by clearing
  the 'to-move' state when the list of options changes.
- Fixes the bug whereby a change of 'options' in available would also cause an update to
  `selectedKeys`, causing the entire selected field to clear. Fixed by making `selectedKeys` a
  static object updated only when `selected` is generated rather than generating it anew with each
  re-rerender. (Hey, kids, can you say "functional programming and immutability" five time fast? I
  knew you could!)
- Fixes the bug whereby the change of outpost type would not cause an update of the `options`
  collection.
- Fixes the bug whereby the CSS was not creating enough whitespace separation between the whole
  component and its siblings. Host components are coded `span:static` unless otherwise styled to be
  `block`; we want `block` most of the time.
- Fixes the bug whereby the list of existing objects wasn't being passed to the handler correctly.
- Updates the Form Handler to recognize this new input object.
- Fixes the bug whereby changing outpost type doesn't handle the list of selected applications well.
- Fixes the bug whereby the identity of the outpost type's associated `fetch()` function loses
  identity -- necessary to maintain the selected outpost type switch.
- Fixes the CSS bug whereby horizontal scrolling would not enable correctly when the application's
  name overflows the listbox.
- Completes this assignment.  :-)

* web: last-minute pre-commit cleanup.

* running localize extract

* web: codeql found an issue with one of my tests.

* web: multi-select

Modified the display so that if it's a template we display it
correctly opposite the text, and provide classes that can be used
in the display to differentiate between the main label and the
descriptive label.

Added a sort key, so the select can sort the right-hand pane correctly.

Fixed the `this.selected` setters to use Arrays instead of maps.
Theoretically, this is terribly inefficient, as it makes it
theoretically O(n^2) rather than O(1), but in practice even if both
lists were 10,000 elements long a modern desktop could perform the
entire scan in 150ms or so.

* fix lint error

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update strings slightly

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start on dark theme support

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: Add searchbar and enable it for "selected"

"Available" requires a round-trip to the provider level, so that's next.

* web: provide a search for the dual list multiselect

**This commit**

- Includes a new widget that represents the basic, Patternfly-designed search bar.  It just emits
  events of search request updates.
- Changes the definition of a data provider to take an optional search string.
- Changes the handler in the *independent* layer so that it catches search requests and those
  requests work on the "selected" collection.
- Changes the handler of the `authentik` interface layer so that it catches search requests and
  those requests are sent to the data provider.
- Provides a debounce function for the `authentik` interface layer to not hammer the Django instance
  too much.
- Updates the data providers in the example for `OutpostForm` to handle search requests.
- Provides a property in the `authentik` interface layer so that the debounce can be tuned.

* web: always trim the search string passed.

* web: code quality pass, extra comments, pre-commit check.

* Serious (and bizarre) merge bug.  I guess it doesn't like XML that much.

* Attempting to reason with whatever eslint GitHub is using.

* Prettier has opinions.

* Enable better dark mode.

There were two issues: the dark mode didn't reach into the "search"
bar, and there were several hover states that weren't handled well.

This commit handles both.  The color scheme mirrors the one we
currently use, but it's a bit backwards from Patternfly 5.  Dunno
how we're gonna reconcile all that.

* Prettier fixes and locale extraction

* web: update pagination type to use generic, provided type

* web: fixed a few comment typos

* Discordant version numbers for @go-authentik/api were causing build failures.

* What is up with CI/CD?

* web: missed a lint issue that prevented the build from running successfully

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Ken Sternberg
2024-01-25 10:08:00 -08:00
committed by GitHub
parent dcbfe73891
commit 5f1ba45966
46 changed files with 3151 additions and 277 deletions

View File

@ -5,6 +5,9 @@
"packages": { "packages": {
"": { "": {
"name": "@goauthentik/web-tests", "name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^120.0.1"
},
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/eslint-plugin": "^6.19.1",
@ -786,6 +789,11 @@
"node": ">=14.16" "node": ">=14.16"
} }
}, },
"node_modules/@testim/chrome-version": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.4.tgz",
"integrity": "sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g=="
},
"node_modules/@tootallnate/quickjs-emscripten": { "node_modules/@tootallnate/quickjs-emscripten": {
"version": "0.23.0", "version": "0.23.0",
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
@ -885,7 +893,7 @@
"version": "20.7.0", "version": "20.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.7.0.tgz",
"integrity": "sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg==", "integrity": "sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg==",
"dev": true "devOptional": true
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
"version": "2.4.2", "version": "2.4.2",
@ -939,7 +947,6 @@
"version": "2.10.1", "version": "2.10.1",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.1.tgz",
"integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==", "integrity": "sha512-CHzgNU3qYBnp/O4S3yv2tXPlvMTq0YWSTVg2/JYLqWZGHwwgJGAwd00poay/11asPq8wLFwHzubyInqHIFmmiw==",
"dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@ -1725,6 +1732,11 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/available-typed-arrays": { "node_modules/available-typed-arrays": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
@ -1737,6 +1749,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/axios": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.3.tgz",
"integrity": "sha512-fWyNdeawGam70jXSVlKl+SUNVcL6j6W79CuSIPfi6HnDUmSCH6gyUys/HrqHeA/wU0Az41rRgean494d0Jb+ww==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": { "node_modules/b4a": {
"version": "1.6.4", "version": "1.6.4",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
@ -1882,7 +1904,6 @@
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true,
"engines": { "engines": {
"node": "*" "node": "*"
} }
@ -2037,6 +2058,50 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chromedriver": {
"version": "120.0.1",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-120.0.1.tgz",
"integrity": "sha512-ETTJlkibcAmvoKsaEoq2TFqEsJw18N0O9gOQZX6Uv/XoEiOV8p+IZdidMeIRYELWJIgCZESvlOx5d1QVnB4v0w==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",
"axios": "^1.6.0",
"compare-versions": "^6.1.0",
"extract-zip": "^2.0.1",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^1.1.0",
"tcp-port-used": "^1.0.2"
},
"bin": {
"chromedriver": "bin/chromedriver"
},
"engines": {
"node": ">=18"
}
},
"node_modules/chromedriver/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/chromedriver/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/chromium-bidi": { "node_modules/chromium-bidi": {
"version": "0.4.16", "version": "0.4.16",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz",
@ -2197,6 +2262,17 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true "dev": true
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "9.5.0", "version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
@ -2206,6 +2282,11 @@
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
}, },
"node_modules/compare-versions": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz",
"integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg=="
},
"node_modules/compress-commons": { "node_modules/compress-commons": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz",
@ -2353,7 +2434,6 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@ -2408,8 +2488,7 @@
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
"dev": true
}, },
"node_modules/deepmerge-ts": { "node_modules/deepmerge-ts": {
"version": "5.1.0", "version": "5.1.0",
@ -2486,6 +2565,14 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": { "node_modules/dequal": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -2698,7 +2785,6 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"dev": true,
"dependencies": { "dependencies": {
"once": "^1.4.0" "once": "^1.4.0"
} }
@ -3234,7 +3320,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"dependencies": { "dependencies": {
"debug": "^4.1.1", "debug": "^4.1.1",
"get-stream": "^5.1.0", "get-stream": "^5.1.0",
@ -3254,7 +3339,6 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"dependencies": { "dependencies": {
"pump": "^3.0.0" "pump": "^3.0.0"
}, },
@ -3318,7 +3402,6 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dev": true,
"dependencies": { "dependencies": {
"pend": "~1.2.0" "pend": "~1.2.0"
} }
@ -3473,6 +3556,25 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
"dev": true "dev": true
}, },
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -3498,6 +3600,19 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data-encoder": { "node_modules/form-data-encoder": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
@ -4295,6 +4410,14 @@
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==",
"dev": true "dev": true
}, },
"node_modules/ip-regex": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz",
"integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
@ -4587,6 +4710,11 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
},
"node_modules/is-weakref": { "node_modules/is-weakref": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@ -4599,6 +4727,19 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is2": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz",
"integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==",
"dependencies": {
"deep-is": "^0.1.3",
"ip-regex": "^4.1.0",
"is-url": "^1.2.4"
},
"engines": {
"node": ">=v0.10.0"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@ -5546,6 +5687,25 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-fn": { "node_modules/mimic-fn": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@ -5865,8 +6025,7 @@
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true
}, },
"node_modules/mute-stream": { "node_modules/mute-stream": {
"version": "1.0.0", "version": "1.0.0",
@ -6160,7 +6319,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
@ -6496,8 +6654,7 @@
"node_modules/pend": { "node_modules/pend": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
"dev": true
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -6643,14 +6800,12 @@
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"dev": true
}, },
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
"dev": true,
"dependencies": { "dependencies": {
"end-of-stream": "^1.1.0", "end-of-stream": "^1.1.0",
"once": "^1.3.1" "once": "^1.3.1"
@ -8008,6 +8163,31 @@
"streamx": "^2.15.0" "streamx": "^2.15.0"
} }
}, },
"node_modules/tcp-port-used": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
"integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==",
"dependencies": {
"debug": "4.3.1",
"is2": "^2.0.6"
}
},
"node_modules/tcp-port-used/node_modules/debug": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -8839,8 +9019,7 @@
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"dev": true
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.14.2", "version": "8.14.2",
@ -8954,7 +9133,6 @@
"version": "2.10.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dev": true,
"dependencies": { "dependencies": {
"buffer-crc32": "~0.2.3", "buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0" "fd-slicer": "~1.1.0"

View File

@ -30,5 +30,8 @@
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"
},
"dependencies": {
"chromedriver": "^120.0.1"
} }
} }

View File

@ -61,6 +61,9 @@ export const config: Options.Testrunner = {
capabilities: [ capabilities: [
{ {
"browserName": "chrome", "browserName": "chrome",
"wdio:chromedriverOptions": {
binary: "./node_modules/.bin/chromedriver",
},
"goog:chromeOptions": { "goog:chromeOptions": {
args: ["--disable-infobars", "--window-size=1280,800"].concat( args: ["--disable-infobars", "--window-size=1280,800"].concat(
(function () { (function () {

View File

@ -29,6 +29,7 @@ const rawCssImportMaps = [
'import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";', 'import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";',
'import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";', 'import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";',
'import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";', 'import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";',
'import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";',
'import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";', 'import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";',
'import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css";', 'import PFExpandableSection from "@patternfly/patternfly/components/ExpandableSection/expandable-section.css";',
'import PFFAIcons from "@patternfly/patternfly/base/patternfly-fa-icons.css";', 'import PFFAIcons from "@patternfly/patternfly/base/patternfly-fa-icons.css";',

View File

@ -1,7 +1,7 @@
import type { Preview } from "@storybook/web-components"; import type { Preview } from "@storybook/web-components";
import "@goauthentik/common/styles/authentik.css"; import "@goauthentik/common/styles/authentik.css";
import "@goauthentik/common/styles/theme-dark.css"; // import "@goauthentik/common/styles/theme-dark.css";
import "@patternfly/patternfly/components/Brand/brand.css"; import "@patternfly/patternfly/components/Brand/brand.css";
import "@patternfly/patternfly/components/Page/page.css"; import "@patternfly/patternfly/components/Page/page.css";
// .storybook/preview.js // .storybook/preview.js

13
web/package-lock.json generated
View File

@ -85,6 +85,7 @@
"eslint-plugin-lit": "^1.11.0", "eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.23.0", "eslint-plugin-sonarjs": "^0.23.0",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.6.15",
"github-slugger": "^2.0.0",
"lit-analyzer": "^2.0.3", "lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.2.4", "prettier": "^3.2.4",
@ -11825,9 +11826,9 @@
"optional": true "optional": true
}, },
"node_modules/github-slugger": { "node_modules/github-slugger": {
"version": "1.5.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"dev": true "dev": true
}, },
"node_modules/glob": { "node_modules/glob": {
@ -16323,6 +16324,12 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/remark-slug/node_modules/github-slugger": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz",
"integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==",
"dev": true
},
"node_modules/remark-slug/node_modules/mdast-util-to-string": { "node_modules/remark-slug/node_modules/mdast-util-to-string": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz",

View File

@ -110,6 +110,7 @@
"eslint-plugin-lit": "^1.11.0", "eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.23.0", "eslint-plugin-sonarjs": "^0.23.0",
"eslint-plugin-storybook": "^0.6.15", "eslint-plugin-storybook": "^0.6.15",
"github-slugger": "^2.0.0",
"lit-analyzer": "^2.0.3", "lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.2.4", "prettier": "^3.2.4",

View File

@ -77,8 +77,10 @@ const rawCssImportMaps = [
${importLines.map(createOneImportLine).join("\n")} ${importLines.map(createOneImportLine).join("\n")}
]; ];
const cssImportMaps = rawCssImportMaps.reduce((acc, line) => ( const cssImportMaps = rawCssImportMaps.reduce(
{...acc, [line]: line.replace(/\\.css/, ".css?inline")}), {}); (acc, line) => ({ ...acc, [line]: line.replace(/\\.css/, ".css?inline") }),
{},
);
export { cssImportMaps }; export { cssImportMaps };
export default cssImportMaps; export default cssImportMaps;

View File

@ -1,18 +1,22 @@
import { DataProvider, DualSelectPair } from "@goauthentik/app/elements/ak-dual-select/types";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { docLink } from "@goauthentik/common/global"; import { docLink } from "@goauthentik/common/global";
import { groupBy } from "@goauthentik/common/utils"; import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror"; import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror"; import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import YAML from "yaml"; import YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import { map } from "lit/directives/map.js";
import { import {
Outpost, Outpost,
@ -20,14 +24,70 @@ import {
OutpostTypeEnum, OutpostTypeEnum,
OutpostsApi, OutpostsApi,
OutpostsServiceConnectionsAllListRequest, OutpostsServiceConnectionsAllListRequest,
PaginatedLDAPProviderList,
PaginatedProxyProviderList,
PaginatedRACProviderList,
PaginatedRadiusProviderList,
ProvidersApi, ProvidersApi,
ServiceConnection, ServiceConnection,
} from "@goauthentik/api"; } from "@goauthentik/api";
interface ProviderBase {
pk: number;
name: string;
assignedBackchannelApplicationName?: string;
assignedApplicationName?: string;
}
const api = () => new ProvidersApi(DEFAULT_CONFIG);
const providerListArgs = (page: number, search = "") => ({
ordering: "name",
applicationIsnull: false,
pageSize: 20,
search: search.trim(),
page,
});
const dualSelectPairMaker = (item: ProviderBase): DualSelectPair => {
const label = item.assignedBackchannelApplicationName
? item.assignedBackchannelApplicationName
: item.assignedApplicationName;
return [
`${item.pk}`,
html`<div class="selection-main">${label}</div>
<div class="selection-desc">${item.name}</div>`,
label,
];
};
const provisionMaker = (results: PaginatedResponse<ProviderBase>) => ({
pagination: results.pagination,
options: results.results.map(dualSelectPairMaker),
});
const proxyListFetch = async (page: number, search = "") =>
provisionMaker(await api().providersProxyList(providerListArgs(page, search)));
const ldapListFetch = async (page: number, search = "") =>
provisionMaker(await api().providersLdapList(providerListArgs(page, search)));
const radiusListFetch = async (page: number, search = "") =>
provisionMaker(await api().providersRadiusList(providerListArgs(page, search)));
const racListProvider = async (page: number, search = "") =>
provisionMaker(await api().providersRacList(providerListArgs(page, search)));
function providerProvider(type: OutpostTypeEnum): DataProvider {
switch (type) {
case OutpostTypeEnum.Proxy:
return proxyListFetch;
case OutpostTypeEnum.Ldap:
return ldapListFetch;
case OutpostTypeEnum.Radius:
return radiusListFetch;
case OutpostTypeEnum.Rac:
return racListProvider;
default:
throw new Error(`Unrecognized OutputType: ${type}`);
}
}
@customElement("ak-outpost-form") @customElement("ak-outpost-form")
export class OutpostForm extends ModelForm<Outpost, string> { export class OutpostForm extends ModelForm<Outpost, string> {
@property() @property()
@ -37,12 +97,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
embedded = false; embedded = false;
@state() @state()
providers?: providers?: DataProvider;
| PaginatedProxyProviderList
| PaginatedLDAPProviderList
| PaginatedRadiusProviderList
| PaginatedRACProviderList;
defaultConfig?: OutpostDefaultConfig; defaultConfig?: OutpostDefaultConfig;
async loadInstance(pk: string): Promise<Outpost> { async loadInstance(pk: string): Promise<Outpost> {
@ -57,34 +112,7 @@ export class OutpostForm extends ModelForm<Outpost, string> {
this.defaultConfig = await new OutpostsApi( this.defaultConfig = await new OutpostsApi(
DEFAULT_CONFIG, DEFAULT_CONFIG,
).outpostsInstancesDefaultSettingsRetrieve(); ).outpostsInstancesDefaultSettingsRetrieve();
switch (this.type) { this.providers = providerProvider(this.type);
case OutpostTypeEnum.Proxy:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersProxyList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Ldap:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersLdapList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Radius:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersRadiusList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.Rac:
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersRacList({
ordering: "name",
applicationIsnull: false,
});
break;
case OutpostTypeEnum.UnknownDefaultOpenApi:
this.providers = undefined;
}
} }
getSuccessMessage(): string { getSuccessMessage(): string {
@ -107,6 +135,13 @@ export class OutpostForm extends ModelForm<Outpost, string> {
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {
const typeOptions = [
[OutpostTypeEnum.Proxy, msg("Proxy")],
[OutpostTypeEnum.Ldap, msg("LDAP")],
[OutpostTypeEnum.Radius, msg("Radius")],
[OutpostTypeEnum.Rac, msg("RAC")],
];
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name"> return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input <input
type="text" type="text"
@ -124,30 +159,16 @@ export class OutpostForm extends ModelForm<Outpost, string> {
this.load(); this.load();
}} }}
> >
<option ${map(
value=${OutpostTypeEnum.Proxy} typeOptions,
?selected=${this.instance?.type === OutpostTypeEnum.Proxy} ([instanceType, label]) =>
> html` <option
${msg("Proxy")} value=${instanceType}
</option> ?selected=${this.instance?.type === instanceType}
<option >
value=${OutpostTypeEnum.Ldap} ${label}
?selected=${this.instance?.type === OutpostTypeEnum.Ldap} </option>`,
> )}
${msg("LDAP")}
</option>
<option
value=${OutpostTypeEnum.Radius}
?selected=${this.instance?.type === OutpostTypeEnum.Radius}
>
${msg("Radius")}
</option>
<option
value=${OutpostTypeEnum.Rac}
?selected=${this.instance?.type === OutpostTypeEnum.Rac}
>
${msg("RAC")}
</option>
</select> </select>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Integration")} name="serviceConnection"> <ak-form-element-horizontal label=${msg("Integration")} name="serviceConnection">
@ -200,26 +221,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
?required=${!this.embedded} ?required=${!this.embedded}
name="providers" name="providers"
> >
<select class="pf-c-form-control" multiple> <ak-dual-select-provider
${this.providers?.results.map((provider) => { .provider=${this.providers}
const selected = Array.from(this.instance?.providers || []).some((sp) => { .selected=${(this.instance?.providersObj ?? []).map(dualSelectPairMaker)}
return sp == provider.pk; available-label="${msg("Available Applications")}"
}); selected-label="${msg("Selected Applications")}"
let appName = provider.assignedApplicationName; ></ak-dual-select-provider>
if (provider.assignedBackchannelApplicationName) {
appName = provider.assignedBackchannelApplicationName;
}
return html`<option value=${ifDefined(provider.pk)} ?selected=${selected}>
${appName} (${provider.name})
</option>`;
})}
</select>
<p class="pf-c-form__helper-text">
${msg("You can only select providers that match the type of the outpost.")}
</p>
<p class="pf-c-form__helper-text">
${msg("Hold control/command to select multiple items.")}
</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-group aria-label="Advanced settings"> <ak-form-group aria-label="Advanced settings">
<span slot="header"> ${msg("Advanced settings")} </span> <span slot="header"> ${msg("Advanced settings")} </span>

View File

@ -0,0 +1,140 @@
import { AKElement } from "@goauthentik/elements/Base";
import { debounce } from "@goauthentik/elements/utils/debounce";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { PropertyValues, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import type { Pagination } from "@goauthentik/api";
import "./ak-dual-select";
import { AkDualSelect } from "./ak-dual-select";
import type { DataProvider, DualSelectPair } from "./types";
/**
* @element ak-dual-select-provider
*
* A top-level component that understands how the authentik pagination interface works,
* and can provide new pages based upon navigation requests. This is the interface
* between authentik and the generic ak-dual-select component; aside from knowing that
* the Pagination object "looks like Django," the interior components don't know anything
* about authentik at all and could be dropped into Gravity unchanged.)
*
*/
@customElement("ak-dual-select-provider")
export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
/** A function that takes a page and returns the DualSelectPair[] collection with which to update
* the "Available" pane.
*/
@property({ type: Object })
provider!: DataProvider;
@property({ type: Array })
selected: DualSelectPair[] = [];
@property({ attribute: "available-label" })
availableLabel = msg("Available options");
@property({ attribute: "selected-label" })
selectedLabel = msg("Selected options");
/** The remote lists are debounced by definition. This is the interval for the debounce. */
@property({ attribute: "search-delay", type: Number })
searchDelay = 250;
@state()
private options: DualSelectPair[] = [];
private dualSelector: Ref<AkDualSelect> = createRef();
private isLoading = false;
private pagination?: Pagination;
selectedMap: WeakMap<DataProvider, DualSelectPair[]> = new WeakMap();
constructor() {
super();
setTimeout(() => this.fetch(1), 0);
// Notify AkForElementHorizontal how to handle this thing.
this.dataset.akControl = "true";
this.onNav = this.onNav.bind(this);
this.onChange = this.onChange.bind(this);
this.onSearch = this.onSearch.bind(this);
this.addCustomListener("ak-pagination-nav-to", this.onNav);
this.addCustomListener("ak-dual-select-change", this.onChange);
this.addCustomListener("ak-dual-select-search", this.onSearch);
}
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(this.doSearch.bind(this), this.searchDelay);
}
if (changedProperties.has("provider")) {
this.pagination = undefined;
if (changedProperties.get("provider")) {
this.selectedMap.set(changedProperties.get("provider"), this.selected);
this.selected = this.selectedMap.get(this.provider) ?? [];
}
this.fetch();
}
}
async fetch(page?: number, search = "") {
if (this.isLoading) {
return;
}
this.isLoading = true;
const goto = page ?? this.pagination?.current ?? 1;
const data = await this.provider(goto, search);
this.pagination = data.pagination;
this.options = data.options;
this.isLoading = false;
}
onNav(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for navigation, received ${event} instead`);
}
this.fetch(event.detail);
}
onChange(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.selected = event.detail.value;
}
onSearch(event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.doSearch(event.detail);
}
doSearch(search: string) {
this.pagination = undefined;
this.fetch(undefined, search);
}
get value() {
return this.dualSelector.value!.selected.map(([k, _]) => k);
}
render() {
return html`<ak-dual-select
${ref(this.dualSelector)}
.options=${this.options}
.pages=${this.pagination}
.selected=${this.selected}
available-label=${this.availableLabel}
selected-label=${this.selectedLabel}
></ak-dual-select>`;
}
}

View File

@ -0,0 +1,353 @@
import { AKElement } from "@goauthentik/elements/Base";
import {
CustomEmitterElement,
CustomListenerElement,
} from "@goauthentik/elements/utils/eventEmitter";
import { msg, str } from "@lit/localize";
import { PropertyValues, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { globalVariables, mainStyles } from "./components/styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import "./components/ak-dual-select-available-pane";
import { AkDualSelectAvailablePane } from "./components/ak-dual-select-available-pane";
import "./components/ak-dual-select-controls";
import "./components/ak-dual-select-selected-pane";
import { AkDualSelectSelectedPane } from "./components/ak-dual-select-selected-pane";
import "./components/ak-pagination";
import "./components/ak-search-bar";
import {
EVENT_ADD_ALL,
EVENT_ADD_ONE,
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_ONE,
EVENT_REMOVE_SELECTED,
} from "./constants";
import type { BasePagination, DualSelectPair, SearchbarEvent } from "./types";
function alphaSort([_k1, v1, s1]: DualSelectPair, [_k2, v2, s2]: DualSelectPair) {
const [l, r] = [s1 !== undefined ? s1 : v1, s2 !== undefined ? s2 : v2];
return l < r ? -1 : l > r ? 1 : 0;
}
function mapDualPairs(pairs: DualSelectPair[]) {
return new Map(pairs.map(([k, v, _]) => [k, v]));
}
const styles = [PFBase, PFButton, globalVariables, mainStyles];
/**
* @element ak-dual-select
*
* A master (but independent) component that shows two lists-- one of "available options" and one of
* "selected options". The Available Options panel supports pagination if it receives a valid and
* active pagination object (based on Django's pagination object) from the invoking component.
*
* @fires ak-dual-select-change - A custom change event with the current `selected` list.
*/
const keyfinder =
(key: string) =>
([k]: DualSelectPair) =>
k === key;
@customElement("ak-dual-select")
export class AkDualSelect extends CustomEmitterElement(CustomListenerElement(AKElement)) {
static get styles() {
return styles;
}
/* The list of options to *currently* show. Note that this is not *all* the options, only the
* currently shown list of options from a pagination collection. */
@property({ type: Array })
options: DualSelectPair[] = [];
/* The list of options selected. This is the *entire* list and will not be paginated. */
@property({ type: Array })
selected: DualSelectPair[] = [];
@property({ type: Object })
pages?: BasePagination;
@property({ attribute: "available-label" })
availableLabel = msg("Available options");
@property({ attribute: "selected-label" })
selectedLabel = msg("Selected options");
@state()
selectedFilter: string = "";
availablePane: Ref<AkDualSelectAvailablePane> = createRef();
selectedPane: Ref<AkDualSelectSelectedPane> = createRef();
selectedKeys: Set<string> = new Set();
constructor() {
super();
this.handleMove = this.handleMove.bind(this);
this.handleSearch = this.handleSearch.bind(this);
[
EVENT_ADD_ALL,
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_SELECTED,
EVENT_ADD_ONE,
EVENT_REMOVE_ONE,
].forEach((eventName: string) => {
this.addCustomListener(eventName, (event: Event) => this.handleMove(eventName, event));
});
this.addCustomListener("ak-dual-select-move", () => {
this.requestUpdate();
});
this.addCustomListener("ak-search", this.handleSearch);
}
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected")) {
this.selectedKeys = new Set(this.selected.map(([key, _]) => key));
}
// Pagination invalidates available moveables.
if (changedProperties.has("options") && this.availablePane.value) {
this.availablePane.value.clearMove();
}
}
handleMove(eventName: string, event: Event) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expected move event here, got ${eventName}`);
}
switch (eventName) {
case EVENT_ADD_SELECTED: {
this.addSelected();
break;
}
case EVENT_REMOVE_SELECTED: {
this.removeSelected();
break;
}
case EVENT_ADD_ALL: {
this.addAllVisible();
break;
}
case EVENT_REMOVE_ALL: {
this.removeAllVisible();
break;
}
case EVENT_DELETE_ALL: {
this.removeAll();
break;
}
case EVENT_ADD_ONE: {
this.addOne(event.detail);
break;
}
case EVENT_REMOVE_ONE: {
this.removeOne(event.detail);
break;
}
default:
throw new Error(
`AkDualSelect.handleMove received unknown event type: ${eventName}`,
);
}
this.dispatchCustomEvent("ak-dual-select-change", { value: this.value });
event.stopPropagation();
}
addSelected() {
if (this.availablePane.value!.moveable.length === 0) {
return;
}
this.selected = this.availablePane.value!.moveable.reduce(
(acc, key) => {
const value = this.options.find(keyfinder(key));
return value && !acc.find(keyfinder(value[0])) ? [...acc, value] : acc;
},
[...this.selected],
);
// This is where the information gets... lossy. Dammit.
this.availablePane.value!.clearMove();
}
addOne(key: string) {
const requested = this.options.find(keyfinder(key));
if (requested && !this.selected.find(keyfinder(requested[0]))) {
this.selected = [...this.selected, requested];
}
}
// These are the *currently visible* options; the parent node is responsible for paginating and
// updating the list of currently visible options;
addAllVisible() {
// Create a new array of all current options and selected, and de-dupe.
const selected = mapDualPairs([...this.options, ...this.selected]);
this.selected = Array.from(selected.entries());
this.availablePane.value!.clearMove();
}
removeSelected() {
if (this.selectedPane.value!.moveable.length === 0) {
return;
}
const deselected = new Set(this.selectedPane.value!.moveable);
this.selected = this.selected.filter(([key]) => !deselected.has(key));
this.selectedPane.value!.clearMove();
}
removeOne(key: string) {
this.selected = this.selected.filter(([k]) => k !== key);
}
removeAllVisible() {
// Remove all the items from selected that are in the *currently visible* options list
const options = new Set(this.options.map(([k, _]) => k));
this.selected = this.selected.filter(([k]) => !options.has(k));
this.selectedPane.value!.clearMove();
}
removeAll() {
this.selected = [];
this.selectedPane.value!.clearMove();
}
handleSearch(event: SearchbarEvent) {
switch (event.detail.source) {
case "ak-dual-list-available-search":
return this.handleAvailableSearch(event.detail.value);
case "ak-dual-list-selected-search":
return this.handleSelectedSearch(event.detail.value);
}
event.stopPropagation();
}
handleAvailableSearch(value: string) {
this.dispatchCustomEvent("ak-dual-select-search", value);
}
handleSelectedSearch(value: string) {
this.selectedFilter = value;
this.selectedPane.value!.clearMove();
}
get value() {
return this.selected;
}
get canAddAll() {
// False unless any visible option cannot be found in the selected list, so can still be
// added.
const allMoved =
this.options.length ===
this.options.filter(([key, _]) => this.selectedKeys.has(key)).length;
return this.options.length > 0 && !allMoved;
}
get canRemoveAll() {
// False if no visible option can be found in the selected list
return (
this.options.length > 0 && !!this.options.find(([key, _]) => this.selectedKeys.has(key))
);
}
get needPagination() {
return (this.pages?.next ?? 0) > 0 || (this.pages?.previous ?? 0) > 0;
}
render() {
const selected =
this.selectedFilter === ""
? this.selected
: this.selected.filter(([_k, v, s]) => {
const value = s !== undefined ? s : v;
if (typeof value !== "string") {
throw new Error("Filter only works when there's a string comparator");
}
return value.toLowerCase().includes(this.selectedFilter.toLowerCase());
});
const availableCount = this.availablePane.value?.toMove.size ?? 0;
const selectedCount = this.selectedPane.value?.toMove.size ?? 0;
const selectedTotal = selected.length;
const availableStatus =
availableCount > 0 ? msg(str`${availableCount} item(s) marked to add.`) : "&nbsp;";
const selectedTotalStatus = msg(str`${selectedTotal} item(s) selected.`);
const selectedCountStatus =
selectedCount > 0 ? " " + msg(str`${selectedCount} item(s) marked to remove.`) : "";
const selectedStatus = `${selectedTotalStatus} ${selectedCountStatus}`;
return html`
<div class="ak-dual-list-selector">
<div class="ak-available-pane">
<div class="pf-c-dual-list-selector__header">
<div class="pf-c-dual-list-selector__title">
<div class="pf-c-dual-list-selector__title-text">
${this.availableLabel}
</div>
</div>
</div>
<ak-search-bar name="ak-dual-list-available-search"></ak-search-bar>
<div class="pf-c-dual-list-selector__status">
<span
class="pf-c-dual-list-selector__status-text"
id="basic-available-status-text"
>${unsafeHTML(availableStatus)}</span
>
</div>
<ak-dual-select-available-pane
${ref(this.availablePane)}
.options=${this.options}
.selected=${this.selectedKeys}
></ak-dual-select-available-pane>
${this.needPagination
? html`<ak-pagination .pages=${this.pages}></ak-pagination>`
: nothing}
</div>
<ak-dual-select-controls
?add-active=${(this.availablePane.value?.moveable.length ?? 0) > 0}
?remove-active=${(this.selectedPane.value?.moveable.length ?? 0) > 0}
?add-all-active=${this.canAddAll}
?remove-all-active=${this.canRemoveAll}
?delete-all-active=${this.selected.length !== 0}
enable-select-all
enable-delete-all
></ak-dual-select-controls>
<div class="ak-selected-pane">
<div class="pf-c-dual-list-selector__header">
<div class="pf-c-dual-list-selector__title">
<div class="pf-c-dual-list-selector__title-text">
${this.selectedLabel}
</div>
</div>
</div>
<ak-search-bar name="ak-dual-list-selected-search"></ak-search-bar>
<div class="pf-c-dual-list-selector__status">
<span
class="pf-c-dual-list-selector__status-text"
id="basic-available-status-text"
>${unsafeHTML(selectedStatus)}</span
>
</div>
<ak-dual-select-selected-pane
${ref(this.selectedPane)}
.selected=${selected.toSorted(alphaSort)}
></ak-dual-select-selected-pane>
</div>
</div>
`;
}
}

View File

@ -0,0 +1,159 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { availablePaneStyles, listStyles } from "./styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_ADD_ONE } from "../constants";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, availablePaneStyles];
const hostAttributes = [
["aria-labelledby", "dual-list-selector-available-pane-status"],
["aria-multiselectable", "true"],
["role", "listbox"],
];
/**
* @element ak-dual-select-available-panel
*
* The "available options" or "left" pane in a dual-list multi-select. It receives from its parent a
* list of options to show *now*, the list of all "selected" options, and maintains an internal list
* of objects selected to move. "selected" options are marked with a checkmark to show they're
* already in the "selected" collection and would be pointless to move.
*
* @fires ak-dual-select-available-move-changed - When the list of "to move" entries changed.
* Includes the current * `toMove` content.
*
* @fires ak-dual-select-add-one - Double-click with the element clicked on.
*
* It is not expected that the `ak-dual-select-available-move-changed` event will be used; instead,
* the attribute will be read by the parent when a control is clicked.
*
*/
@customElement("ak-dual-select-available-pane")
export class AkDualSelectAvailablePane extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
/* The array of key/value pairs this pane is currently showing */
@property({ type: Array })
readonly options: DualSelectPair[] = [];
/* A set (set being easy for lookups) of keys with all the pairs selected, so that the ones
* currently being shown that have already been selected can be marked and their clicks ignored.
*
*/
@property({ type: Object })
readonly selected: Set<string> = new Set();
/* This is the only mutator for this object. It collects the list of objects the user has
* clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent
* orchestrator for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests.
*
*/
@state()
public toMove: Set<string> = new Set();
constructor() {
super();
this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
}
connectedCallback() {
super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value);
}
});
}
clearMove() {
this.toMove = new Set();
}
onClick(key: string) {
if (this.selected.has(key)) {
return;
}
if (this.toMove.has(key)) {
this.toMove.delete(key);
} else {
this.toMove.add(key);
}
this.dispatchCustomEvent(
"ak-dual-select-available-move-changed",
Array.from(this.toMove.values()).sort(),
);
this.dispatchCustomEvent("ak-dual-select-move");
// Necessary because updating a map won't trigger a state change
this.requestUpdate();
}
onMove(key: string) {
this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_ADD_ONE, key);
this.requestUpdate();
}
get moveable() {
return Array.from(this.toMove.values());
}
// DO NOT use `Array.map()` instead of Lit's `map()` function. Lit's `map()` is object-aware and
// will not re-arrange or reconstruct the list automatically if the actual sources do not
// change; this allows the available pane to illustrate selected items with the checkmark
// without causing the list to scroll back up to the top.
render() {
return html`
<div class="pf-c-dual-list-selector__menu">
<ul class="pf-c-dual-list-selector__list">
${map(this.options, ([key, label]) => {
const selected = classMap({
"pf-m-selected": this.toMove.has(key),
});
return html` <li
class="pf-c-dual-list-selector__list-item"
aria-selected="false"
@click=${() => this.onClick(key)}
@dblclick=${() => this.onMove(key)}
role="option"
data-ak-key=${key}
tabindex="-1"
>
<div class="pf-c-dual-list-selector__list-item-row ${selected}">
<span class="pf-c-dual-list-selector__item">
<span class="pf-c-dual-list-selector__item-main">
<span class="pf-c-dual-list-selector__item-text"
><span>${label}</span>${this.selected.has(key)
? html`<span
class="pf-c-dual-list-selector__item-text-selected-indicator"
><i class="fa fa-check"></i
></span>`
: nothing}</span
></span
></span
>
</div>
</li>`;
})}
</ul>
</div>
`;
}
}
export default AkDualSelectAvailablePane;

View File

@ -0,0 +1,165 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
EVENT_ADD_ALL,
EVENT_ADD_SELECTED,
EVENT_DELETE_ALL,
EVENT_REMOVE_ALL,
EVENT_REMOVE_SELECTED,
} from "../constants";
const styles = [
PFBase,
PFButton,
css`
:host {
align-self: center;
padding-right: var(--pf-c-dual-list-selector__controls--PaddingRight);
padding-left: var(--pf-c-dual-list-selector__controls--PaddingLeft);
}
.pf-c-dual-list-selector {
max-width: 4rem;
}
.ak-dual-list-selector__controls {
display: grid;
justify-content: center;
align-content: center;
height: 100%;
}
`,
];
/**
* @element ak-dual-select-controls
*
* The "control box" for a dual-list multi-select. It's controlled by the parent orchestrator as to
* whether or not any of its controls are enabled. It sends a variety of messages to the parent
* orchestrator which will then reconcile the "available" and "selected" panes at need.
*
*/
@customElement("ak-dual-select-controls")
export class AkDualSelectControls extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
/* Set to true if any *visible* elements can be added to the selected list
*/
@property({ attribute: "add-active", type: Boolean })
addActive = false;
/* Set to true if any elements can be removed from the selected list (essentially,
* if the selected list is not empty)
*/
@property({ attribute: "remove-active", type: Boolean })
removeActive = false;
/* Set to true if *all* the currently visible elements can be moved
* into the selected list (essentially, if any visible elements are
* not currently selected)
*/
@property({ attribute: "add-all-active", type: Boolean })
addAllActive = false;
/* Set to true if *any* of the elements currently visible in the available
* pane are available to be moved to the selected list, enabling that
* all of those specific elements be moved out of the selected list
*/
@property({ attribute: "remove-all-active", type: Boolean })
removeAllActive = false;
/* if deleteAll is enabled, set to true to show that there are elements in the
* selected list that can be deleted.
*/
@property({ attribute: "delete-all-active", type: Boolean })
enableDeleteAll = false;
/* Set to true if you want the `...AllActive` buttons made available. */
@property({ attribute: "enable-select-all", type: Boolean })
selectAll = false;
/* Set to true if you want the `ClearAllSelected` button made available */
@property({ attribute: "enable-delete-all", type: Boolean })
deleteAll = false;
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick(eventName: string) {
this.dispatchCustomEvent(eventName);
}
renderButton(label: string, event: string, active: boolean, direction: string) {
return html`
<div class="pf-c-dual-list-selector__controls-item">
<button
?aria-disabled=${!active}
?disabled=${!active}
aria-label=${label}
class="pf-c-button pf-m-plain"
type="button"
@click=${() => this.onClick(event)}
data-ouia-component-type="AK/Button"
>
<i class="fa ${direction}"></i>
</button>
</div>
</div>`;
}
render() {
return html`
<div class="ak-dual-list-selector__controls">
${this.renderButton(
msg("Add"),
EVENT_ADD_SELECTED,
this.addActive,
"fa-angle-right",
)}
${this.selectAll
? html`
${this.renderButton(
msg("Add All Available"),
EVENT_ADD_ALL,
this.addAllActive,
"fa-angle-double-right",
)}
${this.renderButton(
msg("Remove All Available"),
EVENT_REMOVE_ALL,
this.removeAllActive,
"fa-angle-double-left",
)}
`
: nothing}
${this.renderButton(
msg("Remove"),
EVENT_REMOVE_SELECTED,
this.removeActive,
"fa-angle-left",
)}
${this.deleteAll
? html`${this.renderButton(
msg("Remove All"),
EVENT_DELETE_ALL,
this.enableDeleteAll,
"fa-times",
)}`
: nothing}
</div>
`;
}
}
export default AkDualSelectControls;

View File

@ -0,0 +1,139 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import { listStyles, selectedPaneStyles } from "./styles.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDualListSelector from "@patternfly/patternfly/components/DualListSelector/dual-list-selector.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_REMOVE_ONE } from "../constants";
import type { DualSelectPair } from "../types";
const styles = [PFBase, PFButton, PFDualListSelector, listStyles, selectedPaneStyles];
const hostAttributes = [
["aria-labelledby", "dual-list-selector-selected-pane-status"],
["aria-multiselectable", "true"],
["role", "listbox"],
];
/**
* @element ak-dual-select-available-panel
*
* The "selected options" or "right" pane in a dual-list multi-select. It receives from its parent
* a list of the selected options, and maintains an internal list of objects selected to move.
*
* @fires ak-dual-select-selected-move-changed - When the list of "to move" entries changed.
* Includes the current `toMove` content.
*
* @fires ak-dual-select-remove-one - Double-click with the element clicked on.
*
* It is not expected that the `ak-dual-select-selected-move-changed` will be used; instead, the
* attribute will be read by the parent when a control is clicked.
*
*/
@customElement("ak-dual-select-selected-pane")
export class AkDualSelectSelectedPane extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
/* The array of key/value pairs that are in the selected list. ALL of them. */
@property({ type: Array })
readonly selected: DualSelectPair[] = [];
/*
* This is the only mutator for this object. It collects the list of objects the user has
* clicked on *in this pane*. It is explicitly marked as "public" to emphasize that the parent
* orchestrator for the dual-select widget can and will access it to get the list of keys to be
* moved (removed) if the user so requests.
*
*/
@state()
public toMove: Set<string> = new Set();
constructor() {
super();
this.onClick = this.onClick.bind(this);
this.onMove = this.onMove.bind(this);
}
connectedCallback() {
super.connectedCallback();
hostAttributes.forEach(([attr, value]) => {
if (!this.hasAttribute(attr)) {
this.setAttribute(attr, value);
}
});
}
clearMove() {
this.toMove = new Set();
}
onClick(key: string) {
if (this.toMove.has(key)) {
this.toMove.delete(key);
} else {
this.toMove.add(key);
}
this.dispatchCustomEvent(
"ak-dual-select-selected-move-changed",
Array.from(this.toMove.values()).sort(),
);
this.dispatchCustomEvent("ak-dual-select-move");
// Necessary because updating a map won't trigger a state change
this.requestUpdate();
}
onMove(key: string) {
this.toMove.delete(key);
this.dispatchCustomEvent(EVENT_REMOVE_ONE, key);
this.requestUpdate();
}
get moveable() {
return Array.from(this.toMove.values());
}
render() {
return html`
<div class="pf-c-dual-list-selector__menu">
<ul class="pf-c-dual-list-selector__list">
${map(this.selected, ([key, label]) => {
const selected = classMap({
"pf-m-selected": this.toMove.has(key),
});
return html` <li
class="pf-c-dual-list-selector__list-item"
aria-selected="false"
id="dual-list-selector-basic-selected-pane-list-option-0"
@click=${() => this.onClick(key)}
@dblclick=${() => this.onMove(key)}
role="option"
data-ak-key=${key}
tabindex="-1"
>
<div class="pf-c-dual-list-selector__list-item-row ${selected}">
<span class="pf-c-dual-list-selector__item">
<span class="pf-c-dual-list-selector__item-main">
<span class="pf-c-dual-list-selector__item-text"
>${label}</span
></span
></span
>
</div>
</li>`;
})}
</ul>
</div>
`;
}
}
export default AkDualSelectSelectedPane;

View File

@ -0,0 +1,94 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg, str } from "@lit/localize";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFPagination from "@patternfly/patternfly/components/Pagination/pagination.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { BasePagination } from "../types";
const styles = [
PFBase,
PFButton,
PFPagination,
css`
:host([theme="dark"]) .pf-c-pagination__nav-control .pf-c-button {
color: var(--pf-c-button--m-plain--disabled--Color);
--pf-c-button--disabled--Color: var(--pf-c-button--m-plain--Color);
}
:host([theme="dark"]) .pf-c-pagination__nav-control .pf-c-button:disabled {
color: var(--pf-c-button--disabled--Color);
}
`,
];
@customElement("ak-pagination")
export class AkPagination extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
@property({ attribute: false })
pages?: BasePagination;
constructor() {
super();
this.onClick = this.onClick.bind(this);
}
onClick(nav: number | undefined) {
this.dispatchCustomEvent("ak-pagination-nav-to", nav ?? 0);
}
render() {
return this.pages
? html` <div class="pf-c-pagination pf-m-compact pf-m-hidden pf-m-visible-on-md">
<div
class="pf-c-pagination pf-m-compact pf-m-compact pf-m-hidden pf-m-visible-on-md"
>
<div class="pf-c-options-menu">
<div class="pf-c-options-menu__toggle pf-m-text pf-m-plain">
<span class="pf-c-options-menu__toggle-text">
${msg(
str`${this.pages?.startIndex} - ${this.pages?.endIndex} of ${this.pages?.count}`,
)}
</span>
</div>
</div>
<nav class="pf-c-pagination__nav" aria-label="Pagination">
<div class="pf-c-pagination__nav-control pf-m-prev">
<button
class="pf-c-button pf-m-plain"
@click=${() => {
this.onClick(this.pages?.previous);
}}
?disabled="${(this.pages?.previous ?? 0) < 1}"
aria-label="${msg("Go to previous page")}"
>
<i class="fas fa-angle-left" aria-hidden="true"></i>
</button>
</div>
<div class="pf-c-pagination__nav-control pf-m-next">
<button
class="pf-c-button pf-m-plain"
@click=${() => {
this.onClick(this.pages?.next);
}}
?disabled="${(this.pages?.next ?? 0) <= 0}"
aria-label="${msg("Go to next page")}"
>
<i class="fas fa-angle-right" aria-hidden="true"></i>
</button>
</div>
</nav>
</div>
</div>`
: nothing;
}
}
export default AkPagination;

View File

@ -0,0 +1,69 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import type { Ref } from "lit/directives/ref.js";
import { globalVariables, searchStyles } from "./search.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { SearchbarEvent } from "../types";
const styles = [PFBase, globalVariables, searchStyles];
@customElement("ak-search-bar")
export class AkSearchbar extends CustomEmitterElement(AKElement) {
static get styles() {
return styles;
}
@property({ type: String, reflect: true })
value = "";
/**
* If you're using more than one search, this token can help listeners distinguishing between
* those searches. Lit's own helpers sometimes erase the source and current targets.
*/
@property({ type: String })
name = "";
input: Ref<HTMLInputElement> = createRef();
constructor() {
super();
this.onChange = this.onChange.bind(this);
}
onChange(_event: Event) {
if (this.input.value) {
this.value = this.input.value.value;
}
this.dispatchCustomEvent<SearchbarEvent>("ak-search", {
source: this.name,
value: this.value,
});
}
render() {
return html`
<div class="pf-c-text-input-group">
<div class="pf-c-text-input-group__main pf-m-icon">
<span class="pf-c-text-input-group__text"
><span class="pf-c-text-input-group__icon"
><i class="fa fa-search fa-fw"></i></span
><input
type="search"
class="pf-c-text-input-group__text-input"
${ref(this.input)}
@input=${this.onChange}
value="${this.value}"
/></span>
</div>
</div>
`;
}
}
export default AkSearchbar;

View File

@ -0,0 +1,190 @@
import { css } from "lit";
// The `host` information for the Patternfly dual list selector came with some default settings that
// we do not want in a web component. By isolating what we *really* use into this collection here,
// we get all the benefits of Patternfly without having to wrestle without also having to counteract
// those default settings.
export const globalVariables = css`
:host {
--pf-c-text-input-group--BackgroundColor: var(--pf-global--BackgroundColorg--100);
--pf-c-text-input-group--Color: var(--pf-global--Color--dark-100);
--pf-c-text-input-group__text--before--BorderWidth: var(--pf-global--BorderWidth--sm);
--pf-c-text-input-group__text--before--BorderColor: var(--pf-global--BorderColor--300);
--pf-c-text-input-group__text--after--BorderBottomWidth: var(--pf-global--BorderWidth--sm);
--pf-c-text-input-group__text--after--BorderBottomColor: var(--pf-global--BorderColor--200);
--pf-c-text-input-group--hover__text--after--BorderBottomColor: var(
--pf-global--primary-color--100
);
--pf-c-text-input-group__text--focus-within--after--BorderBottomWidth: var(
--pf-global--BorderWidth--md
);
--pf-c-text-input-group__text--focus-within--after--BorderBottomColor: var(
--pf-global--primary-color--100
);
--pf-c-text-input-group__main--first-child--not--text-input--MarginLeft: var(
--pf-global--spacer--sm
);
--pf-c-text-input-group__main--m-icon__text-input--PaddingLeft: var(
--pf-global--spacer--xl
);
--pf-c-text-input-group__main--RowGap: var(--pf-global--spacer--xs);
--pf-c-text-input-group__main--ColumnGap: var(--pf-global--spacer--sm);
--pf-c-text-input-group--c-chip-group__main--PaddingTop: var(--pf-global--spacer--xs);
--pf-c-text-input-group--c-chip-group__main--PaddingRight: var(--pf-global--spacer--xs);
--pf-c-text-input-group--c-chip-group__main--PaddingBottom: var(--pf-global--spacer--xs);
--pf-c-text-input-group__text-input--PaddingTop: var(--pf-global--spacer--form-element);
--pf-c-text-input-group__text-input--PaddingRight: var(--pf-global--spacer--sm);
--pf-c-text-input-group__text-input--PaddingBottom: var(--pf-global--spacer--form-element);
--pf-c-text-input-group__text-input--PaddingLeft: var(--pf-global--spacer--sm);
--pf-c-text-input-group__text-input--MinWidth: 12ch;
--pf-c-text-input-group__text-input--m-hint--Color: var(--pf-global--Color--dark-200);
--pf-c-text-input-group--placeholder--Color: var(--pf-global--Color--dark-200);
--pf-c-text-input-group__icon--Left: var(--pf-global--spacer--sm);
--pf-c-text-input-group__icon--Color: var(--pf-global--Color--200);
--pf-c-text-input-group__text--hover__icon--Color: var(--pf-global--Color--100);
--pf-c-text-input-group__icon--TranslateY: -50%;
--pf-c-text-input-group__utilities--MarginRight: var(--pf-global--spacer--sm);
--pf-c-text-input-group__utilities--MarginLeft: var(--pf-global--spacer--xs);
--pf-c-text-input-group__utilities--child--MarginLeft: var(--pf-global--spacer--xs);
--pf-c-text-input-group__utilities--c-button--PaddingRight: var(--pf-global--spacer--xs);
--pf-c-text-input-group__utilities--c-button--PaddingLeft: var(--pf-global--spacer--xs);
--pf-c-text-input-group--m-disabled--Color: var(--pf-global--disabled-color--100);
--pf-c-text-input-group--m-disabled--BackgroundColor: var(--pf-global--disabled-color--300);
}
:host([theme="dark"]) {
--pf-c-text-input-group--BackgroundColor: var(--ak-dark-background-light);
--pf-c-text-input-group--Color: var(--ak-dark-foreground);
--pf-c-text-input-group__text--before--BorderColor: var(--ak-dark-background-lighter);
--pf-c-text-input-group__text--before--BorderWidth: 0;
--pf-c-text-input-group--m-disabled--Color: var(--pf-global--disabled-color--300);
--pf-c-text-input-group--m-disabled--BackgroundColor: var(--pf-global--disabled-color--200);
--pf-c-text-input-group__text--before--BorderBottomColor: var(
--pf-global--BorderColor--200
);
}
`;
export const searchStyles = css`
i.fa,
i.fas,
i.far,
i.fal,
i.fab {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}
i.fa-search:before {
content: "\f002";
}
.fa,
.fas {
position: relative;
font-family: "Font Awesome 5 Free";
font-weight: 900;
}
i.fa-fw {
text-align: center;
width: 1.25em;
}
.pf-c-text-input-group {
position: relative;
display: flex;
width: 100%;
color: var(--pf-c-text-input-group--Color, inherit);
background-color: var(--pf-c-text-input-group--BackgroundColor);
}
.pf-c-text-input-group__main {
display: flex;
flex: 1;
flex-wrap: wrap;
gap: var(--pf-c-text-input-group__main--RowGap)
var(--pf-c-text-input-group__main--ColumnGap);
min-width: 0;
}
.pf-c-text-input-group__main.pf-m-icon {
--pf-c-text-input-group__text-input--PaddingLeft: var(
--pf-c-text-input-group__main--m-icon__text-input--PaddingLeft
);
}
.pf-c-text-input-group__text {
display: inline-grid;
grid-template-columns: 1fr;
grid-template-areas: "text-input";
flex: 1;
z-index: 0;
}
.pf-c-text-input-group__text::before {
border-width: var(--pf-c-text-input-group__text--before--BorderWidth);
border-color: var(--pf-c-text-input-group__text--before--BorderColor);
border-bottom-color: var(--pf-c-text-input-group__text--after--BorderBottomColor);
border-bottom-width: var(--pf-c-text-input-group__text--after--BorderBottomWidth);
border-style: solid;
}
.pf-c-text-input-group__text::after {
border-bottom: var(--pf-c-text-input-group__text--after--BorderBottomWidth) solid
var(--pf-c-text-input-group__text--after--BorderBottomColor);
}
.pf-c-text-input-group__text::before,
.pf-c-text-input-group__text::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
pointer-events: none;
content: "";
z-index: 2;
}
.pf-c-text-input-group__icon {
z-index: 4;
position: absolute;
top: 50%;
left: var(--pf-c-text-input-group__icon--Left);
color: var(--pf-c-text-input-group__icon--Color);
transform: translateY(var(--pf-c-text-input-group__icon--TranslateY));
}
.pf-c-text-input-group__text-input,
.pf-c-text-input-group__text-input.pf-m-hint {
grid-area: text-input;
}
.pf-c-text-input-group__text-input {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: relative;
width: 100%;
color: var(--pf-c-text-input-group--Color);
background-color: var(--pf-c-text-input-group--BackgroundColor);
min-width: var(--pf-c-text-input-group__text-input--MinWidth);
padding: var(--pf-c-text-input-group__text-input--PaddingTop)
var(--pf-c-text-input-group__text-input--PaddingRight)
var(--pf-c-text-input-group__text-input--PaddingBottom)
var(--pf-c-text-input-group__text-input--PaddingLeft);
border: 0;
}
`;

View File

@ -0,0 +1,219 @@
import { css } from "lit";
// The `host` information for the Patternfly dual list selector came with some default settings that
// we do not want in a web component. By isolating what we *really* use into this collection here,
// we get all the benefits of Patternfly without having to wrestle without also having to counteract
// those default settings.
export const globalVariables = css`
:host {
--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min: 12.5rem;
--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max: 28.125rem;
--pf-c-dual-list-selector__header--MarginBottom: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__title-text--FontWeight: var(--pf-global--FontWeight--bold);
--pf-c-dual-list-selector__tools--MarginBottom: var(--pf-global--spacer--md);
--pf-c-dual-list-selector__tools-filter--tools-actions--MarginLeft: var(
--pf-global--spacer--sm
);
--pf-c-dual-list-selector__menu--BorderWidth: var(--pf-global--BorderWidth--sm);
--pf-c-dual-list-selector__menu--BorderColor: var(--pf-global--BorderColor--100);
--pf-c-dual-list-selector__menu--MinHeight: 12.5rem;
--pf-c-dual-list-selector__menu--MaxHeight: 20rem;
--pf-c-dual-list-selector__list-item-row--FontSize: var(--pf-global--FontSize--sm);
--pf-c-dual-list-selector__list-item-row--BackgroundColor: transparent;
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
--pf-global--BackgroundColor--light-300
);
--pf-c-dual-list-selector__list-item-row--focus-within--BackgroundColor: var(
--pf-global--BackgroundColor--light-300
);
--pf-c-dual-list-selector__list-item-row--m-selected--BackgroundColor: var(
--pf-global--BackgroundColor--light-300
);
--pf-c-dual-list-selector__list-item--m-ghost-row--BackgroundColor: var(
--pf-global--BackgroundColor--100
);
--pf-c-dual-list-selector__list-item--m-ghost-row--Opacity: 0.4;
--pf-c-dual-list-selector__item--PaddingTop: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item--PaddingRight: var(--pf-global--spacer--md);
--pf-c-dual-list-selector__item--PaddingBottom: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item--PaddingLeft: var(--pf-global--spacer--md);
--pf-c-dual-list-selector__item--m-expandable--PaddingLeft: 0;
--pf-c-dual-list-selector__item--indent--base: calc(
var(--pf-global--spacer--md) + var(--pf-global--spacer--sm) +
var(--pf-c-dual-list-selector__list-item-row--FontSize)
);
--pf-c-dual-list-selector__item--nested-indent--base: calc(
var(--pf-c-dual-list-selector__item--indent--base) - var(--pf-global--spacer--md)
);
--pf-c-dual-list-selector__draggable--item--PaddingLeft: var(--pf-global--spacer--xs);
--pf-c-dual-list-selector__item-text--Color: var(--pf-global--Color--100);
--pf-c-dual-list-selector__list-item-row--m-selected__text--Color: var(
--pf-global--active-color--100
);
--pf-c-dual-list-selector__list-item-row--m-selected__text--FontWeight: var(
--pf-global--FontWeight--bold
);
--pf-c-dual-list-selector__list-item--m-disabled__item-text--Color: var(
--pf-global--disabled-color--100
);
--pf-c-dual-list-selector__status--MarginBottom: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__status-text--FontSize: var(--pf-global--FontSize--sm);
--pf-c-dual-list-selector__status-text--Color: var(--pf-global--Color--200);
--pf-c-dual-list-selector__controls--PaddingRight: var(--pf-global--spacer--md);
--pf-c-dual-list-selector__controls--PaddingLeft: var(--pf-global--spacer--md);
--pf-c-dual-list-selector__item-toggle--PaddingTop: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item-toggle--PaddingRight: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item-toggle--PaddingBottom: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item-toggle--PaddingLeft: var(--pf-global--spacer--md);
--pf-c-dual-list-selector__item-toggle--MarginTop: calc(var(--pf-global--spacer--sm) * -1);
--pf-c-dual-list-selector__item-toggle--MarginBottom: calc(
var(--pf-global--spacer--sm) * -1
);
--pf-c-dual-list-selector__list__list__item-toggle--Left: 0;
--pf-c-dual-list-selector__list__list__item-toggle--TranslateX: -100%;
--pf-c-dual-list-selector__item-check--MarginRight: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item-count--Marginleft: var(--pf-global--spacer--sm);
--pf-c-dual-list-selector__item--c-badge--m-read--BackgroundColor: var(
--pf-global--disabled-color--200
);
--pf-c-dual-list-selector__item-toggle-icon--Rotate: 0;
--pf-c-dual-list-selector__list-item--m-expanded__item-toggle-icon--Rotate: 90deg;
--pf-c-dual-list-selector__item-toggle-icon--Transition: var(--pf-global--Transition);
--pf-c-dual-list-selector__item-toggle-icon--MinWidth: var(
--pf-c-dual-list-selector__list-item-row--FontSize
);
--pf-c-dual-list-selector__list-item--m-disabled__item-toggle-icon--Color: var(
--pf-global--disabled-color--200
);
/* Unique to authentik */
--pf-c-dual-list-selector--selection-desc--FontSize: var(--pf-global--FontSize--xs);
--pf-c-dual-list-selector--selection-desc--Color: var(--pf-global--Color--dark-200);
--pf-c-dual-list-selector__status--top-padding: var(--pf-global--spacer--xs);
--pf-c-dual-list-panels__gap: var(--pf-global--spacer--xs);
}
:host([theme="dark"]) {
--pf-c-dual-list-selector__menu--BorderColor: var(--ak-dark-background-lighter);
--pf-c-dual-list-selector__item-text--Color: var(--ak-dark-foreground);
--pf-c-dual-list-selector__list-item-row--BackgroundColor: var(
--ak-dark-background-light-ish
);
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
--ak-dark-background-lighter;
);
--pf-c-dual-list-selector__list-item-row--hover--BackgroundColor: var(
--pf-global--BackgroundColor--400
);
}
`;
export const mainStyles = css`
:host {
--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--min: 12.5rem;
--pf-c-dual-list-selector--GridTemplateColumns--pane--MinMax--max: 28.125rem;
}
:host {
display: block grid;
}
.pf-c-dual-list-selector__title-text {
font-weight: var(--pf-c-dual-list-selector__title-text--FontWeight);
}
.pf-c-dual-list-selector__status {
padding-top: var(--pf-c-dual-list-selector__status--top-padding);
}
.pf-c-dual-list-selector__status-text {
font-size: var(--pf-c-dual-list-selector__status-text--FontSize);
color: var(--pf-c-dual-list-selector__status-text--Color);
}
.ak-dual-list-selector {
display: grid;
grid-template-columns: minmax(0, 1fr) min-content minmax(0, 1fr);
}
.ak-available-pane,
.ak-selected-pane {
display: grid;
grid-template-rows: auto auto auto 1fr auto;
gap: var(--pf-c-dual-list-panels__gap);
max-width: 100%;
overflow: hidden;
}
ak-dual-select-controls {
height: 100%;
}
`;
export const listStyles = css`
:host {
display: block;
overflow: hidden;
max-width: 100%;
}
.pf-c-dual-list-selector__menu {
max-width: 100%;
height: 100%;
}
.pf-c-dual-list-selector__list {
max-width: 100%;
display: block;
}
.pf-c-dual-list-selector__item {
padding: 0.25rem;
width: auto;
}
.pf-c-dual-list-selector__item-text {
user-select: none;
flex-grow: 0;
}
.pf-c-dual-list-selector__item-text .selection-main {
color: var(--pf-c-dual-list-selector__item-text--Color);
}
.pf-c-dual-list-selector__item-text .selection-main:hover {
color: var(--pf-c-dual-list-selector__item-text--Color);
}
.pf-c-dual-list-selector__item-text .selection-desc {
font-size: var(--pf-c-dual-list-selector--selection-desc--FontSize);
color: var(--pf-c-dual-list-selector--selection-desc--Color);
}
`;
export const selectedPaneStyles = css`
input[type="checkbox"][readonly] {
pointer-events: none;
}
`;
export const availablePaneStyles = css`
.pf-c-dual-list-selector__item-text {
display: grid;
grid-template-columns: 1fr auto;
}
.pf-c-dual-list-selector__item-text .pf-c-dual-list-selector__item-text-selected-indicator {
display: grid;
justify-content: center;
align-content: center;
}
.pf-c-dual-list-selector__item-text i {
display: inline-block;
padding-left: 1rem;
font-weight: 200;
color: var(--pf-c-dual-list-selector--selection-desc--Color);
font-size: var(--pf-global--FontSize--xs);
}
`;

View File

@ -0,0 +1,7 @@
export const EVENT_ADD_SELECTED = "ak-dual-select-add";
export const EVENT_REMOVE_SELECTED = "ak-dual-select-remove";
export const EVENT_ADD_ALL = "ak-dual-select-add-all";
export const EVENT_REMOVE_ALL = "ak-dual-select-remove-all";
export const EVENT_DELETE_ALL = "ak-dual-select-remove-everything";
export const EVENT_ADD_ONE = "ak-dual-select-add-one";
export const EVENT_REMOVE_ONE = "ak-dual-select-remove-one";

View File

@ -0,0 +1,7 @@
import { AkDualSelect } from "./ak-dual-select";
import "./ak-dual-select";
import { AkDualSelectProvider } from "./ak-dual-select-provider";
import "./ak-dual-select-provider";
export { AkDualSelect, AkDualSelectProvider };
export default AkDualSelect;

View File

@ -0,0 +1,115 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { slug } from "github-slugger";
import { TemplateResult, html } from "lit";
import "../components/ak-dual-select-available-pane";
import { AkDualSelectAvailablePane } from "../components/ak-dual-select-available-pane";
import "./sb-host-provider";
const metadata: Meta<AkDualSelectAvailablePane> = {
title: "Elements / Dual Select / Available Items Pane",
component: "ak-dual-select-available-pane",
parameters: {
docs: {
description: {
component: "The vertical panel separating two dual-select elements.",
},
},
},
argTypes: {
options: {
type: "string",
description: "An array of [key, label] pairs of what to show",
},
selected: {
type: "string",
description: "An array of [key] of what has already been selected",
},
toMove: {
type: "string",
description: "An array of items which are to be moved to the receiving pane.",
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
<sb-dual-select-host-provider> ${testItem} </sb-dual-select-host-provider>
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMoveChanged = (result: any) => {
const target = document.querySelector("#action-button-message-pad");
target!.innerHTML = "";
result.detail.forEach((key: string) => {
target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!);
});
};
window.addEventListener("ak-dual-select-available-move-changed", handleMoveChanged);
type Story = StoryObj;
const goodForYou = [
"Apple",
"Arrowroot",
"Artichoke",
"Arugula",
"Asparagus",
"Avocado",
"Bamboo",
"Banana",
"Basil",
"Beet Root",
"Blackberry",
"Blueberry",
"Bok Choy",
"Broccoli",
"Brussels sprouts",
"Cabbage",
"Cantaloupes",
"Carrot",
"Cauliflower",
];
const goodForYouPairs = goodForYou.map((key) => [slug(key), key]);
export const Default: Story = {
render: () =>
container(
html` <ak-dual-select-available-pane
.options=${goodForYouPairs}
></ak-dual-select-available-pane>`,
),
};
const someSelected = new Set([
goodForYouPairs[2][0],
goodForYouPairs[8][0],
goodForYouPairs[14][0],
]);
export const SomeSelected: Story = {
render: () =>
container(
html` <ak-dual-select-available-pane
.options=${goodForYouPairs}
.selected=${someSelected}
></ak-dual-select-available-pane>`,
),
};

View File

@ -0,0 +1,101 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
import "../components/ak-dual-select-controls";
import { AkDualSelectControls } from "../components/ak-dual-select-controls";
const metadata: Meta<AkDualSelectControls> = {
title: "Elements / Dual Select / Control Panel",
component: "ak-dual-select-controls",
parameters: {
docs: {
description: {
component: "The vertical panel separating two dual-select elements.",
},
},
},
argTypes: {
addActive: {
type: "boolean",
description:
"Highlighted if the sample panel has something to move to the result panel.",
},
removeActive: {
type: "boolean",
description:
"Highlighted if the result panel has something to move to the sample panel.",
},
selectAll: {
type: "boolean",
description: "Enable if you want both the 'move all visible' buttons.",
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayMessage = (result: any) => {
const doc = new DOMParser().parseFromString(`<li><i>Event</i>: ${result}</li>`, "text/xml");
const target = document.querySelector("#action-button-message-pad");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.appendChild(doc.firstChild!);
};
window.addEventListener("ak-dual-select-add", () => displayMessage("add"));
window.addEventListener("ak-dual-select-remove", () => displayMessage("remove"));
window.addEventListener("ak-dual-select-add-all", () => displayMessage("add all"));
window.addEventListener("ak-dual-select-remove-all", () => displayMessage("remove all"));
type Story = StoryObj;
export const Default: Story = {
render: () => container(html` <ak-dual-select-controls></ak-dual-select-controls>`),
};
export const AddActive: Story = {
render: () => container(html` <ak-dual-select-controls add-active></ak-dual-select-controls>`),
};
export const RemoveActive: Story = {
render: () =>
container(html` <ak-dual-select-controls remove-active></ak-dual-select-controls>`),
};
export const AddAllActive: Story = {
render: () =>
container(
html` <ak-dual-select-controls
enable-select-all
add-all-active
></ak-dual-select-controls>`,
),
};
export const RemoveAllActive: Story = {
render: () =>
container(
html` <ak-dual-select-controls
enable-select-all
remove-all-active
></ak-dual-select-controls>`,
),
};

View File

@ -0,0 +1,156 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { slug } from "github-slugger";
import { LitElement, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Pagination } from "@goauthentik/api";
import "../ak-dual-select";
import { AkDualSelect } from "../ak-dual-select";
import type { DualSelectPair } from "../types";
const goodForYouRaw = `
Apple, Arrowroot, Artichoke, Arugula, Asparagus, Avocado, Bamboo, Banana, Basil, Beet Root,
Blackberry, Blueberry, Bok Choy, Broccoli, Brussels sprouts, Cabbage, Cantaloupes, Carrot,
Cauliflower, Celery, Chayote, Chives, Cilantro, Coconut, Collard Greens, Corn, Cucumber, Daikon,
Date, Dill, Eggplant, Endive, Fennel, Fig, Garbanzo Bean, Garlic, Ginger, Gourds, Grape, Guava,
Honeydew, Horseradish, Iceberg Lettuce, Jackfruit, Jicama, Kale, Kangkong, Kiwi, Kohlrabi, Leek,
Lentils, Lychee, Macadamia, Mango, Mushroom, Mustard, Nectarine, Okra, Onion, Papaya, Parsley,
Parsley root, Parsnip, Passion Fruit, Peach, Pear, Peas, Peppers, Persimmon, Pimiento, Pineapple,
Plum, Plum, Pomegranate, Potato, Pumpkin, Radicchio, Radish, Raspberry, Rhubarb, Romaine Lettuce,
Rosemary, Rutabaga, Shallot, Soybeans, Spinach, Squash, Strawberries, Sweet potato, Swiss Chard,
Thyme, Tomatillo, Tomato, Turnip, Waterchestnut, Watercress, Watermelon, Yams
`;
const keyToPair = (key: string): DualSelectPair => [slug(key), key];
const goodForYou: DualSelectPair[] = goodForYouRaw
.split("\n")
.join(" ")
.split(",")
.map((a: string) => a.trim())
.map(keyToPair);
const metadata: Meta<AkDualSelect> = {
title: "Elements / Dual Select / Dual Select With Pagination",
component: "ak-dual-select",
parameters: {
docs: {
description: {
component: "The three-panel assembly",
},
},
},
argTypes: {
options: {
type: "string",
description: "An array of [key, label] pairs of what to show",
},
selected: {
type: "string",
description: "An array of [key] of what has already been selected",
},
pages: {
type: "string",
description: "An authentik pagination object.",
},
},
};
export default metadata;
@customElement("ak-sb-fruity")
export class AkSbFruity extends LitElement {
@property({ type: Array })
options: DualSelectPair[] = goodForYou;
@property({ attribute: "page-length", type: Number })
pageLength = 20;
@state()
page: Pagination;
constructor() {
super();
this.page = {
count: this.options.length,
current: 1,
startIndex: 1,
endIndex: this.options.length > this.pageLength ? this.pageLength : this.options.length,
next: this.options.length > this.pageLength ? 2 : 0,
previous: 0,
totalPages: Math.ceil(this.options.length / this.pageLength),
};
this.onNavigation = this.onNavigation.bind(this);
this.addEventListener("ak-pagination-nav-to", this.onNavigation);
}
onNavigation(evt: Event) {
const current: number = (evt as CustomEvent).detail;
const index = current - 1;
if (index * this.pageLength > this.options.length) {
console.warn(
`Attempted to index from ${index} for options length ${this.options.length}`,
);
return;
}
const endCount = this.pageLength * (index + 1);
const endIndex = Math.min(endCount, this.options.length);
this.page = {
...this.page,
current,
startIndex: this.pageLength * index + 1,
endIndex,
next: (index + 1) * this.pageLength > this.options.length ? 0 : current + 1,
previous: index,
};
}
get pageoptions() {
return this.options.slice(
this.pageLength * (this.page.current - 1),
this.pageLength * this.page.current,
);
}
render() {
return html`<ak-dual-select
.options=${this.pageoptions}
.pages=${this.page}
></ak-dual-select>`;
}
}
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<div id="action-button-message-pad" style="margin-top: 1em"></div>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMoveChanged = (result: any) => {
const target = document.querySelector("#action-button-message-pad");
target!.innerHTML = "";
// @ts-ignore
target!.append(result.detail.value.map(([k, _]) => k).join(", "));
};
window.addEventListener("change", handleMoveChanged);
type Story = StoryObj;
export const Default: Story = {
render: () => container(html` <ak-sb-fruity .options=${goodForYou}></ak-sb-fruity>`),
};

View File

@ -0,0 +1,70 @@
import "@goauthentik/elements/messages/MessageContainer";
import { debounce } from "@goauthentik/elements/utils/debounce";
import { Meta, StoryObj } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
import "../components/ak-search-bar";
import { AkSearchbar } from "../components/ak-search-bar";
const metadata: Meta<AkSearchbar> = {
title: "Elements / Dual Select / Search Bar",
component: "ak-dual-select-search",
parameters: {
docs: {
description: {
component: "A search input bar",
},
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<div id="action-button-message-pad" style="margin-top: 1em"></div>
<div id="action-button-message-pad-2" style="margin-top: 1em"></div>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayMessage = (result: any) => {
const doc = new DOMParser().parseFromString(`<p><i>Content</i>: ${result}</p>`, "text/xml");
const target = document.querySelector("#action-button-message-pad");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.replaceChildren(doc.firstChild!);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const displayMessage2 = (result: any) => {
console.log("Huh.");
const doc = new DOMParser().parseFromString(`<p><i>Behavior</i>: ${result}</p>`, "text/xml");
const target = document.querySelector("#action-button-message-pad-2");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
target!.replaceChildren(doc.firstChild!);
};
const displayMessage2b = debounce(displayMessage2, 250);
window.addEventListener("input", (event: Event) => {
const message = (event.target as HTMLInputElement | undefined)?.value ?? "-- undefined --";
displayMessage(message);
displayMessage2b(message);
});
type Story = StoryObj;
export const Default: Story = {
render: () => container(html` <ak-search-bar></ak-search-bar>`),
};

View File

@ -0,0 +1,96 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { slug } from "github-slugger";
import { TemplateResult, html } from "lit";
import "../components/ak-dual-select-selected-pane";
import { AkDualSelectSelectedPane } from "../components/ak-dual-select-selected-pane";
import "./sb-host-provider";
const metadata: Meta<AkDualSelectSelectedPane> = {
title: "Elements / Dual Select / Selected Items Pane",
component: "ak-dual-select-selected-pane",
parameters: {
docs: {
description: {
component: "The vertical panel separating two dual-select elements.",
},
},
},
argTypes: {
// @ts-ignore
options: {
type: "string",
description: "An array of [key, label] pairs of what to show",
},
toMove: {
type: "string",
description: "An array of items which are to be moved to the receiving pane.",
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
<sb-dual-select-host-provider> ${testItem} </sb-dual-select-host-provider>
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMoveChanged = (result: any) => {
const target = document.querySelector("#action-button-message-pad");
target!.innerHTML = "";
result.detail.forEach((key: string) => {
target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!);
});
};
window.addEventListener("ak-dual-select-selected-move-changed", handleMoveChanged);
type Story = StoryObj;
const goodForYou = [
"Apple",
"Arrowroot",
"Artichoke",
"Arugula",
"Asparagus",
"Avocado",
"Bamboo",
"Banana",
"Basil",
"Beet Root",
"Blackberry",
"Blueberry",
"Bok Choy",
"Broccoli",
"Brussels sprouts",
"Cabbage",
"Cantaloupes",
"Carrot",
"Cauliflower",
];
const goodForYouPairs = goodForYou.map((key) => [slug(key), key]);
export const Default: Story = {
render: () =>
container(
html` <ak-dual-select-selected-pane
.selected=${goodForYouPairs}
></ak-dual-select-selected-pane>`,
),
};

View File

@ -0,0 +1,93 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { slug } from "github-slugger";
import { TemplateResult, html } from "lit";
import "../ak-dual-select";
import { AkDualSelect } from "../ak-dual-select";
const metadata: Meta<AkDualSelect> = {
title: "Elements / Dual Select / Dual Select",
component: "ak-dual-select",
parameters: {
docs: {
description: {
component: "The three-panel assembly",
},
},
},
argTypes: {
options: {
type: "string",
description: "An array of [key, label] pairs of what to show",
},
selected: {
type: "string",
description: "An array of [key] of what has already been selected",
},
pages: {
type: "string",
description: "An authentik pagination object.",
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMoveChanged = (result: any) => {
const target = document.querySelector("#action-button-message-pad");
target!.innerHTML = "";
result.detail.value.forEach((key: string) => {
target!.append(new DOMParser().parseFromString(`<li>${key}</li>`, "text/xml").firstChild!);
});
};
window.addEventListener("change", handleMoveChanged);
type Story = StoryObj;
const goodForYou = [
"Apple",
"Arrowroot",
"Artichoke",
"Arugula",
"Asparagus",
"Avocado",
"Bamboo",
"Banana",
"Basil",
"Beet Root",
"Blackberry",
"Blueberry",
"Bok Choy",
"Broccoli",
"Brussels sprouts",
"Cabbage",
"Cantaloupes",
"Carrot",
"Cauliflower",
];
const goodForYouPairs = goodForYou.map((key) => [slug(key), key]);
export const Default: Story = {
render: () => container(html` <ak-dual-select .options=${goodForYouPairs}></ak-dual-select>`),
};

View File

@ -0,0 +1,83 @@
import "@goauthentik/elements/messages/MessageContainer";
import { Meta, StoryObj } from "@storybook/web-components";
import { TemplateResult, html } from "lit";
import "../components/ak-pagination";
import { AkPagination } from "../components/ak-pagination";
const metadata: Meta<AkPagination> = {
title: "Elements / Dual Select / Pagination Control",
component: "ak-pagination",
parameters: {
docs: {
description: {
component: "The Pagination Control",
},
},
},
argTypes: {
pages: {
type: "string",
description: "An authentik Pagination struct",
},
},
};
export default metadata;
const container = (testItem: TemplateResult) =>
html` <div style="background: #fff; padding: 2em">
<style>
li {
display: block;
}
p {
margin-top: 1em;
}
</style>
<ak-message-container></ak-message-container>
${testItem}
<p>Messages received from the button:</p>
<ul id="action-button-message-pad" style="margin-top: 1em"></ul>
</div>`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMoveChanged = (result: any) => {
console.log(result);
const target = document.querySelector("#action-button-message-pad");
target!.append(
new DOMParser().parseFromString(
`<li>Request to move to page ${result.detail}</li>`,
"text/xml",
).firstChild!,
);
};
window.addEventListener("ak-pagination-nav-to", handleMoveChanged);
type Story = StoryObj;
const pages = {
count: 44,
startIndex: 1,
endIndex: 20,
next: 2,
previous: 0,
};
export const Default: Story = {
render: () => container(html` <ak-pagination .pages=${pages}></ak-pagination>`),
};
const morePages = {
count: 86,
startIndex: 21,
endIndex: 40,
next: 3,
previous: 1,
};
export const More: Story = {
render: () => container(html` <ak-pagination .pages=${morePages}></ak-pagination>`),
};

View File

@ -0,0 +1,22 @@
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import { globalVariables } from "../components/styles.css";
/**
* @element sb-dual-select-host-provider
*
* A *very simple* wrapper which provides the CSS Custom Properties used by the components when
* being displayed in Storybook or Vite. Not needed for the parent widget since it provides these by itself.
*/
@customElement("sb-dual-select-host-provider")
export class SbHostProvider extends LitElement {
static get styles() {
return globalVariables;
}
render() {
return html`<slot></slot>`;
}
}

View File

@ -0,0 +1,26 @@
import { TemplateResult } from "lit";
import { Pagination } from "@goauthentik/api";
// Key, Label (string or TemplateResult), (optional) string to sort by. If the sort string is
// missing, it will use the label, which doesn't always work for TemplateResults).
export type DualSelectPair = [string, string | TemplateResult, string?];
export type BasePagination = Pick<
Pagination,
"startIndex" | "endIndex" | "count" | "previous" | "next"
>;
export type DataProvision = {
pagination: Pagination;
options: DualSelectPair[];
};
export type DataProvider = (page: number, search?: string) => Promise<DataProvision>;
export interface SearchbarEvent extends CustomEvent {
detail: {
source: string;
value: string;
};
}

View File

@ -69,7 +69,6 @@ export function serializeForm<T extends KeyUnknown>(
return; return;
} }
// TODO: Tighten up the typing so that we can handle both.
if ("akControl" in element.dataset) { if ("akControl" in element.dataset) {
assignValue(element, (element as unknown as AkControlElement).json(), json); assignValue(element, (element as unknown as AkControlElement).json(), json);
return; return;
@ -79,6 +78,12 @@ export function serializeForm<T extends KeyUnknown>(
if (element.hidden || !inputElement) { if (element.hidden || !inputElement) {
return; return;
} }
if ("akControl" in inputElement.dataset) {
assignValue(element, inputElement.value, json);
return;
}
// Skip elements that are writeOnly where the user hasn't clicked on the value // Skip elements that are writeOnly where the user hasn't clicked on the value
if (element.writeOnly && !element.writeOnlyActivated) { if (element.writeOnly && !element.writeOnlyActivated) {
return; return;

View File

@ -36,6 +36,22 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
* *
*/ */
const isAkControl = (el: unknown): boolean =>
el instanceof HTMLElement &&
"dataset" in el &&
el.dataset instanceof DOMStringMap &&
"akControl" in el.dataset;
const nameables = new Set([
"input",
"textarea",
"select",
"ak-codemirror",
"ak-chip-group",
"ak-search-select",
"ak-radio",
]);
@customElement("ak-form-element-horizontal") @customElement("ak-form-element-horizontal")
export class HorizontalFormElement extends AKElement { export class HorizontalFormElement extends AKElement {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -112,19 +128,18 @@ export class HorizontalFormElement extends AKElement {
}); });
} }
this.querySelectorAll("*").forEach((input) => { this.querySelectorAll("*").forEach((input) => {
switch (input.tagName.toLowerCase()) { if (isAkControl(input) && !input.getAttribute("name")) {
case "input": input.setAttribute("name", this.name);
case "textarea": // This is fine; writeOnly won't apply to anything built this way.
case "select": return;
case "ak-codemirror":
case "ak-chip-group":
case "ak-search-select":
case "ak-radio":
input.setAttribute("name", this.name);
break;
default:
return;
} }
if (nameables.has(input.tagName.toLowerCase())) {
input.setAttribute("name", this.name);
} else {
return;
}
if (this.writeOnly && !this.writeOnlyActivated) { if (this.writeOnly && !this.writeOnlyActivated) {
const i = input as HTMLInputElement; const i = input as HTMLInputElement;
i.setAttribute("hidden", "true"); i.setAttribute("hidden", "true");

View File

@ -0,0 +1,13 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Callback = (...args: any[]) => any;
export function debounce<F extends Callback, T extends object>(callback: F, wait: number) {
let timeout: ReturnType<typeof setTimeout>;
return (...args: Parameters<F>) => {
// @ts-ignore
const context: T = this satisfies object;
if (timeout !== undefined) {
clearTimeout(timeout);
}
timeout = setTimeout(() => callback.apply(context, args), wait);
};
}

View File

@ -9,22 +9,26 @@ export const isCustomEvent = (v: any): v is CustomEvent =>
export function CustomEmitterElement<T extends Constructor<LitElement>>(superclass: T) { export function CustomEmitterElement<T extends Constructor<LitElement>>(superclass: T) {
return class EmmiterElementHandler extends superclass { return class EmmiterElementHandler extends superclass {
// eslint-disable-next-line @typescript-eslint/no-explicit-any dispatchCustomEvent<F extends CustomEvent>(
dispatchCustomEvent(eventName: string, detail: any = {}, options = {}) { eventName: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
detail: any = {},
options = {},
) {
const fullDetail = const fullDetail =
typeof detail === "object" && !Array.isArray(detail) typeof detail === "object" && !Array.isArray(detail)
? { ? {
target: this,
...detail, ...detail,
} }
: detail; : detail;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(eventName, { new CustomEvent(eventName, {
composed: true, composed: true,
bubbles: true, bubbles: true,
...options, ...options,
detail: fullDetail, detail: fullDetail,
}), }) as F,
); );
} }
}; };

View File

@ -4608,10 +4608,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>Die Auswahl einer Integration ermöglicht die Verwaltung des Outposts durch Authentik.</target> <target>Die Auswahl einer Integration ermöglicht die Verwaltung des Outposts durch Authentik.</target>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Sie können nur Anbieter auswählen, die zum Typ des Outposts passen.</target>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
<target>Konfiguration</target> <target>Konfiguration</target>
@ -6311,9 +6307,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -6346,6 +6339,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4841,10 +4841,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>Selecting an integration enables the management of the outpost by authentik.</target> <target>Selecting an integration enables the management of the outpost by authentik.</target>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>You can only select providers that match the type of the outpost.</target>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
<target>Configuration</target> <target>Configuration</target>
@ -6586,9 +6582,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -6621,6 +6614,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4535,10 +4535,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>La selección de una integración permite la gestión del puesto avanzado por authentik.</target> <target>La selección de una integración permite la gestión del puesto avanzado por authentik.</target>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Solo puede seleccionar proveedores que coincidan con el tipo de puesto avanzado.</target>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
<target>Configuración</target> <target>Configuración</target>
@ -6227,9 +6223,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -6262,6 +6255,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="fr" source-language="en" original="lit-localize-inputs" datatype="plaintext"> <file target-language="fr" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body> <body>
<trans-unit id="s4caed5b7a7e5d89b"> <trans-unit id="s4caed5b7a7e5d89b">
@ -608,9 +608,9 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target>
</trans-unit> </trans-unit>
<trans-unit id="saa0e2675da69651b"> <trans-unit id="saa0e2675da69651b">
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source> <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>L'URL &quot; <target>L'URL "
<x id="0" equiv-text="${this.url}"/>&quot; n'a pas été trouvée.</target> <x id="0" equiv-text="${this.url}"/>" n'a pas été trouvée.</target>
</trans-unit> </trans-unit>
<trans-unit id="s58cd9c2fe836d9c6"> <trans-unit id="s58cd9c2fe836d9c6">
@ -1052,8 +1052,8 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target>
</trans-unit> </trans-unit>
<trans-unit id="sa8384c9c26731f83"> <trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source> <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur &quot;.*&quot;. Soyez conscient des possibles implications de sécurité que cela peut avoir.</target> <target>Pour permettre n'importe quelle URI de redirection, définissez cette valeur sur ".*". Soyez conscient des possibles implications de sécurité que cela peut avoir.</target>
</trans-unit> </trans-unit>
<trans-unit id="s55787f4dfcdce52b"> <trans-unit id="s55787f4dfcdce52b">
@ -1625,7 +1625,7 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target>
</trans-unit> </trans-unit>
<trans-unit id="s33ed903c210a6209"> <trans-unit id="s33ed903c210a6209">
<source>Token to authenticate with. Currently only bearer authentication is supported.</source> <source>Token to authenticate with. Currently only bearer authentication is supported.</source>
<target>Jeton d'authentification à utiliser. Actuellement, seule l'authentification &quot;bearer authentication&quot; est prise en charge.</target> <target>Jeton d'authentification à utiliser. Actuellement, seule l'authentification "bearer authentication" est prise en charge.</target>
</trans-unit> </trans-unit>
<trans-unit id="sfc8bb104e2c05af8"> <trans-unit id="sfc8bb104e2c05af8">
@ -1793,8 +1793,8 @@ Il y a <x id="0" equiv-text="${ago}"/> jour(s)</target>
</trans-unit> </trans-unit>
<trans-unit id="sa90b7809586c35ce"> <trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source> <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome &quot;fa-test&quot;.</target> <target>Entrez une URL complète, un chemin relatif ou utilisez 'fa://fa-test' pour utiliser l'icône Font Awesome "fa-test".</target>
</trans-unit> </trans-unit>
<trans-unit id="s0410779cb47de312"> <trans-unit id="s0410779cb47de312">
@ -2887,7 +2887,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s33683c3b1dbaf264"> <trans-unit id="s33683c3b1dbaf264">
<source>To use SSL instead, use 'ldaps://' and disable this option.</source> <source>To use SSL instead, use 'ldaps://' and disable this option.</source>
<target>Pour utiliser SSL à la base, utilisez &quot;ldaps://&quot; et désactviez cette option.</target> <target>Pour utiliser SSL à la base, utilisez "ldaps://" et désactviez cette option.</target>
</trans-unit> </trans-unit>
<trans-unit id="s2221fef80f4753a2"> <trans-unit id="s2221fef80f4753a2">
@ -2976,8 +2976,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s76768bebabb7d543"> <trans-unit id="s76768bebabb7d543">
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source> <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>Champ qui contient les membres d'un groupe. Si vous utilisez le champ &quot;memberUid&quot;, la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...'</target> <target>Champ qui contient les membres d'un groupe. Si vous utilisez le champ "memberUid", la valeur est censée contenir un nom distinctif relatif, par exemple 'memberUid=un-utilisateur' au lieu de 'memberUid=cn=un-utilisateur,ou=groups,...'</target>
</trans-unit> </trans-unit>
<trans-unit id="s026555347e589f0e"> <trans-unit id="s026555347e589f0e">
@ -3272,7 +3272,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s3198c384c2f68b08"> <trans-unit id="s3198c384c2f68b08">
<source>Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually.</source> <source>Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually.</source>
<target>Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID &quot;transient&quot; et que l'utilisateur ne se déconnecte pas manuellement.</target> <target>Moment où les utilisateurs temporaires doivent être supprimés. Cela ne s'applique que si votre IDP utilise le format NameID "transient" et que l'utilisateur ne se déconnecte pas manuellement.</target>
</trans-unit> </trans-unit>
<trans-unit id="sb32e9c1faa0b8673"> <trans-unit id="sb32e9c1faa0b8673">
@ -3440,7 +3440,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s9f8aac89fe318acc"> <trans-unit id="s9f8aac89fe318acc">
<source>Optionally set the 'FriendlyName' value of the Assertion attribute.</source> <source>Optionally set the 'FriendlyName' value of the Assertion attribute.</source>
<target>Indiquer la valeur &quot;FriendlyName&quot; de l'attribut d'assertion (optionnel)</target> <target>Indiquer la valeur "FriendlyName" de l'attribut d'assertion (optionnel)</target>
</trans-unit> </trans-unit>
<trans-unit id="s851c108679653d2a"> <trans-unit id="s851c108679653d2a">
@ -3754,8 +3754,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s7b1fba26d245cb1c"> <trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source> <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à &quot;minutes=5&quot;.</target> <target>En cas d'utilisation d'une solution de journalisation externe pour l'archivage, cette valeur peut être fixée à "minutes=5".</target>
</trans-unit> </trans-unit>
<trans-unit id="s44536d20bb5c8257"> <trans-unit id="s44536d20bb5c8257">
@ -3931,10 +3931,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="sa95a538bfbb86111"> <trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source> <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<target>Êtes-vous sûr de vouloir mettre à jour <target>Êtes-vous sûr de vouloir mettre à jour
<x id="0" equiv-text="${this.objectLabel}"/>&quot; <x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</target> <x id="1" equiv-text="${this.obj?.name}"/>"?</target>
</trans-unit> </trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6"> <trans-unit id="sc92d7cfb6ee1fec6">
@ -5015,8 +5015,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="sdf1d8edef27236f0"> <trans-unit id="sdf1d8edef27236f0">
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source> <source>A "roaming" authenticator, like a YubiKey</source>
<target>Un authentificateur &quot;itinérant&quot;, comme une YubiKey</target> <target>Un authentificateur "itinérant", comme une YubiKey</target>
</trans-unit> </trans-unit>
<trans-unit id="sfffba7b23d8fb40c"> <trans-unit id="sfffba7b23d8fb40c">
@ -5341,7 +5341,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s5170f9ef331949c0"> <trans-unit id="s5170f9ef331949c0">
<source>Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.</source> <source>Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.</source>
<target>Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable &quot;prompt_data&quot;.</target> <target>Afficher des champs de saisie arbitraires à l'utilisateur, par exemple pendant l'inscription. Les données sont enregistrées dans le contexte du flux sous la variable "prompt_data".</target>
</trans-unit> </trans-unit>
<trans-unit id="s36cb242ac90353bc"> <trans-unit id="s36cb242ac90353bc">
@ -5350,10 +5350,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s2d5f69929bb7221d"> <trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source> <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target> <target>
<x id="0" equiv-text="${prompt.name}"/>(&quot; <x id="0" equiv-text="${prompt.name}"/>("
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, de type <x id="1" equiv-text="${prompt.fieldKey}"/>", de type
<x id="2" equiv-text="${prompt.type}"/>)</target> <x id="2" equiv-text="${prompt.type}"/>)</target>
</trans-unit> </trans-unit>
@ -5402,8 +5402,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s1608b2f94fa0dbd4"> <trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source> <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<target>Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de &quot;rester connecté&quot;, ce qui prolongera sa session jusqu'à la durée spécifiée ici.</target> <target>Si défini à une durée supérieure à 0, l'utilisateur aura la possibilité de choisir de "rester connecté", ce qui prolongera sa session jusqu'à la durée spécifiée ici.</target>
</trans-unit> </trans-unit>
<trans-unit id="s542a71bb8f41e057"> <trans-unit id="s542a71bb8f41e057">
@ -6050,11 +6050,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>La sélection d'une intégration permet la gestion de l'avant-poste par authentik.</target> <target>La sélection d'une intégration permet la gestion de l'avant-poste par authentik.</target>
</trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Vous pouvez uniquement sélectionner des fournisseurs qui correspondent au type d'avant-poste.</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
@ -6187,7 +6182,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit> </trans-unit>
<trans-unit id="sa7fcf026bd25f231"> <trans-unit id="sa7fcf026bd25f231">
<source>Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system.</source> <source>Can be in the format of 'unix://' when connecting to a local docker daemon, using 'ssh://' to connect via SSH, or 'https://:2376' when connecting to a remote system.</source>
<target>Peut être au format &quot;unix://&quot; pour une connexion à un service docker local, &quot;ssh://&quot; pour une connexion via SSH, ou &quot;https://:2376&quot; pour une connexion à un système distant.</target> <target>Peut être au format "unix://" pour une connexion à un service docker local, "ssh://" pour une connexion via SSH, ou "https://:2376" pour une connexion à un système distant.</target>
</trans-unit> </trans-unit>
<trans-unit id="saf1d289e3137c2ea"> <trans-unit id="saf1d289e3137c2ea">
@ -7494,7 +7489,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit> </trans-unit>
<trans-unit id="sff0ac1ace2d90709"> <trans-unit id="sff0ac1ace2d90709">
<source>Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).</source> <source>Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).</source>
<target>Utilisez ce fournisseur avec l'option &quot;auth_request&quot; de Nginx ou &quot;forwardAuth&quot; de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, &quot;/outpost.goauthentik.io&quot; doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous).</target> <target>Utilisez ce fournisseur avec l'option "auth_request" de Nginx ou "forwardAuth" de Traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, "/outpost.goauthentik.io" doit être routé vers le poste avancé (lorsque vous utilisez un poste avancé géré, cela est fait pour vous).</target>
</trans-unit> </trans-unit>
<trans-unit id="scb58b8a60cad8762"> <trans-unit id="scb58b8a60cad8762">
<source>Default relay state</source> <source>Default relay state</source>
@ -7904,7 +7899,7 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<target>Utilisateur créé et ajouté au groupe <x id="0" equiv-text="${this.group.name}"/> avec succès</target> <target>Utilisateur créé et ajouté au groupe <x id="0" equiv-text="${this.group.name}"/> avec succès</target>
</trans-unit> </trans-unit>
<trans-unit id="s824e0943a7104668"> <trans-unit id="s824e0943a7104668">
<source>This user will be added to the group &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source> <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<target>Cet utilisateur sera ajouté au groupe &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;.</target> <target>Cet utilisateur sera ajouté au groupe &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;.</target>
</trans-unit> </trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca"> <trans-unit id="s62e7f6ed7d9cb3ca">
@ -8305,10 +8300,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<source>Footer links</source> <source>Footer links</source>
<target>Liens de pied de page</target> <target>Liens de pied de page</target>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
<target>Cette option configure les liens de pied de page sur les pages de l'exécuteur de flux. Doit être une liste JSON valide et peut être utilisée comme suit :</target>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
<target>Conformité RGPD</target> <target>Conformité RGPD</target>
@ -8352,7 +8343,40 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
<target>Utilisateur anonyme</target> <target>Utilisateur anonyme</target>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -6027,11 +6027,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>통합을 선택하면 authentik을 통해 outpost를 관리할 수 있습니다.</target> <target>통합을 선택하면 authentik을 통해 outpost를 관리할 수 있습니다.</target>
</trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Outpost 유형과 일치하는 공급자만 선택할 수 있습니다.</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
@ -8185,9 +8180,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -8220,6 +8212,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6010,11 +6010,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>Het selecteren van een integratie maakt het beheer van de buitenpost mogelijk door authentik.</target> <target>Het selecteren van een integratie maakt het beheer van de buitenpost mogelijk door authentik.</target>
</trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>U kunt alleen providers selecteren die overeenkomen met het type van de buitenpost.</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
@ -8025,9 +8020,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -8060,6 +8052,39 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4704,10 +4704,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>Wybranie integracji umożliwia zarządzanie placówką przez authentik.</target> <target>Wybranie integracji umożliwia zarządzanie placówką przez authentik.</target>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Możesz wybrać tylko tych dostawców, którzy pasują do typu placówki.</target>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
<target>Konfiguracja</target> <target>Konfiguracja</target>
@ -6434,9 +6430,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -6469,6 +6462,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6013,11 +6013,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>Śēĺēćţĩńĝ àń ĩńţēĝŕàţĩōń ēńàƀĺēś ţĥē màńàĝēmēńţ ōƒ ţĥē ōũţƥōśţ ƀŷ àũţĥēńţĩķ.</target> <target>Śēĺēćţĩńĝ àń ĩńţēĝŕàţĩōń ēńàƀĺēś ţĥē màńàĝēmēńţ ōƒ ţĥē ōũţƥōśţ ƀŷ àũţĥēńţĩķ.</target>
</trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Ŷōũ ćàń ōńĺŷ śēĺēćţ ƥŕōvĩďēŕś ţĥàţ màţćĥ ţĥē ţŷƥē ōƒ ţĥē ōũţƥōśţ.</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
@ -8159,9 +8154,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -8195,4 +8187,37 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit> </trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit>
</body></file></xliff> </body></file></xliff>

View File

@ -4528,10 +4528,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>Bir entegrasyon seçilmesi, oentik tarafından üssün yönetimini sağlar.</target> <target>Bir entegrasyon seçilmesi, oentik tarafından üssün yönetimini sağlar.</target>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>Yalnızca üssün türüne uyan sağlayıcıları seçebilirsiniz.</target>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
<target>yapılandırma</target> <target>yapılandırma</target>
@ -6220,9 +6216,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -6255,6 +6248,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -4352,9 +4352,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s9c29565c5ae1cc92"> <trans-unit id="s9c29565c5ae1cc92">
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
</trans-unit> </trans-unit>
@ -5128,9 +5125,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -5164,6 +5158,39 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit> </trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2"> <?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext"> <file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body> <body>
<trans-unit id="s4caed5b7a7e5d89b"> <trans-unit id="s4caed5b7a7e5d89b">
@ -608,9 +608,9 @@
</trans-unit> </trans-unit>
<trans-unit id="saa0e2675da69651b"> <trans-unit id="saa0e2675da69651b">
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source> <source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL &quot; <target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>&quot;。</target> <x id="0" equiv-text="${this.url}"/>"。</target>
</trans-unit> </trans-unit>
<trans-unit id="s58cd9c2fe836d9c6"> <trans-unit id="s58cd9c2fe836d9c6">
@ -1052,8 +1052,8 @@
</trans-unit> </trans-unit>
<trans-unit id="sa8384c9c26731f83"> <trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source> <source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target> <target>要允许任何重定向 URI请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
</trans-unit> </trans-unit>
<trans-unit id="s55787f4dfcdce52b"> <trans-unit id="s55787f4dfcdce52b">
@ -1794,8 +1794,8 @@
</trans-unit> </trans-unit>
<trans-unit id="sa90b7809586c35ce"> <trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source> <source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target> <target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
</trans-unit> </trans-unit>
<trans-unit id="s0410779cb47de312"> <trans-unit id="s0410779cb47de312">
@ -2978,8 +2978,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s76768bebabb7d543"> <trans-unit id="s76768bebabb7d543">
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source> <source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target> <target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
</trans-unit> </trans-unit>
<trans-unit id="s026555347e589f0e"> <trans-unit id="s026555347e589f0e">
@ -3756,8 +3756,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s7b1fba26d245cb1c"> <trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source> <source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</target> <target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
</trans-unit> </trans-unit>
<trans-unit id="s44536d20bb5c8257"> <trans-unit id="s44536d20bb5c8257">
@ -3933,10 +3933,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="sa95a538bfbb86111"> <trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source> <source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<target>您确定要更新 <target>您确定要更新
<x id="0" equiv-text="${this.objectLabel}"/>&quot; <x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target> <x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
</trans-unit> </trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6"> <trans-unit id="sc92d7cfb6ee1fec6">
@ -5017,7 +5017,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="sdf1d8edef27236f0"> <trans-unit id="sdf1d8edef27236f0">
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source> <source>A "roaming" authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target> <target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit> </trans-unit>
@ -5352,10 +5352,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s2d5f69929bb7221d"> <trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source> <source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target> <target>
<x id="0" equiv-text="${prompt.name}"/>&quot; <x id="0" equiv-text="${prompt.name}"/>"
<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;,类型为 <x id="1" equiv-text="${prompt.fieldKey}"/>",类型为
<x id="2" equiv-text="${prompt.type}"/></target> <x id="2" equiv-text="${prompt.type}"/></target>
</trans-unit> </trans-unit>
@ -5404,7 +5404,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit> </trans-unit>
<trans-unit id="s1608b2f94fa0dbd4"> <trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source> <source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target> <target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit> </trans-unit>
@ -6052,11 +6052,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>选择集成使 authentik 能够管理前哨。</target> <target>选择集成使 authentik 能够管理前哨。</target>
</trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>您只能选择与前哨类型匹配的提供程序。</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
@ -7906,7 +7901,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target> <target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
</trans-unit> </trans-unit>
<trans-unit id="s824e0943a7104668"> <trans-unit id="s824e0943a7104668">
<source>This user will be added to the group &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source> <source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target> <target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target>
</trans-unit> </trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca"> <trans-unit id="s62e7f6ed7d9cb3ca">
@ -8307,10 +8302,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Footer links</source> <source>Footer links</source>
<target>页脚链接</target> <target>页脚链接</target>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
<target>此选项配置流程执行器页面上的页脚链接。必须为有效的 JSON 列表,可以使用以下值:</target>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
<target>GDPR 合规性</target> <target>GDPR 合规性</target>
@ -8354,7 +8345,40 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
<target>匿名用户</target> <target>匿名用户</target>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View File

@ -4570,10 +4570,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>选择集成可以使authentik对 Outpost 进行管理。</target> <target>选择集成可以使authentik对 Outpost 进行管理。</target>
</trans-unit> </trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>您只能选择与 Outpost 类型匹配的提供商。</target>
</trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
<target>配置</target> <target>配置</target>
@ -6268,9 +6264,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -6303,6 +6296,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6001,11 +6001,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>Selecting an integration enables the management of the outpost by authentik.</source> <source>Selecting an integration enables the management of the outpost by authentik.</source>
<target>選擇一個整合讓 authentik 對 Outpost 進行管理。</target> <target>選擇一個整合讓 authentik 對 Outpost 進行管理。</target>
</trans-unit>
<trans-unit id="s554ce268e9727e79">
<source>You can only select providers that match the type of the outpost.</source>
<target>您只能選擇與 Outpost 類型相符的供應商。</target>
</trans-unit> </trans-unit>
<trans-unit id="sf9b1c0661a02d9f9"> <trans-unit id="sf9b1c0661a02d9f9">
<source>Configuration</source> <source>Configuration</source>
@ -8144,9 +8139,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s57b52b60ed5e2bc7"> <trans-unit id="s57b52b60ed5e2bc7">
<source>Footer links</source> <source>Footer links</source>
</trans-unit> </trans-unit>
<trans-unit id="s7349802b2f7f99c2">
<source>This option configures the footer links on the flow executor pages. It must be a valid JSON list and can be used as follows:</source>
</trans-unit>
<trans-unit id="s166b59f3cc5d8ec3"> <trans-unit id="s166b59f3cc5d8ec3">
<source>GDPR compliance</source> <source>GDPR compliance</source>
</trans-unit> </trans-unit>
@ -8179,6 +8171,39 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sa65e7bc7ddd3484d"> <trans-unit id="sa65e7bc7ddd3484d">
<source>Anonymous user</source> <source>Anonymous user</source>
</trans-unit>
<trans-unit id="s0db494ff61b48438">
<source>Add All Available</source>
</trans-unit>
<trans-unit id="s2f68faa5b84ee9fd">
<source>Remove All Available</source>
</trans-unit>
<trans-unit id="s0ba85594344a5c02">
<source>Remove All</source>
</trans-unit>
<trans-unit id="s1efa17e3407e0f56">
<source>Available options</source>
</trans-unit>
<trans-unit id="s886db4d476f66522">
<source>Selected options</source>
</trans-unit>
<trans-unit id="sd429eba21dd59f44">
<source><x id="0" equiv-text="${availableCount}"/> item(s) marked to add.</source>
</trans-unit>
<trans-unit id="s4abfe4e0d0665fd1">
<source><x id="0" equiv-text="${selectedTotal}"/> item(s) selected.</source>
</trans-unit>
<trans-unit id="s56b3d54118c34b2f">
<source><x id="0" equiv-text="${selectedCount}"/> item(s) marked to remove.</source>
</trans-unit>
<trans-unit id="scb76a63c68b0d81f">
<source>Available Applications</source>
</trans-unit>
<trans-unit id="sd176021da2ea0fe3">
<source>Selected Applications</source>
</trans-unit>
<trans-unit id="s862505f29064fc72">
<source>This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>