Compare commits

...

10 Commits

Author SHA1 Message Date
27e9e892e7 web: update the type names for routes.
While writing up the commit message for the previous commit, I realized that I didn't really like
the typenames as they're outlaid in `routeUtils`.  This is much more explicit, describing the
four types of routes we have: ListRoute, ViewRoute, InternalRedirect, ExternalRedirect.
2024-03-14 14:40:19 -07:00
70bf745c0c web: remove a lot of duplication from the Routes
This commit extracts the highly repetitive `new Route(new RegExp(...))` syntax into a mappable
function, leaving only the regular expression and the asynchronous loader behind.  In the process,
it creates a typed description of what kinds of routes we have, and uses type-level pattern matching
to generate the route handlers and redirects.

For example, it establishes that for the purposes of discriminating between redirects and loaders,
the loader type is irrelevant to routing correctly. The loader type *is* still well-typed to prevent
anyone from putting an incompatible loader into a route, but the two different loader types-- ones
that take arguments, and ones that do not-- do not matter to the route builder.

Likewise, the two different kinds of redirects *do* matter, but only because JavaScript makes a
distinction between methods of one argument that can be `call`'ed and methods of more than one
argument that must be `apply`'d.

I suppose I could make this irrelevant by enforcing that the second argument to RawRedirect always
be an Array, but that's not ergonomic, and when the `match()` function has such a lovely and simple
way of distinguishing between the two forms, ergonomics always wins.

