Skip to content

Commit

Permalink
web/admin: application wizard (part 1) (#2745)
Browse files Browse the repository at this point in the history
* initial

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* remove log

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* start oauth

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* use form for all type wizard pages

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* more oauth

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* basic wizard actions

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* make resets work

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add hint in provider wizard

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* render correct icon in empty state in table page

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* improve empty state

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* more

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add more pages

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* fix

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add group PK to service account creation response

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* use wizard-level isValid prop

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* re-add old buttons

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
  • Loading branch information
BeryJu committed Jun 25, 2022
1 parent a8c04f9 commit 504338e
Show file tree
Hide file tree
Showing 36 changed files with 1,275 additions and 147 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ web-install:
cd web && npm ci

web-watch:
rm -rf web/dist/
mkdir web/dist/
touch web/dist/.gitkeep
cd web && npm run watch

web-lint-fix:
Expand Down
8 changes: 8 additions & 0 deletions authentik/core/api/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"meta_publisher",
"group",
]
filterset_fields = [
"name",
"slug",
"meta_launch_url",
"meta_description",
"meta_publisher",
"group",
]
lookup_field = "slug"
filterset_fields = ["name", "slug"]
ordering = ["name"]
Expand Down
20 changes: 18 additions & 2 deletions authentik/core/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
)
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, ListField, SerializerMethodField
from rest_framework.fields import (
CharField,
IntegerField,
JSONField,
ListField,
SerializerMethodField,
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
Expand Down Expand Up @@ -315,6 +321,9 @@ def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
{
"username": CharField(required=True),
"token": CharField(required=True),
"user_uid": CharField(required=True),
"user_pk": IntegerField(required=True),
"group_pk": CharField(required=False),
},
)
},
Expand All @@ -332,18 +341,25 @@ def service_account(self, request: Request) -> Response:
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
path=USER_PATH_SERVICE_ACCOUNT,
)
response = {
"username": user.username,
"user_uid": user.uid,
"user_pk": user.pk,
}
if create_group and self.request.user.has_perm("authentik_core.add_group"):
group = Group.objects.create(
name=username,
)
group.users.add(user)
response["group_pk"] = str(group.pk)
token = Token.objects.create(
identifier=slugify(f"service-account-{username}-password"),
intent=TokenIntents.INTENT_APP_PASSWORD,
user=user,
expires=now() + timedelta(days=360),
)
return Response({"username": user.username, "token": token.key})
response["token"] = token.key
return Response(response)
except (IntegrityError) as exc:
return Response(data={"non_field_errors": [str(exc)]}, status=400)

