From 854d94056e072343c2d928dfeeb125885c3684a0 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 20 Feb 2021 00:09:53 +0100 Subject: [PATCH] web: migrate remaining list views to web --- .../administration/stage_invitation/list.html | 109 ----- .../administration/stage_prompt/list.html | 125 ----- authentik/admin/urls.py | 10 - authentik/admin/views/stages_invitations.py | 30 +- authentik/admin/views/stages_prompts.py | 38 +- authentik/api/v2/urls.py | 2 +- authentik/flows/transfer/common.py | 1 + authentik/stages/invitation/api.py | 5 + authentik/stages/prompt/api.py | 6 +- swagger.yaml | 454 ++++++++++++++---- web/src/api/Invitations.ts | 27 ++ web/src/api/Prompts.ts | 30 ++ web/src/elements/sidebar/Sidebar.ts | 2 +- web/src/interfaces/AdminInterface.ts | 4 +- web/src/pages/stages/InvitationListPage.ts | 72 +++ web/src/pages/stages/PromptListPage.ts | 84 ++++ web/src/routes.ts | 4 + 17 files changed, 606 insertions(+), 397 deletions(-) delete mode 100644 authentik/admin/templates/administration/stage_invitation/list.html delete mode 100644 authentik/admin/templates/administration/stage_prompt/list.html create mode 100644 web/src/api/Invitations.ts create mode 100644 web/src/api/Prompts.ts create mode 100644 web/src/pages/stages/InvitationListPage.ts create mode 100644 web/src/pages/stages/PromptListPage.ts diff --git a/authentik/admin/templates/administration/stage_invitation/list.html b/authentik/admin/templates/administration/stage_invitation/list.html deleted file mode 100644 index ec753169e..000000000 --- a/authentik/admin/templates/administration/stage_invitation/list.html +++ /dev/null @@ -1,109 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Invitations' %} -

-

{% trans "Create Invitation Links to enroll Users, and optionally force specific attributes of their account." %} -

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - {% for invitation in object_list %} - - - - - - - {% endfor %} - -
{% trans 'ID' %}{% trans 'Created by' %}{% trans 'Expiry' %}
- - {{ invitation.invite_uuid }} - - - - {{ invitation.created_by }} - - - - {{ invitation.expiry|default:"-" }} - - - - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Invitations.' %} -

