3f02534eb1
* 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.
124 lines
5.2 KiB
JSON
124 lines
5.2 KiB
JSON
{
|
|
"name": "@goauthentik/web",
|
|
"version": "0.0.0",
|
|
"private": true,
|
|
"license": "MIT",
|
|
"scripts": {
|
|
"extract-locales": "lit-localize extract",
|
|
"build-locales": "run-s build-locales:build",
|
|
"build-locales:build": "lit-localize build",
|
|
"build-locales:repair": "prettier --write ./src/locale-codes.ts",
|
|
"rollup:build": "node --max-old-space-size=4096 node_modules/.bin/rollup -c ./rollup.config.js",
|
|
"rollup:build-proxy": "node --max-old-space-size=4096 node_modules/.bin/rollup -c ./rollup.proxy.js",
|
|
"rollup:watch": "node --max-old-space-size=8192 node_modules/.bin/rollup -c -w",
|
|
"build": "run-s build-locales rollup:build",
|
|
"build-proxy": "run-s build-locales rollup:build-proxy",
|
|
"watch": "run-s build-locales rollup:watch",
|
|
"lint": "eslint . --max-warnings 0 --fix",
|
|
"lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s",
|
|
"lit-analyse": "lit-analyzer src",
|
|
"precommit": "run-s tsc lit-analyse lint lint:spelling prettier",
|
|
"prettier-check": "prettier --check .",
|
|
"prettier": "prettier --write .",
|
|
"tsc:execute": "tsc --noEmit -p .",
|
|
"tsc": "run-s build-locales tsc:execute",
|
|
"storybook": "storybook dev -p 6006",
|
|
"storybook:build": "storybook build",
|
|
"background-image": "sharp resize 2650 --mozjpeg -i src/assets/images/flow_background.jpg -o src/assets/images/flow_background.jpg"
|
|
},
|
|
"dependencies": {
|
|
"@codemirror/lang-html": "^6.4.5",
|
|
"@codemirror/lang-javascript": "^6.1.9",
|
|
"@codemirror/lang-python": "^6.1.3",
|
|
"@codemirror/lang-xml": "^6.0.2",
|
|
"@codemirror/legacy-modes": "^6.3.3",
|
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
"@formatjs/intl-listformat": "^7.4.0",
|
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
|
"@goauthentik/api": "^2023.6.1-1690455444",
|
|
"@lit-labs/context": "^0.3.3",
|
|
"@lit-labs/task": "^2.1.2",
|
|
"@lit/localize": "^0.11.4",
|
|
"@patternfly/patternfly": "^4.224.2",
|
|
"@sentry/browser": "^7.60.1",
|
|
"@sentry/tracing": "^7.60.1",
|
|
"@webcomponents/webcomponentsjs": "^2.8.0",
|
|
"base64-js": "^1.5.1",
|
|
"chart.js": "^4.3.2",
|
|
"chartjs-adapter-moment": "^1.0.1",
|
|
"codemirror": "^6.0.1",
|
|
"construct-style-sheets-polyfill": "^3.1.0",
|
|
"core-js": "^3.32.0",
|
|
"country-flag-icons": "^1.5.7",
|
|
"fuse.js": "^6.6.2",
|
|
"lit": "^2.7.6",
|
|
"mermaid": "^10.3.0",
|
|
"rapidoc": "^9.3.4",
|
|
"style-mod": "^4.0.3",
|
|
"webcomponent-qr-code": "^1.2.0",
|
|
"yaml": "^2.3.1"
|
|
},
|
|
"devDependencies": {
|
|
"@babel/core": "^7.22.9",
|
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
"@babel/plugin-proposal-decorators": "^7.22.7",
|
|
"@babel/plugin-proposal-private-methods": "^7.18.6",
|
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
|
"@babel/plugin-transform-runtime": "^7.22.9",
|
|
"@babel/preset-env": "^7.22.9",
|
|
"@babel/preset-typescript": "^7.22.5",
|
|
"@hcaptcha/types": "^1.0.3",
|
|
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
|
|
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
|
|
"@lit/localize-tools": "^0.6.9",
|
|
"@rollup/plugin-babel": "^6.0.3",
|
|
"@rollup/plugin-commonjs": "^25.0.3",
|
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
|
"@rollup/plugin-replace": "^5.0.2",
|
|
"@rollup/plugin-typescript": "^11.1.2",
|
|
"@storybook/addon-essentials": "^7.1.1",
|
|
"@storybook/addon-links": "^7.1.1",
|
|
"@storybook/blocks": "^7.1.1",
|
|
"@storybook/web-components": "^7.1.0",
|
|
"@storybook/web-components-vite": "^7.1.1",
|
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
|
"@types/chart.js": "^2.9.37",
|
|
"@types/codemirror": "5.60.8",
|
|
"@types/grecaptcha": "^3.0.4",
|
|
"@typescript-eslint/eslint-plugin": "^6.2.0",
|
|
"@typescript-eslint/parser": "^6.2.0",
|
|
"babel-plugin-macros": "^3.1.0",
|
|
"babel-plugin-tsconfig-paths": "^1.0.3",
|
|
"eslint": "^8.45.0",
|
|
"eslint-config-google": "^0.14.0",
|
|
"eslint-plugin-custom-elements": "0.0.8",
|
|
"eslint-plugin-lit": "^1.8.3",
|
|
"eslint-plugin-storybook": "^0.6.13",
|
|
"lit-analyzer": "^1.2.1",
|
|
"npm-run-all": "^4.1.5",
|
|
"prettier": "^3.0.0",
|
|
"pyright": "^1.1.319",
|
|
"react": "^18.2.0",
|
|
"react-dom": "^18.2.0",
|
|
"rollup": "^2.79.1",
|
|
"rollup-plugin-copy": "^3.4.0",
|
|
"rollup-plugin-cssimport": "^1.0.3",
|
|
"rollup-plugin-minify-html-literals": "^1.2.6",
|
|
"rollup-plugin-postcss-lit": "^2.1.0",
|
|
"rollup-plugin-terser": "^7.0.2",
|
|
"sharp-cli": "^4.1.1",
|
|
"storybook": "^7.1.1",
|
|
"storybook-addon-mock": "^4.1.0",
|
|
"ts-lit-plugin": "^1.2.1",
|
|
"tslib": "^2.6.1",
|
|
"turnstile-types": "^1.1.2",
|
|
"typescript": "^5.1.6",
|
|
"vite-tsconfig-paths": "^4.2.0"
|
|
},
|
|
"optionalDependencies": {
|
|
"@esbuild/darwin-arm64": "^0.18.17",
|
|
"@esbuild/linux-amd64": "^0.18.11",
|
|
"@esbuild/linux-arm64": "^0.18.17"
|
|
}
|
|
}
|