This might seem like a curious bit of cleanup, but by exposing the regular expressions as strings,
independent of the Routes that they ultimately represent, we get the power to associate paths to
routeable objects with paths to navigable objects in the Sidebar, *and* ultimately with paths to
navigable objects in a command palette. It might also lead to replacing our Routes infrastructure
with an off-the-shelf router such as [Vaadin.router](https://github.com/vaadin/router) or Justin
Fagnani's preferred [page.js](https://visionmedia.github.io/page.js/)
2024-03-14 14:19:41 -07:00
8b4e0361c4 Merge branch 'main' into dev
* main:
  web: clean up and remove redundant alias '@goauthentik/app' (#8889)
  web/admin: fix markdown table rendering (#8908)
2024-03-14 10:35:46 -07:00
22cb5b7379 Merge branch 'main' into dev
* main:
  web: bump chromedriver from 122.0.5 to 122.0.6 in /tests/wdio (#8902)
  web: bump vite-tsconfig-paths from 4.3.1 to 4.3.2 in /web (#8903)
  core: bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#8901)
  web: provide InstallID on EnterpriseListPage (#8898)
2024-03-14 08:14:43 -07:00
2d0117d096 Merge branch 'main' into dev
* main:
  api: capabilities: properly set can_save_media when s3 is enabled (#8896)
  web: bump the rollup group in /web with 3 updates (#8891)
  core: bump pydantic from 2.6.3 to 2.6.4 (#8892)
  core: bump twilio from 9.0.0 to 9.0.1 (#8893)
2024-03-13 14:05:11 -07:00
035bda4eac Merge branch 'main' into dev
* main:
  Update _envoy_istio.md (#8888)
  website/docs: new landing page for Providers (#8879)
  web: bump the sentry group in /web with 1 update (#8881)
  web: bump chromedriver from 122.0.4 to 122.0.5 in /tests/wdio (#8884)
  web: bump the eslint group in /tests/wdio with 2 updates (#8883)
  web: bump the eslint group in /web with 2 updates (#8885)
  website: bump @types/react from 18.2.64 to 18.2.65 in /website (#8886)
2024-03-12 13:31:35 -07:00
50906214e5 Merge branch 'main' into dev
* main:
  web: upgrade to lit 3 (#8781)
2024-03-11 11:03:04 -07:00
e505f274b6 Merge branch 'main' into dev
* main:
  web: fix esbuild issue with style sheets (#8856)
2024-03-11 10:28:05 -07:00
fe52f44dca Merge branch 'main' into dev
* main:
  tenants: really ensure default tenant cannot be deleted (#8875)
  core: bump github.com/go-openapi/runtime from 0.27.2 to 0.28.0 (#8867)
  core: bump pytest from 8.0.2 to 8.1.1 (#8868)
  core: bump github.com/go-openapi/strfmt from 0.22.2 to 0.23.0 (#8869)
  core: bump bandit from 1.7.7 to 1.7.8 (#8870)
  core: bump packaging from 23.2 to 24.0 (#8871)
  core: bump ruff from 0.3.1 to 0.3.2 (#8873)
  web: bump the wdio group in /tests/wdio with 3 updates (#8865)
  core: bump requests-oauthlib from 1.3.1 to 1.4.0 (#8866)
  core: bump uvicorn from 0.27.1 to 0.28.0 (#8872)
  core: bump django-filter from 23.5 to 24.1 (#8874)
2024-03-11 10:27:43 -07:00
3146e5a50f web: fix esbuild issue with style sheets
Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.
2024-03-08 14:15:55 -08:00
3 changed files with 314 additions and 157 deletions

View File

@ -1,155 +1,266 @@
import "@goauthentik/admin/admin-overview/AdminOverviewPage";
import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
import { RawRoute, makeRoute } from "@goauthentik/elements/router/routeUtils";
import { html } from "lit";
export const ROUTES: Route[] = [
export const _ROUTES: RawRoute[] = [
// Prevent infinite Shell loops
new Route(new RegExp("^/$")).redirect("/administration/overview"),
new Route(new RegExp("^#.*")).redirect("/administration/overview"),
new Route(new RegExp("^/library$")).redirect("/if/user/", true),
["^/$", "/administration/overview"],
["^#.*", "/administration/overview"],
["^/library$", ["/if/user/", true]],
// statically imported since this is the default route
new Route(new RegExp("^/administration/overview$"), async () => {
[
"^/administration/overview$",
async () => {
return html`<ak-admin-overview></ak-admin-overview>`;
}),
new Route(new RegExp("^/administration/dashboard/users$"), async () => {
},
],
[
"^/administration/dashboard/users$",
async () => {
await import("@goauthentik/admin/admin-overview/DashboardUserPage");
return html`<ak-admin-dashboard-users></ak-admin-dashboard-users>`;
}),
new Route(new RegExp("^/administration/system-tasks$"), async () => {
},
],
[
"^/administration/system-tasks$",
async () => {
await import("@goauthentik/admin/system-tasks/SystemTaskListPage");
return html`<ak-system-task-list></ak-system-task-list>`;
}),
new Route(new RegExp("^/core/providers$"), async () => {
},
],
[
"^/core/providers$",
async () => {
await import("@goauthentik/admin/providers/ProviderListPage");
return html`<ak-provider-list></ak-provider-list>`;
}),
new Route(new RegExp(`^/core/providers/(?<id>${ID_REGEX})$`), async (args) => {
},
],
[
`^/core/providers/(?<id>${ID_REGEX}])$`,
async (args) => {
await import("@goauthentik/admin/providers/ProviderViewPage");
return html`<ak-provider-view .providerID=${parseInt(args.id, 10)}></ak-provider-view>`;
}),
new Route(new RegExp("^/core/applications$"), async () => {
},
],
[
"^/core/applications$",
async () => {
await import("@goauthentik/admin/applications/ApplicationListPage");
return html`<ak-application-list></ak-application-list>`;
}),
new Route(new RegExp(`^/core/applications/(?<slug>${SLUG_REGEX})$`), async (args) => {
},
],
[
`^/core/applications/(?<slug>${SLUG_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/applications/ApplicationViewPage");
return html`<ak-application-view .applicationSlug=${args.slug}></ak-application-view>`;
}),
new Route(new RegExp("^/core/sources$"), async () => {
},
],
[
"^/core/sources$",
async () => {
await import("@goauthentik/admin/sources/SourceListPage");
return html`<ak-source-list></ak-source-list>`;
}),
new Route(new RegExp(`^/core/sources/(?<slug>${SLUG_REGEX})$`), async (args) => {
},
],
[
`^/core/sources/(?<slug>${SLUG_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/sources/SourceViewPage");
return html`<ak-source-view .sourceSlug=${args.slug}></ak-source-view>`;
}),
new Route(new RegExp("^/core/property-mappings$"), async () => {
},
],
[
"^/core/property-mappings$",
async () => {
await import("@goauthentik/admin/property-mappings/PropertyMappingListPage");
return html`<ak-property-mapping-list></ak-property-mapping-list>`;
}),
new Route(new RegExp("^/core/tokens$"), async () => {
},
],
[
"^/core/tokens$",
async () => {
await import("@goauthentik/admin/tokens/TokenListPage");
return html`<ak-token-list></ak-token-list>`;
}),
new Route(new RegExp("^/core/brands"), async () => {
},
],
[
"^/core/brands",
async () => {
await import("@goauthentik/admin/brands/BrandListPage");
return html`<ak-brand-list></ak-brand-list>`;
}),
new Route(new RegExp("^/policy/policies$"), async () => {
},
],
[
"^/policy/policies$",
async () => {
await import("@goauthentik/admin/policies/PolicyListPage");
return html`<ak-policy-list></ak-policy-list>`;
}),
new Route(new RegExp("^/policy/reputation$"), async () => {
},
],
[
"^/policy/reputation$",
async () => {
await import("@goauthentik/admin/policies/reputation/ReputationListPage");
return html`<ak-policy-reputation-list></ak-policy-reputation-list>`;
}),
new Route(new RegExp("^/identity/groups$"), async () => {
},
],
[
"^/identity/groups$",
async () => {
await import("@goauthentik/admin/groups/GroupListPage");
return html`<ak-group-list></ak-group-list>`;
}),
new Route(new RegExp(`^/identity/groups/(?<uuid>${UUID_REGEX})$`), async (args) => {
},
],
[
`^/identity/groups/(?<uuid>${UUID_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/groups/GroupViewPage");
return html`<ak-group-view .groupId=${args.uuid}></ak-group-view>`;
}),
new Route(new RegExp("^/identity/users$"), async () => {
},
],
[
"^/identity/users$",
async () => {
await import("@goauthentik/admin/users/UserListPage");
return html`<ak-user-list></ak-user-list>`;
}),
new Route(new RegExp(`^/identity/users/(?<id>${ID_REGEX})$`), async (args) => {
},
],
[
`^/identity/users/(?<id>${ID_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/users/UserViewPage");
return html`<ak-user-view .userId=${parseInt(args.id, 10)}></ak-user-view>`;
}),
new Route(new RegExp("^/identity/roles$"), async () => {
},
],
[
"^/identity/roles$",
async () => {
await import("@goauthentik/admin/roles/RoleListPage");
return html`<ak-role-list></ak-role-list>`;
}),
new Route(new RegExp(`^/identity/roles/(?<id>${UUID_REGEX})$`), async (args) => {
},
],
[
`^/identity/roles/(?<id>${UUID_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/roles/RoleViewPage");
return html`<ak-role-view roleId=${args.id}></ak-role-view>`;
}),
new Route(new RegExp("^/flow/stages/invitations$"), async () => {
},
],
[
"^/flow/stages/invitations$",
async () => {
await import("@goauthentik/admin/stages/invitation/InvitationListPage");
return html`<ak-stage-invitation-list></ak-stage-invitation-list>`;
}),
new Route(new RegExp("^/flow/stages/prompts$"), async () => {
},
],
[
"^/flow/stages/prompts$",
async () => {
await import("@goauthentik/admin/stages/prompt/PromptListPage");
return html`<ak-stage-prompt-list></ak-stage-prompt-list>`;
}),
new Route(new RegExp("^/flow/stages$"), async () => {
},
],
[
"^/flow/stages$",
async () => {
await import("@goauthentik/admin/stages/StageListPage");
return html`<ak-stage-list></ak-stage-list>`;
}),
new Route(new RegExp("^/flow/flows$"), async () => {
},
],
[
"^/flow/flows$",
async () => {
await import("@goauthentik/admin/flows/FlowListPage");
return html`<ak-flow-list></ak-flow-list>`;
}),
new Route(new RegExp(`^/flow/flows/(?<slug>${SLUG_REGEX})$`), async (args) => {
},
],
[
`^/flow/flows/(?<slug>${SLUG_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/flows/FlowViewPage");
return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;
}),
new Route(new RegExp("^/events/log$"), async () => {
},
],
[
"^/events/log$",
async () => {
await import("@goauthentik/admin/events/EventListPage");
return html`<ak-event-list></ak-event-list>`;
}),
new Route(new RegExp(`^/events/log/(?<id>${UUID_REGEX})$`), async (args) => {
},
],
[
`^/events/log/(?<id>${UUID_REGEX})$`,
async (args) => {
await import("@goauthentik/admin/events/EventViewPage");
return html`<ak-event-view .eventID=${args.id}></ak-event-view>`;
}),
new Route(new RegExp("^/events/transports$"), async () => {
},
],
[
"^/events/transports$",
async () => {
await import("@goauthentik/admin/events/TransportListPage");
return html`<ak-event-transport-list></ak-event-transport-list>`;
}),
new Route(new RegExp("^/events/rules$"), async () => {
},
],
[
"^/events/rules$",
async () => {
await import("@goauthentik/admin/events/RuleListPage");
return html`<ak-event-rule-list></ak-event-rule-list>`;
}),
new Route(new RegExp("^/outpost/outposts$"), async () => {
},
],
[
"^/outpost/outposts$",
async () => {
await import("@goauthentik/admin/outposts/OutpostListPage");
return html`<ak-outpost-list></ak-outpost-list>`;
}),
new Route(new RegExp("^/outpost/integrations$"), async () => {
},
],
[
"^/outpost/integrations$",
async () => {
await import("@goauthentik/admin/outposts/ServiceConnectionListPage");
return html`<ak-outpost-service-connection-list></ak-outpost-service-connection-list>`;
}),
new Route(new RegExp("^/crypto/certificates$"), async () => {
},
],
[
"^/crypto/certificates$",
async () => {
await import("@goauthentik/admin/crypto/CertificateKeyPairListPage");
return html`<ak-crypto-certificate-list></ak-crypto-certificate-list>`;
}),
new Route(new RegExp("^/admin/settings$"), async () => {
},
],
[
"^/admin/settings$",
async () => {
await import("@goauthentik/admin/admin-settings/AdminSettingsPage");
return html`<ak-admin-settings></ak-admin-settings>`;
}),
new Route(new RegExp("^/blueprints/instances$"), async () => {
},
],
[
"^/blueprints/instances$",
async () => {
await import("@goauthentik/admin/blueprints/BlueprintListPage");
return html`<ak-blueprint-list></ak-blueprint-list>`;
}),
new Route(new RegExp("^/debug$"), async () => {
},
],
[
"^/debug$",
async () => {
await import("@goauthentik/admin/DebugPage");
return html`<ak-admin-debug-page></ak-admin-debug-page>`;
}),
new Route(new RegExp("^/enterprise/licenses$"), async () => {
},
],
[
"^/enterprise/licenses$",
async () => {
await import("@goauthentik/admin/enterprise/EnterpriseLicenseListPage");
return html`<ak-enterprise-license-list></ak-enterprise-license-list>`;
}),
},
],
];
export const ROUTES = _ROUTES.map(makeRoute);

View File

@ -0,0 +1,41 @@
import { Route, RouteArgs } from "@goauthentik/elements/router/Route";
import { P, match } from "ts-pattern";
import { TemplateResult } from "lit";
type ListRoute = () => Promise<TemplateResult<1>>;
type ViewRoute = (_1: RouteArgs) => Promise<TemplateResult<1>>;
type InternalRedirect = string;
type ExternalRedirect = [string, boolean];
type RouteInvoke = ViewRoute | ListRoute;
type RedirectRoute = [string, InternalRedirect | ExternalRedirect];
type PageRoute = [string, RouteInvoke];
export type RawRoute = PageRoute | RedirectRoute;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isLoader = (v: any): v is RouteInvoke => typeof v === "function";
// When discriminating between redirects and loaders, the loader type is irrelevant to routing
// correctly. The loader type *is* still well-typed to prevent an incompatible loader being added to
// a route, but the two different loader types-- ones that take arguments, and ones that do not-- do
// not matter to the route builder.
// On the other hand, the two different kinds of redirects *do* matter, but only because JavaScript
// makes a distinction between methods of one argument that can be `call`'ed and methods of more
// than one argument that must be `apply`'d. (Spread arguments are converted to call or apply as
// needed).
// prettier-ignore
export function makeRoute(route: RawRoute): Route {
return match(route)
.with([P.string, P.when(isLoader)],
([path, loader]) => new Route(new RegExp(path), loader))
.with([P.string, P.string],
([path, redirect]) => new Route(new RegExp(path)).redirect(redirect))
.with([P.string, [P.string, P.boolean]],
([path, redirect]) => new Route(new RegExp(path)).redirect(...redirect))
.exhaustive();
}

View File

@ -1,15 +1,20 @@
import { Route } from "@goauthentik/elements/router/Route";
import { RawRoute, makeRoute } from "@goauthentik/elements/router/routeUtils";
import "@goauthentik/user/LibraryPage/LibraryPage";
import { html } from "lit";
export const ROUTES: Route[] = [
export const _ROUTES: RawRoute[] = [
// Prevent infinite Shell loops
new Route(new RegExp("^/$")).redirect("/library"),
new Route(new RegExp("^#.*")).redirect("/library"),
new Route(new RegExp("^/library$"), async () => html`<ak-library></ak-library>`),
new Route(new RegExp("^/settings$"), async () => {
["^/$", "/library"],
["^#.*", "/library"],
["^/library$", async () => html`<ak-library></ak-library>`],
[
"^/settings$",
async () => {
await import("@goauthentik/user/user-settings/UserSettingsPage");
return html`<ak-user-settings></ak-user-settings>`;
}),
},
],
];
export const ROUTES = _ROUTES.map(makeRoute);