220378b3f2bc7bf503b595b275e82f025e763ec7
5 Commits
Author | SHA1 | Message | Date | |
---|---|---|---|---|
220378b3f2 | web: Fix TypeScript compilation issues for mixins, events. (#13766) | |||
5f1ba45966 |
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> |
|||
3b171a02b7 |
web: laying the groundwork for future expansion (#7045)
* web: laying the groundwork for future expansion This commit is a hodge-podge of updates and changes to the web. Functional changes: - Makefile: Fixed a bug in the `help` section that prevented the WIDTH from being accurately calculated if `help` was included rather than in-lined. - ESLint: Modified the "unused vars" rule so that variables starting with an underline are not considered by the rule. This allows for elided variables in event handlers. It's not a perfect solution-- a better one would be to use Typescript's function-specialization typing, but there are too many places where we elide or ignore some variables in a function's usage that switching over to specialization would be a huge lift. - locale: It turns out, lit-locale does its own context management. We don't need to have a context at all in this space, and that's one less listener we need to attach t othe DOM. - ModalButton: A small thing, but using `nothing` instead of "html``" allows lit better control over rendering and reduces the number of actual renders of the page. - FormGroup: Provided a means to modify the aria-label, rather than stick with the just the word "Details." Specializing this field will both help users of screen readers in the future, and will allow test suites to find specific form groups now. - RadioButton: provide a more consistent interface to the RadioButton. First, we dispatch the events to the outside world, and we set the value locally so that the current `Form.ts` continues to behave as expected. We also prevent the "button lost value" event from propagating; this presents a unified select-like interface to users of the RadioButtonGroup. The current value semantics are preserved; other clients of the RadioButton do not see a change in behavior. - EventEmitter: If the custom event detail is *not* an object, do not use the object-like semantics for forwarding it; just send it as-is. - Comments: In the course of laying the groundwork for the application wizard, I throw a LOT of comments into the code, describing APIs, interfaces, class and function signatures, to better document the behavior inside and as signposts for future work. * web: permit arrays to be sent in custom events without interpolation. * actually use assignValue or rather serializeFieldRecursive Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io> |
|||
3f02534eb1 |
web: weightloss program, part 1: FlowSearch (#6332)
* web: weightloss program, part 1: FlowSearch This commit extracts the multiple uses of SearchSelect for Flow lookups in the `providers` collection and replaces them with a slightly more legible format, from: ```HTML <ak-search-select .fetchObjects=${async (query?: string): Promise<Flow[]> => { const args: FlowsInstancesListRequest = { ordering: "slug", designation: FlowsInstancesListDesignationEnum.Authentication, }; if (query !== undefined) { args.search = query; } const flows = await new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(args); return flows.results; }} .renderElement=${(flow: Flow): string => { return RenderFlowOption(flow); }} .renderDescription=${(flow: Flow): TemplateResult => { return html`${flow.name}`; }} .value=${(flow: Flow | undefined): string | undefined => { return flow?.pk; }} .selected=${(flow: Flow): boolean => { return flow.pk === this.instance?.authenticationFlow; }} > </ak-search-select> ``` ... to: ```HTML <ak-flow-search flowType=${FlowsInstancesListDesignationEnum.Authentication} .currentFlow=${this.instance?.authenticationFlow} required ></ak-flow-search> ``` All of those middle methods, like `renderElement`, `renderDescription`, etc, are *completely the same* for *all* of the searches, and there are something like 25 of them; this commit only covers the 8 in `providers`, but the next commit should carry all of them. The topmost example has been extracted into its own Web Component, `ak-flow-search`, that takes only two arguments: the type of `FlowInstanceListDesignation` and the current instance of the flow. The static methods for `renderElement`, `renderDescription` and `value` (which are all the same in all 25 instances of `FlowInstancesListRequest`) have been made into standalone functions. `fetchObjects` has been made into a method that takes the parameter from the `designation` property, and `selected` has been turned into a method that takes the comparator instance from the `currentFlow` property. That's it. That's the whole of it. `SearchSelect` now emits an event whenever the user changes the field, and `ak-flow-search` intercepts that event to mirror the value locally. `Form` has been adapted to recognize the `ak-flow-search` element and extract the current value. There are a number of legibility issues remaining, even with this fix. The Authentik Form manager is dependent upon a component named `ak-form-element-horizontal`, which is a container for a single displayed element in a form: ```HTML <ak-form-element-horizontal label=${msg("Authorization flow")} ?required=${true} name="authorizationFlow" > <ak-flow-search flowType=${FlowsInstancesListDesignationEnum.Authorization} .currentFlow=${this.instance?.authorizationFlow} required ></ak-flow-search> <p class="pf-c-form__helper-text"> ${msg("Flow used when authorizing this provider.")} </p> </ak-form-element-horizontal> ``` Imagine, instead, if we could write: ```HTML <ak-form-element-flow-search flowType=${FlowsInstancesListDesignationEnum.Authorization} .currentFlow=${this.instance?.authorizationFlow} required name="authorizationFlow"> <label slot="label">${msg("Authorization flow")}</label> <span slot="help">${msg("Flow used when authorizing this provider.")}</span> <ak-form-element-flow-search> ``` Starting with a superclass that understands the need for `label` and `help` slots, it would automatically configure the input object that would be used. We've already specified multiple identical copies of this thing in multiple different places; centralizing their definition and then re-using them would be classic code re-use. Even better, since the Authorization flow is used 10 times in the whole of our code base, and the Authentication flow 8 times, and they are *all identical*, it would be fitting if we just created wrappers: ```HTML <ak-form-element-flow-search flowType=${FlowsInstancesListDesignationEnum.Authorization}> <ak-form-element-flow-search> ``` That's really all that's needed. There are *hundreds* (about 470 total) cases where nine or more lines of repetitious HTML could be replaced with a one-liner like the above. A "narrow waist" design is one that allows for a system to communicate between two different components through a small but consistent collection of calls. The Form manager needs to be narrowed hard. The `ak-form-element-horizontal` is a wrapper around an input object, and it has this at its core for extracting that information. This forwards the name component to the containing input object so that when the input object generates an event, we can identify the field it's associated with. ```Javascript this.querySelectorAll("*").forEach((input) => { switch (input.tagName.toLowerCase()) { case "input": case "textarea": case "select": case "ak-codemirror": case "ak-chip-group": case "ak-search-select": case "ak-radio": input.setAttribute("name", this.name); break; default: return; } ``` A *temporary* variant of this is in the `ak-flow-search` component, to support this API without having to modify `ak-form-element-horizontal`. And then `ak-form` itself has this: ```Javascript if ( inputElement.tagName.toLowerCase() === "select" && "multiple" in inputElement.attributes ) { const selectElement = inputElement as unknown as HTMLSelectElement; json[element.name] = Array.from(selectElement.selectedOptions).map((v) => v.value); } else if ( inputElement.tagName.toLowerCase() === "input" && inputElement.type === "date" ) { json[element.name] = inputElement.valueAsDate; } else if ( inputElement.tagName.toLowerCase() === "input" && inputElement.type === "datetime-local" ) { json[element.name] = new Date(inputElement.valueAsNumber); } // ... another 20 lines removed ``` This ought to read: ```Javascript const json = elements.filter((element => element instanceof AkFormComponent) .reduce((acc, element) => ({ ...acc, [element.name]: element.value] }); ``` Where, instead of hand-writing all the different input objects for date and datetime and checkbox into our forms, and then having to craft custom value extractors for each and every one of them, just write *one* version of each with all the wrappers and bells and whistles already attached, and have each one of them have a `value` getter descriptor that returns the value expected by our form handler. A back-of-the-envelope estimation is that there's about four *thousand* lines that could disappear if we did this right. More importantly, it would be possible to create new `AkFormComponent`s without having to register them or define them for `ak-form`; as long as they conformed to the AkFormComponent's expectations for "what is a source of values for a Form", `ak-form` would understand how to handle it. Ultimately, what I want is to be able to do this: ``` HTML <ak-input-form itemtype="ak-search" itemid="ak-authentication" itemprop=${this.instance}></ak-inputform> ``` And it will (1) go out and find the right kind of search to put there, (2) conduct the right kind of fetch to fill that search, (3) pre-configure it with the user's current choice in that locale. I don't think this is possible-- for one thing, it would be very expensive in terms of development, and it may break the "narrow waist" ideal by require that the `ak-input-form` object know all the different kinds of searches that are available. The old Midgardian dream was that the object would have *just* the identity triple (A table, a row of that table, a field of that row), and the Javascript would go out and, using the identity, *find* the right object for CRUD (Creating, Retrieving, Updating, and Deleting) it. But that inspiration, as unreachable as it is, is where I'm headed. Where our objects are both *smart* and *standalone*. Where they're polite citizens in an ordered universe, capable of independence sufficient to be tested and validated and trusted, but working in concert to achieve our aims. * web: unravel the search-select for flows completely. This commit removes *all* instances of the search-select for flows, classifying them into four different categories: - a search with no default - a search with a default - a search with a default and a fallback to a static default if non specified - a search with a default and a fallback to the tenant's preferred default if this is a new instance and no flow specified. It's not humanly possible to test all the instances where this has been committed, but the linters are very happy with the results, and I'm going to eyeball every one of them in the github presentation before I move this out of draft. * web: several were declared 'required' that were not. * web: I can't believe this was rejected because of a misspelling in a code comment. Well done\! * web: another codespell fix for a comment. * web: adding 'codespell' to the pre-commit command. Fixed spelling error in eventEmitter. |
|||
12c4ac704f |
web: basic cleanup of buttons (#6107)
* web: basic cleanup of buttons This commit adds Storybook features to the Authentik four-stage button. The four-stage button is used to: - trigger an action - show that the action is running - show when the action has succeeded, then reset - show when the action has failed, then reset It is used mostly for fetching data from the server. The variants are: - ak-spinner-button: The basic form takes a single property argument, `callAction` a function that returns a Promise (an asynchronous function). - ak-action-button: Takes an API request function (which are all asynchronous) and adapts it to the `callAction`. The only difference in behavior with the Spinner button is that on failure the error message will be displayed by a notification. - ak-token-copy-button: A specialized button that, on success, pushes the content of the retrieved object into the clipboard. Cleanup consisted of: - removing a lot of the in-line code from the HTML, decluttering it and making more explicit what the behaviors of each button type are on success and on failure. - Replacing the ad-hoc Promise management with Lit's own `Task` handler. The `Task` handler knows how to notify a Lit-Element of its own internal state change, making it ideal for objects like this button that need to change their appearance as a Promise'd task progresses from idle → running → (success or failure). - Providing JSDoc strings for all of the properties, slots, attributes, elements, and events. - Adding 'pointer-events: none' during the running phases of the action, to prevent the user from clicking the button multiple times and launching multiple queries. - Emitting an event for every stage of the operation: - `ak-button-click` when the button is clicked. - `ak-button-success` when the action completes. The payload is included in `Event.detail.result` - `ak-button-failure` when the action fails. The error message is included in `Event.detail.error` - `ak-button-reset` when the button completes a notification and goes back to idle **Storybook** Since the API requests for both `ak-spinner-button` and `ak-action-button` require only that a promise be returned, Storybooking them was straightforward. `ak-token-copy-button` is a special-purpose derivative with an internal functionality that can't be easily mocked (yet), so there's no Storybook for it. All of the stories provide the required asynchronous function, in this cose one that waits three seconds before emitting either a `response` or `reject` Promise. `ak-action-button`'s Story has event handler code so that pressing on the button will result in a message being written to a display block under the button. I've added a new pair of class mixins, `CustomEmitterElement` and `CustomListenerElement`. These each add an additional method to the classes they're mixed into; one provides a very easy way to emit a custom event and one provides a way to receive the custom event while sweeping all of the custom event type handling under the rug. `emitCustomEvent` replaces this: ``` JavaScript this.dispatchEvent( new CustomEvent('ak-button-click', { composed: true, bubbles: true, detail: { target: this, result: "Some result, huh?" }, }) ); ``` ... with this: ``` JavaScript this.dispatchCustomEvent('ak-button-click', { result: "Some result, huh?" }); ``` The `CustomListenerElement` handler just ensures that the handler being passed to it takes a CustomEvent, and then makes sure that any actual event passed to the handler has been type-guarded to ensure it is a custom event. **Observations** *Composition vs Inheritance, Part 1* The four-state button has three implementations. All three inherit from `BaseTaskButton`: - `spinner` - provides a default `callAction()` - `action` - provides a different name for `callAction` - overrides `onError` to display a Notification. - `token-copy` - provides a custom `callAction` - overrides `onSuccess` to copy the results to the keyboard - overrides `onError` to display a Notification, with special handling for asynchronous processing. The *results* of all of these could be handled higher up as event handlers, and the button could be just a thing that displays the states. As it is, the BaseStateToken has only one reason to change (the Promise changes its state), so I'm satisfied that this is a suitable evolution of the product, and that it does what it says it does. *Developer Ergonomics* The one thing that stands out to me time and again is just how *confusing* all of the Patternfly stuff tends to be; not because it's not logical, but because it overwhelms the human 7±2 ability to remember details like this without any imperative to memorize all of them. I would like to get them under control by marshalling them under a semantic CSS regime, but I'm blocked by some basic disconnects in the current development environment. We can't shake out the CSS as much as we'd like because there's no ESPrima equivalent for Typescript, and the smallest bundle purgeCSS is capable of making for just *one* button is about 55KB. That's a bit too much. It's a great system for getting off the ground, but long-term it needs more love than we (can) give it. * Prettier has opinions. * Removed extraneous debugging code. * Added comments to the BaseTaskButton parent class. * web: fixed two build errors (typing) in the stories. * web: prettier's got opinions * web: refactor the buttons This commit adds URL mocking to Storybook, which in turn allows us to commit a Story for ak-token-copy-button. I have confirmed that the button's algorithm for writing to the clipboard works on Safari, Chrome, and Firefox. I don't know what's up with IE. * ONE BYTE in .storybook/main blocked integration. With the repair of lit-analyze, it's time to fix the rule set to at least let us pass for the moment. * Still looking for the list of exceptions in lit-analyze that will let us pass once more. * web: repair error in EnterpriseLicenseForm This commit continues to find the right configuration for lit-analyze. During the course of this repair, I discovered a bug in the EnterpriseLicenseForm; the original usage could result in the _string_ `undefined` being passed back as a value. To handle the case where the value truly is undefined, the `ifDefined()` directive must be used in the HTML template. I have also instituted a case-by-case stylistic decision to allow the HTML, and only the HTML, to be longer that 100 characters when doing so reduces the visual "noise" of a function. |