Expand Down
8 changes: 8 additions & 0 deletions schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31589,8 +31589,16 @@ components:
type: string
token:
type: string
user_uid:
type: string
user_pk:
type: integer
group_pk:
type: string
required:
- token
- user_pk
- user_uid
- username
UserSetting:
type: object
Expand Down
2 changes: 1 addition & 1 deletion web/src/common/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class WebsocketClient {
});
this.messageSocket.addEventListener("close", (e) => {
console.debug(`authentik/ws: closed ws connection: ${e}`);
if (this.retryDelay > 3000) {
if (this.retryDelay > 6000) {
showMessage(
{
level: MessageLevel.error,
Expand Down
36 changes: 22 additions & 14 deletions web/src/elements/forms/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export class APIError extends Error {
}
}

export interface KeyUnknown {
[key: string]: unknown;
}

@customElement("ak-form")
export class Form<T> extends LitElement {
viewportCheck = true;
Expand Down Expand Up @@ -101,15 +105,11 @@ export class Form<T> extends LitElement {
ironForm?.reset();
}

/**
* If this form contains a file input, and the input as been filled, this function returns
* said file.
* @returns File object or undefined
*/
getFormFile(): File | undefined {
getFormFiles(): { [key: string]: File } {
const ironForm = this.shadowRoot?.querySelector("iron-form");
const files: { [key: string]: File } = {};
if (!ironForm) {
return;
return files;
}
const elements = ironForm._getSubmittableElements();
for (let i = 0; i < elements.length; i++) {
Expand All @@ -118,13 +118,18 @@ export class Form<T> extends LitElement {
if ((element.files || []).length < 1) {
continue;
}
// We already checked the length
return (element.files || [])[0];
files[element.name] = (element.files || [])[0];
}
}
return files;
}

serializeForm(form: IronFormElement): T {
serializeForm(): T | undefined {
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
if (!form) {
console.warn("authentik/forms: failed to find iron-form");
return;
}
const elements: HTMLInputElement[] = form._getSubmittableElements();
const json: { [key: string]: unknown } = {};
elements.forEach((element) => {
Expand Down Expand Up @@ -189,12 +194,15 @@ export class Form<T> extends LitElement {

submit(ev: Event): Promise<unknown> | undefined {
ev.preventDefault();
const ironForm = this.shadowRoot?.querySelector("iron-form");
if (!ironForm) {
const data = this.serializeForm();
if (!data) {
return;
}
const form = this.shadowRoot?.querySelector<IronFormElement>("iron-form");
if (!form) {
console.warn("authentik/forms: failed to find iron-form");
return;
}
const data = this.serializeForm(ironForm);
return this.send(data)
.then((r) => {
showMessage({
Expand All @@ -221,7 +229,7 @@ export class Form<T> extends LitElement {
throw errorMessage;
}
// assign all input-related errors to their elements
const elements: HorizontalFormElement[] = ironForm._getSubmittableElements();
const elements: HorizontalFormElement[] = form._getSubmittableElements();
elements.forEach((element) => {
const elementName = element.name;
if (!elementName) return;
Expand Down
147 changes: 147 additions & 0 deletions web/src/elements/wizard/ActionWizardPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { t } from "@lingui/macro";

import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";

import AKGlobal from "../../authentik.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";

import { ResponseError } from "@goauthentik/api";

import { EVENT_REFRESH } from "../../constants";
import { WizardAction } from "./Wizard";
import { WizardPage } from "./WizardPage";

export enum ActionState {
pending = "pending",
running = "running",
done = "done",
failed = "failed",
}

export interface ActionStateBundle {
action: WizardAction;
state: ActionState;
idx: number;
}

@customElement("ak-wizard-page-action")
export class ActionWizardPage extends WizardPage {
static get styles(): CSSResult[] {
return [PFBase, PFBullseye, PFEmptyState, PFTitle, PFProgressStepper, AKGlobal];
}

@property({ attribute: false })
states: ActionStateBundle[] = [];

@property({ attribute: false })
currentStep?: ActionStateBundle;

activeCallback = async (): Promise<void> => {
this.states = [];
this.host.actions.map((act, idx) => {
this.states.push({
action: act,
state: ActionState.pending,
idx: idx,
});
});
this.host.canBack = false;
this.host.canCancel = false;
await this.run();
// Ensure wizard is closable, even when run() failed
this.host.isValid = true;
};

sidebarLabel = () => t`Apply changes`;

async run(): Promise<void> {
this.currentStep = this.states[0];
await new Promise((r) => setTimeout(r, 500));
for await (const bundle of this.states) {
this.currentStep = bundle;
this.currentStep.state = ActionState.running;
this.requestUpdate();
try {
await bundle.action.run();
await new Promise((r) => setTimeout(r, 500));
this.currentStep.state = ActionState.done;
this.requestUpdate();
} catch (exc) {
if (exc instanceof ResponseError) {
this.currentStep.action.subText = await exc.response.text();
} else {
this.currentStep.action.subText = (exc as Error).toString();
}
this.currentStep.state = ActionState.failed;
this.requestUpdate();
return;
}
}
this.host.isValid = true;
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
}

render(): TemplateResult {
return html`<div class="pf-l-bullseye">
<div class="pf-c-empty-state pf-m-lg">
<div class="pf-c-empty-state__content">
<i class="fas fa- fa-cogs pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">${this.currentStep?.action.displayName}</h1>
<div class="pf-c-empty-state__body">
<ol class="pf-c-progress-stepper pf-m-vertical">
${this.states.map((state) => {
let cls = "";
switch (state.state) {
case ActionState.pending:
cls = "pf-m-pending";
break;
case ActionState.done:
cls = "pf-m-success";
break;
case ActionState.running:
cls = "pf-m-info";
break;
case ActionState.failed:
cls = "pf-m-danger";
break;
}
if (state.idx === this.currentStep?.idx) {
cls += " pf-m-current";
}
return html` <li class="pf-c-progress-stepper__step ${cls}">
<div class="pf-c-progress-stepper__step-connector">
<span class="pf-c-progress-stepper__step-icon">
<i class="fas fa-check-circle" aria-hidden="true"></i>
</span>
</div>
<div class="pf-c-progress-stepper__step-main">
<div class="pf-c-progress-stepper__step-title">
${state.action.displayName}
</div>
${state.action.subText
? html`<div
class="pf-c-progress-stepper__step-description"
>
${state.action.subText}
</div>`
: html``}
</div>
</li>`;
})}
</ol>
</div>
</div>
</div>
</div>`;
}
}
6 changes: 0 additions & 6 deletions web/src/elements/wizard/FormWizardPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ import { WizardPage } from "./WizardPage";

@customElement("ak-wizard-page-form")
export class FormWizardPage extends WizardPage {
_isValid = true;

isValid(): boolean {
return this._isValid;
}

nextCallback = async () => {
const form = this.querySelector<Form<unknown>>("*");
if (!form) {
Expand Down

0 comments on commit 504338e

Please sign in to comment.