-
- {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any invitations." %} - {% else %} - {% trans 'Currently no invitations exist. Click the button below to create one.' %} - {% endif %} -
- - - {% trans 'Create' %} - -
-
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/templates/administration/stage_prompt/list.html b/authentik/admin/templates/administration/stage_prompt/list.html deleted file mode 100644 index 41b435e68..000000000 --- a/authentik/admin/templates/administration/stage_prompt/list.html +++ /dev/null @@ -1,125 +0,0 @@ -{% extends "administration/base.html" %} - -{% load i18n %} -{% load authentik_utils %} - -{% block content %} -
-
-

- - {% trans 'Prompts' %} -

-

{% trans "Single Prompts that can be used for Prompt Stages." %}

-
-
-
-
- {% if object_list %} -
-
- {% include 'partials/toolbar_search.html' %} -
- - - {% trans 'Create' %} - -
-
- -
- {% include 'partials/pagination.html' %} -
-
- - - - - - - - - - - - - {% for prompt in object_list %} - - - - - - - - - {% endfor %} - -
{% trans 'Field' %}{% trans 'Label' %}{% trans 'Type' %}{% trans 'Order' %}{% trans 'Flows' %}
-
-
{{ prompt.field_key }}
-
-
-
- {{ prompt.label }} -
-
-
- {{ prompt.type }} -
-
-
- {{ prompt.order }} -
-
-
    - {% for flow in prompt.flow_set.all %} -
  • {{ flow.slug }}
  • - {% empty %} -
  • -
  • - {% endfor %} -
-
- - - {% trans 'Update' %} - -
-
- - - {% trans 'Delete' %} - -
-
-
-
- {% include 'partials/pagination.html' %} -
- {% else %} -
-
- {% include 'partials/toolbar_search.html' %} -
-
-
-
- -

- {% trans 'No Stage Prompts.' %} -

-
- {% if request.GET.search != "" %} - {% trans "Your search query doesn't match any stage prompts." %} - {% else %} - {% trans 'Currently no stage prompts exist. Click the button below to create one.' %} - {% endif %} -
- {% trans 'Create' %} -
-
- {% endif %} -
-
-{% endblock %} diff --git a/authentik/admin/urls.py b/authentik/admin/urls.py index 2ac5b256b..a8527e312 100644 --- a/authentik/admin/urls.py +++ b/authentik/admin/urls.py @@ -153,11 +153,6 @@ urlpatterns = [ name="stage-binding-delete", ), # Stage Prompts - path( - "stages_prompts/", - stages_prompts.PromptListView.as_view(), - name="stage-prompts", - ), path( "stages_prompts/create/", stages_prompts.PromptCreateView.as_view(), @@ -174,11 +169,6 @@ urlpatterns = [ name="stage-prompt-delete", ), # Stage Invitations - path( - "stages/invitations/", - stages_invitations.InvitationListView.as_view(), - name="stage-invitations", - ), path( "stages/invitations/create/", stages_invitations.InvitationCreateView.as_view(), diff --git a/authentik/admin/views/stages_invitations.py b/authentik/admin/views/stages_invitations.py index 8c6e0aa81..3e87247cc 100644 --- a/authentik/admin/views/stages_invitations.py +++ b/authentik/admin/views/stages_invitations.py @@ -5,37 +5,15 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.http import HttpResponseRedirect -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.stages.invitation.forms import InvitationForm from authentik.stages.invitation.models import Invitation -class InvitationListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all invitations""" - - model = Invitation - permission_required = "authentik_stages_invitation.view_invitation" - template_name = "administration/stage_invitation/list.html" - ordering = "-expires" - search_fields = ["created_by__username", "expires", "fixed_data"] - - class InvitationCreateView( SuccessMessageMixin, LoginRequiredMixin, @@ -49,7 +27,7 @@ class InvitationCreateView( permission_required = "authentik_stages_invitation.add_invitation" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:stage-invitations") + success_url = "/" success_message = _("Successfully created Invitation") def form_valid(self, form): @@ -68,5 +46,5 @@ class InvitationDeleteView( permission_required = "authentik_stages_invitation.delete_invitation" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:stage-invitations") + success_url = "/" success_message = _("Successfully deleted Invitation") diff --git a/authentik/admin/views/stages_prompts.py b/authentik/admin/views/stages_prompts.py index e1f182336..d61b04850 100644 --- a/authentik/admin/views/stages_prompts.py +++ b/authentik/admin/views/stages_prompts.py @@ -4,42 +4,16 @@ from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy from django.utils.translation import gettext as _ -from django.views.generic import ListView, UpdateView -from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from django.views.generic import UpdateView +from guardian.mixins import PermissionRequiredMixin -from authentik.admin.views.utils import ( - DeleteMessageView, - SearchListMixin, - UserPaginateListMixin, -) +from authentik.admin.views.utils import DeleteMessageView from authentik.lib.views import CreateAssignPermView from authentik.stages.prompt.forms import PromptAdminForm from authentik.stages.prompt.models import Prompt -class PromptListView( - LoginRequiredMixin, - PermissionListMixin, - UserPaginateListMixin, - SearchListMixin, - ListView, -): - """Show list of all prompts""" - - model = Prompt - permission_required = "authentik_stages_prompt.view_prompt" - ordering = "order" - template_name = "administration/stage_prompt/list.html" - search_fields = [ - "field_key", - "label", - "type", - "placeholder", - ] - - class PromptCreateView( SuccessMessageMixin, LoginRequiredMixin, @@ -53,7 +27,7 @@ class PromptCreateView( permission_required = "authentik_stages_prompt.add_prompt" template_name = "generic/create.html" - success_url = reverse_lazy("authentik_admin:stage-prompts") + success_url = "/" success_message = _("Successfully created Prompt") @@ -70,7 +44,7 @@ class PromptUpdateView( permission_required = "authentik_stages_prompt.change_prompt" template_name = "generic/update.html" - success_url = reverse_lazy("authentik_admin:stage-prompts") + success_url = "/" success_message = _("Successfully updated Prompt") @@ -81,5 +55,5 @@ class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessag permission_required = "authentik_stages_prompt.delete_prompt" template_name = "generic/delete.html" - success_url = reverse_lazy("authentik_admin:stage-prompts") + success_url = "/" success_message = _("Successfully deleted Prompt") diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 20323b7d4..a96cc1c6f 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -136,8 +136,8 @@ router.register("stages/captcha", CaptchaStageViewSet) router.register("stages/consent", ConsentStageViewSet) router.register("stages/email", EmailStageViewSet) router.register("stages/identification", IdentificationStageViewSet) -router.register("stages/invitation", InvitationStageViewSet) router.register("stages/invitation/invitations", InvitationViewSet) +router.register("stages/invitation/stages", InvitationStageViewSet) router.register("stages/password", PasswordStageViewSet) router.register("stages/prompt/prompts", PromptViewSet) router.register("stages/prompt/stages", PromptStageViewSet) diff --git a/authentik/flows/transfer/common.py b/authentik/flows/transfer/common.py index 2b1173942..c3cd629d0 100644 --- a/authentik/flows/transfer/common.py +++ b/authentik/flows/transfer/common.py @@ -23,6 +23,7 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]: "verbose_name_plural", "object_type", "flow_set", + "promptstage_set", ) for to_remove_name in to_remove: if to_remove_name in data: diff --git a/authentik/stages/invitation/api.py b/authentik/stages/invitation/api.py index a96363f38..8bc103c56 100644 --- a/authentik/stages/invitation/api.py +++ b/authentik/stages/invitation/api.py @@ -34,7 +34,9 @@ class InvitationSerializer(ModelSerializer): "pk", "expires", "fixed_data", + "created_by", ] + depth = 2 class InvitationViewSet(ModelViewSet): @@ -42,6 +44,9 @@ class InvitationViewSet(ModelViewSet): queryset = Invitation.objects.all() serializer_class = InvitationSerializer + order = ["-expires"] + search_fields = ["created_by__username", "expires"] + filterset_fields = ["created_by__username", "expires"] def perform_create(self, serializer: InvitationSerializer): serializer.instance.created_by = self.request.user diff --git a/authentik/stages/prompt/api.py b/authentik/stages/prompt/api.py index b6bdbf30b..a318ff646 100644 --- a/authentik/stages/prompt/api.py +++ b/authentik/stages/prompt/api.py @@ -31,6 +31,8 @@ class PromptStageViewSet(ModelViewSet): class PromptSerializer(ModelSerializer): """Prompt Serializer""" + promptstage_set = StageSerializer(many=True, required=False) + class Meta: model = Prompt @@ -42,11 +44,13 @@ class PromptSerializer(ModelSerializer): "required", "placeholder", "order", + "promptstage_set", ] class PromptViewSet(ModelViewSet): """Prompt Viewset""" - queryset = Prompt.objects.all() + queryset = Prompt.objects.all().prefetch_related("promptstage_set") serializer_class = PromptSerializer + filterset_fields = ["field_key", "label", "type", "placeholder"] diff --git a/swagger.yaml b/swagger.yaml index 78110582e..0aba5c0e2 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -6830,78 +6830,21 @@ paths: required: true type: string format: uuid - /stages/invitation/: - get: - operationId: stages_invitation_list - description: InvitationStage Viewset - parameters: - - name: ordering - in: query - description: Which field to use when ordering the results. - required: false - type: string - - name: search - in: query - description: A search term. - required: false - type: string - - name: page - in: query - description: A page number within the paginated result set. - required: false - type: integer - - name: page_size - in: query - description: Number of results to return per page. - required: false - type: integer - responses: - '200': - description: '' - schema: - required: - - count - - results - type: object - properties: - count: - type: integer - next: - type: string - format: uri - x-nullable: true - previous: - type: string - format: uri - x-nullable: true - results: - type: array - items: - $ref: '#/definitions/InvitationStage' - tags: - - stages - post: - operationId: stages_invitation_create - description: InvitationStage Viewset - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/InvitationStage' - responses: - '201': - description: '' - schema: - $ref: '#/definitions/InvitationStage' - tags: - - stages - parameters: [] /stages/invitation/invitations/: get: operationId: stages_invitation_invitations_list description: Invitation Viewset parameters: + - name: created_by__username + in: query + description: '' + required: false + type: string + - name: expires + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -7024,9 +6967,76 @@ paths: required: true type: string format: uuid - /stages/invitation/{stage_uuid}/: + /stages/invitation/stages/: get: - operationId: stages_invitation_read + operationId: stages_invitation_stages_list + description: InvitationStage Viewset + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: A page number within the paginated result set. + required: false + type: integer + - name: page_size + in: query + description: Number of results to return per page. + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - count + - results + type: object + properties: + count: + type: integer + next: + type: string + format: uri + x-nullable: true + previous: + type: string + format: uri + x-nullable: true + results: + type: array + items: + $ref: '#/definitions/InvitationStage' + tags: + - stages + post: + operationId: stages_invitation_stages_create + description: InvitationStage Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/InvitationStage' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/InvitationStage' + tags: + - stages + parameters: [] + /stages/invitation/stages/{stage_uuid}/: + get: + operationId: stages_invitation_stages_read description: InvitationStage Viewset parameters: [] responses: @@ -7037,7 +7047,7 @@ paths: tags: - stages put: - operationId: stages_invitation_update + operationId: stages_invitation_stages_update description: InvitationStage Viewset parameters: - name: data @@ -7053,7 +7063,7 @@ paths: tags: - stages patch: - operationId: stages_invitation_partial_update + operationId: stages_invitation_stages_partial_update description: InvitationStage Viewset parameters: - name: data @@ -7069,7 +7079,7 @@ paths: tags: - stages delete: - operationId: stages_invitation_delete + operationId: stages_invitation_stages_delete description: InvitationStage Viewset parameters: [] responses: @@ -7216,6 +7226,26 @@ paths: operationId: stages_prompt_prompts_list description: Prompt Viewset parameters: + - name: field_key + in: query + description: '' + required: false + type: string + - name: label + in: query + description: '' + required: false + type: string + - name: type + in: query + description: '' + required: false + type: string + - name: placeholder + in: query + description: '' + required: false + type: string - name: ordering in: query description: Which field to use when ordering the results. @@ -11335,6 +11365,263 @@ definitions: type: string format: uuid x-nullable: true + Invitation: + description: Invitation Serializer + type: object + properties: + pk: + title: Invite uuid + type: string + format: uuid + readOnly: true + expires: + title: Expires + type: string + format: date-time + x-nullable: true + fixed_data: + title: Fixed data + description: Optional fixed data to enforce on user enrollment. + type: object + created_by: + description: Custom User model to allow easier adding o f user-based settings + required: + - password + - username + - name + type: object + properties: + id: + title: ID + type: integer + readOnly: true + password: + title: Password + type: string + maxLength: 128 + minLength: 1 + last_login: + title: Last login + type: string + format: date-time + x-nullable: true + username: + title: Username + description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ + only. + type: string + pattern: ^[\w.@+-]+$ + maxLength: 150 + minLength: 1 + first_name: + title: First name + type: string + maxLength: 150 + last_name: + title: Last name + type: string + maxLength: 150 + email: + title: Email address + type: string + format: email + maxLength: 254 + is_active: + title: Active + description: Designates whether this user should be treated as active. + Unselect this instead of deleting accounts. + type: boolean + date_joined: + title: Date joined + type: string + format: date-time + uuid: + title: Uuid + type: string + format: uuid + readOnly: true + name: + title: Name + description: User's display name. + type: string + minLength: 1 + password_change_date: + title: Password change date + type: string + format: date-time + readOnly: true + attributes: + title: Attributes + type: object + groups: + description: '' + type: array + items: + description: Groups are a generic way of categorizing users to apply + permissions, or some other label, to those users. A user can belong + to any number of groups. A user in a group automatically has all the + permissions granted to that group. For example, if the group 'Site + editors' has the permission can_edit_home_page, any user in that group + will have that permission. Beyond permissions, groups are a convenient + way to categorize users to apply some label, or extended functionality, + to them. For example, you could create a group 'Special users', and + you could write code that would do special things to those users -- + such as giving them access to a members-only portion of your site, + or sending them members-only email messages. + required: + - name + type: object + properties: + id: + title: ID + type: integer + readOnly: true + name: + title: Name + type: string + maxLength: 150 + minLength: 1 + permissions: + type: array + items: + type: integer + uniqueItems: true + readOnly: true + user_permissions: + description: '' + type: array + items: + description: "The permissions system provides a way to assign permissions\ + \ to specific users and groups of users. The permission system is\ + \ used by the Django admin site, but may also be useful in your own\ + \ code. The Django admin site uses permissions as follows: - The \"\ + add\" permission limits the user's ability to view the \"add\" form\ + \ and add an object. - The \"change\" permission limits a user's ability\ + \ to view the change list, view the \"change\" form and change an\ + \ object. - The \"delete\" permission limits the ability to delete\ + \ an object. - The \"view\" permission limits the ability to view\ + \ an object. Permissions are set globally per type of object, not\ + \ per specific object instance. It is possible to say \"Mary may change\ + \ news stories,\" but it's not currently possible to say \"Mary may\ + \ change news stories, but only the ones she created herself\" or\ + \ \"Mary may only change news stories that have a certain status or\ + \ publication date.\" The permissions listed above are automatically\ + \ created for each model." + required: + - name + - codename + - content_type + type: object + properties: + id: + title: ID + type: integer + readOnly: true + name: + title: Name + type: string + maxLength: 255 + minLength: 1 + codename: + title: Codename + type: string + maxLength: 100 + minLength: 1 + content_type: + title: Content type + type: integer + readOnly: true + sources: + description: '' + type: array + items: + description: Base Authentication source, i.e. an OAuth Provider, SAML + Remote or LDAP Server + required: + - name + - slug + type: object + properties: + pbm_uuid: + title: Pbm uuid + type: string + format: uuid + readOnly: true + name: + title: Name + description: Source's display Name. + type: string + minLength: 1 + slug: + title: Slug + description: Internal source name, used in URLs. + type: string + format: slug + pattern: ^[-a-zA-Z0-9_]+$ + maxLength: 50 + minLength: 1 + enabled: + title: Enabled + type: boolean + authentication_flow: + title: Authentication flow + description: Flow to use when authenticating existing users. + type: string + format: uuid + x-nullable: true + enrollment_flow: + title: Enrollment flow + description: Flow to use when enrolling new users. + type: string + format: uuid + x-nullable: true + policies: + type: array + items: + type: string + format: uuid + readOnly: true + uniqueItems: true + property_mappings: + type: array + items: + type: string + format: uuid + uniqueItems: true + readOnly: true + ak_groups: + description: '' + type: array + items: + description: Custom Group model which supports a basic hierarchy + required: + - name + - parent + type: object + properties: + group_uuid: + title: Group uuid + type: string + format: uuid + readOnly: true + name: + title: Name + type: string + maxLength: 80 + minLength: 1 + is_superuser: + title: Is superuser + description: Users added to this group will be superusers. + type: boolean + attributes: + title: Attributes + type: object + parent: + title: Parent + type: string + format: uuid + readOnly: true + readOnly: true InvitationStage: description: InvitationStage Serializer required: @@ -11373,24 +11660,6 @@ definitions: no Invitation is given. By default this Stage will cancel the Flow when no invitation is given. type: boolean - Invitation: - description: Invitation Serializer - type: object - properties: - pk: - title: Invite uuid - type: string - format: uuid - readOnly: true - expires: - title: Expires - type: string - format: date-time - x-nullable: true - fixed_data: - title: Fixed data - description: Optional fixed data to enforce on user enrollment. - type: object PasswordStage: description: PasswordStage Serializer required: @@ -11496,6 +11765,11 @@ definitions: type: integer maximum: 2147483647 minimum: -2147483648 + promptstage_set: + description: '' + type: array + items: + $ref: '#/definitions/Stage' PromptStage: description: PromptStage Serializer required: diff --git a/web/src/api/Invitations.ts b/web/src/api/Invitations.ts new file mode 100644 index 000000000..e28bd8c25 --- /dev/null +++ b/web/src/api/Invitations.ts @@ -0,0 +1,27 @@ +import { DefaultClient, QueryArguments, AKResponse } from "./Client"; +import { EventContext } from "./Events"; +import { User } from "./Users"; + +export class Invitation { + + pk: string; + expires: number; + fixed_date: EventContext; + created_by: User; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["stages", "invitation", "invitations", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["stages", "invitation", "invitations"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/stages/invitations/${rest}`; + } +} diff --git a/web/src/api/Prompts.ts b/web/src/api/Prompts.ts new file mode 100644 index 000000000..b688f4812 --- /dev/null +++ b/web/src/api/Prompts.ts @@ -0,0 +1,30 @@ +import { DefaultClient, QueryArguments, AKResponse } from "./Client"; +import { Stage } from "./Flows"; + +export class Prompt { + + pk: string; + field_key: string; + label: string; + type: string; + required: boolean; + placeholder: string; + order: number; + promptstage_set: Stage[]; + + constructor() { + throw Error(); + } + + static get(pk: string): Promise { + return DefaultClient.fetch(["stages", "prompt", "prompts", pk]); + } + + static list(filter?: QueryArguments): Promise> { + return DefaultClient.fetch>(["stages", "prompt", "prompts"], filter); + } + + static adminUrl(rest: string): string { + return `/administration/stages/prompts/${rest}`; + } +} diff --git a/web/src/elements/sidebar/Sidebar.ts b/web/src/elements/sidebar/Sidebar.ts index da2203027..bf639b84e 100644 --- a/web/src/elements/sidebar/Sidebar.ts +++ b/web/src/elements/sidebar/Sidebar.ts @@ -29,7 +29,7 @@ export class SidebarItem { this.condition = async () => true; this.activeMatchers = []; if (this.path) { - this.activeMatchers.push(new RegExp(`^${this.path}`)); + this.activeMatchers.push(new RegExp(`^${this.path}$`)); } } diff --git a/web/src/interfaces/AdminInterface.ts b/web/src/interfaces/AdminInterface.ts index 704a212d3..b5c9f3325 100644 --- a/web/src/interfaces/AdminInterface.ts +++ b/web/src/interfaces/AdminInterface.ts @@ -41,8 +41,8 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [ new SidebarItem("Flows").children( new SidebarItem("Flows", "/flow/flows").activeWhen(`^/flow/flows/(?${SLUG_REGEX})$`), new SidebarItem("Stages", "/flow/stages"), - new SidebarItem("Prompts", "/administration/stages_prompts/"), - new SidebarItem("Invitations", "/administration/stages/invitations/"), + new SidebarItem("Prompts", "/flow/stages/prompts"), + new SidebarItem("Invitations", "/flow/stages/invitations"), ).when((): Promise => { return User.me().then(u => u.is_superuser); }), diff --git a/web/src/pages/stages/InvitationListPage.ts b/web/src/pages/stages/InvitationListPage.ts new file mode 100644 index 000000000..e53cfd293 --- /dev/null +++ b/web/src/pages/stages/InvitationListPage.ts @@ -0,0 +1,72 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import { TableColumn } from "../../elements/table/Table"; +import { Invitation } from "../../api/Invitations"; + +@customElement("ak-stage-invitation-list") +export class InvitationListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Invitations"); + } + pageDescription(): string { + return gettext("Create Invitation Links to enroll Users, and optionally force specific attributes of their account."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-migration"); + } + + @property() + order = "expires"; + + apiEndpoint(page: number): Promise> { + return Invitation.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("ID", "pk"), + new TableColumn("Created by", "created_by"), + new TableColumn("Expiry"), + new TableColumn(""), + ]; + } + + row(item: Invitation): TemplateResult[] { + return [ + html`${item.pk}`, + html`${item.created_by.username}`, + html`${new Date(item.expires * 1000).toLocaleString()}`, + html` + + + ${gettext("Delete")} + +
+
`, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
+
+ ${super.renderToolbar()} + `; + } +} diff --git a/web/src/pages/stages/PromptListPage.ts b/web/src/pages/stages/PromptListPage.ts new file mode 100644 index 000000000..37bc2c8f6 --- /dev/null +++ b/web/src/pages/stages/PromptListPage.ts @@ -0,0 +1,84 @@ +import { gettext } from "django"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { AKResponse } from "../../api/Client"; +import { TablePage } from "../../elements/table/TablePage"; + +import "../../elements/buttons/ModalButton"; +import "../../elements/buttons/SpinnerButton"; +import { TableColumn } from "../../elements/table/Table"; +import { Prompt } from "../../api/Prompts"; + +@customElement("ak-stage-prompt-list") +export class PromptListPage extends TablePage { + searchEnabled(): boolean { + return true; + } + pageTitle(): string { + return gettext("Prompts"); + } + pageDescription(): string { + return gettext("Single Prompts that can be used for Prompt Stages."); + } + pageIcon(): string { + return gettext("pf-icon pf-icon-plugged"); + } + + @property() + order = "order"; + + apiEndpoint(page: number): Promise> { + return Prompt.list({ + ordering: this.order, + page: page, + search: this.search || "", + }); + } + + columns(): TableColumn[] { + return [ + new TableColumn("Field", "field_key"), + new TableColumn("Label", "label"), + new TableColumn("Type", "type"), + new TableColumn("Order", "order"), + new TableColumn("Stages"), + new TableColumn(""), + ]; + } + + row(item: Prompt): TemplateResult[] { + return [ + html`${item.field_key}`, + html`${item.label}`, + html`${item.type}`, + html`${item.order}`, + html`${item.promptstage_set.map((stage) => { + return html`
  • ${stage.name}
  • `; + })}`, + html` + + + ${gettext("Edit")} + +
    +
    + + + ${gettext("Delete")} + +
    +
    `, + ]; + } + + renderToolbar(): TemplateResult { + return html` + + + ${gettext("Create")} + +
    +
    + ${super.renderToolbar()} + `; + } +} diff --git a/web/src/routes.ts b/web/src/routes.ts index c33ce2f76..d30eb5f0f 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -22,6 +22,8 @@ import "./pages/providers/ProviderViewPage"; import "./pages/sources/SourcesListPage"; import "./pages/sources/SourceViewPage"; import "./pages/stages/StageListPage"; +import "./pages/stages/InvitationListPage"; +import "./pages/stages/PromptListPage"; import "./pages/system-tasks/SystemTaskListPage"; import "./pages/tokens/TokenListPage"; import "./pages/users/UserListPage"; @@ -49,6 +51,8 @@ export const ROUTES: Route[] = [ new Route(new RegExp("^/identity/groups$"), html``), new Route(new RegExp("^/identity/users$"), html``), new Route(new RegExp("^/core/tokens$"), html``), + new Route(new RegExp("^/flow/stages/invitations$"), html``), + new Route(new RegExp("^/flow/stages/prompts$"), html``), new Route(new RegExp("^/flow/stages$"), html``), new Route(new RegExp("^/flow/flows$"), html``), new Route(new RegExp(`^/flow/flows/(?${SLUG_REGEX})$`)).then((args) => {