web: add outpost list page

This commit is contained in:
Jens Langhammer 2021-02-08 19:04:19 +01:00
parent 5d460a2537
commit 820f658b49
8 changed files with 247 additions and 9 deletions

View File

@ -12,10 +12,7 @@ from django.utils.translation import gettext as _
from django.views.generic import UpdateView from django.views.generic import UpdateView
from guardian.mixins import PermissionRequiredMixin from guardian.mixins import PermissionRequiredMixin
from authentik.admin.views.utils import ( from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
BackSuccessUrlMixin,
DeleteMessageView,
)
from authentik.lib.views import CreateAssignPermView from authentik.lib.views import CreateAssignPermView
from authentik.outposts.forms import OutpostForm from authentik.outposts.forms import OutpostForm
from authentik.outposts.models import Outpost, OutpostConfig from authentik.outposts.models import Outpost, OutpostConfig

View File

@ -51,6 +51,13 @@ class OutpostViewSet(ModelViewSet):
queryset = Outpost.objects.all() queryset = Outpost.objects.all()
serializer_class = OutpostSerializer serializer_class = OutpostSerializer
filterset_fields = {
"providers": ["isnull"],
}
search_fields = [
"name",
"providers__name",
]
@swagger_auto_schema(responses={200: OutpostHealthSerializer(many=True)}) @swagger_auto_schema(responses={200: OutpostHealthSerializer(many=True)})
@action(methods=["GET"], detail=True) @action(methods=["GET"], detail=True)

View File

@ -1789,6 +1789,11 @@ paths:
operationId: outposts_outposts_list operationId: outposts_outposts_list
description: Outpost Viewset description: Outpost Viewset
parameters: parameters:
- name: providers__isnull
in: query
description: ''
required: false
type: string
- name: ordering - name: ordering
in: query in: query
description: Which field to use when ordering the results. description: Which field to use when ordering the results.
@ -1914,11 +1919,11 @@ paths:
/outposts/outposts/{uuid}/health/: /outposts/outposts/{uuid}/health/:
get: get:
operationId: outposts_outposts_health operationId: outposts_outposts_health
description: Outpost Viewset description: Get outposts current health
parameters: [] parameters: []
responses: responses:
'200': '200':
description: '' description: Outpost health status
schema: schema:
description: '' description: ''
type: array type: array
@ -4457,7 +4462,7 @@ paths:
/providers/oauth2/{id}/setup_urls/: /providers/oauth2/{id}/setup_urls/:
get: get:
operationId: providers_oauth2_setup_urls operationId: providers_oauth2_setup_urls
description: Return metadata as XML string description: Get Providers setup URLs
parameters: [] parameters: []
responses: responses:
'200': '200':
@ -8179,11 +8184,15 @@ definitions:
type: string type: string
format: uuid format: uuid
x-nullable: true x-nullable: true
token_identifier:
title: Token identifier
type: string
readOnly: true
_config: _config:
title: config title: config
type: object type: object
OutpostHealth: OutpostHealth:
description: '' description: Outpost health status
type: object type: object
properties: properties:
last_seen: last_seen:
@ -8191,6 +8200,20 @@ definitions:
type: string type: string
format: date-time format: date-time
readOnly: true readOnly: true
version:
title: Version
type: string
readOnly: true
minLength: 1
version_should:
title: Version should
type: string
readOnly: true
minLength: 1
version_outdated:
title: Version outdated
type: boolean
readOnly: true
OpenIDConnectConfiguration: OpenIDConnectConfiguration:
title: Oidc configuration title: Oidc configuration
description: rest_framework Serializer for OIDC Configuration description: rest_framework Serializer for OIDC Configuration

39
web/src/api/Outposts.ts Normal file
View File

@ -0,0 +1,39 @@
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
import { Provider } from "./Providers";
export interface OutpostHealth {
last_seen: number;
version: string;
version_should: string;
version_outdated: boolean;
}
export class Outpost {
pk: string;
name: string;
providers: Provider[];
service_connection?: string;
_config: QueryArguments;
token_identifier: string;
constructor() {
throw Error();
}
static get(pk: string): Promise<Outpost> {
return DefaultClient.fetch<Outpost>(["outposts", "outposts", pk]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Outpost>> {
return DefaultClient.fetch<PBResponse<Outpost>>(["outposts", "outposts"], filter);
}
static health(pk: string): Promise<OutpostHealth[]> {
return DefaultClient.fetch<OutpostHealth[]>(["outposts", "outposts", pk, "health"]);
}
static adminUrl(rest: string): string {
return `/administration/outposts/${rest}`;
}
}

View File

@ -27,7 +27,7 @@ export const SIDEBAR_ITEMS: SidebarItem[] = [
`^/sources/(?<slug>${SLUG_REGEX})$`, `^/sources/(?<slug>${SLUG_REGEX})$`,
), ),
new SidebarItem("Providers", "/providers"), new SidebarItem("Providers", "/providers"),
new SidebarItem("Outposts", "/administration/outposts/"), new SidebarItem("Outposts", "/outposts"),
new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"), new SidebarItem("Outpost Service Connections", "/administration/outpost_service_connections/"),
).when((): Promise<boolean> => { ).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser); return User.me().then(u => u.is_superuser);

View File

@ -0,0 +1,49 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { Outpost } from "../../api/Outposts";
import { COMMON_STYLES } from "../../common/styles";
@customElement("ak-outpost-health")
export class OutpostHealth extends LitElement {
@property()
outpostId?: string;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
if (!this.outpostId) {
return html`<ak-spinner></ak-spinner>`;
}
return html`<ul>${until(Outpost.health(this.outpostId).then((oh) => {
if (oh.length === 0) {
return html`<li>
<ul>
<li role="cell">
<i class="fas fa-question-circle"></i>&nbsp;${gettext("Not available")}
</li>
</ul>
</li>`;
}
return oh.map((h) => {
return html`<li>
<ul>
<li role="cell">
<i class="fas fa-check pf-m-success"></i>&nbsp;${gettext(`Last seen: ${new Date(h.last_seen * 1000).toLocaleTimeString()}`)}
</li>
<li role="cell">
${h.version_outdated ?
html`<i class="fas fa-times pf-m-danger"></i>&nbsp;
${gettext(`${h.version}, should be ${h.version_should}`)}` :
html`<i class="fas fa-check pf-m-success"></i>&nbsp;${gettext(`Version: ${h.version}`)}`}
</li>
</ul>
</li>`;
});
}), html`<ak-spinner></ak-spinner>`)}</ul>`;
}
}

View File

@ -0,0 +1,121 @@
import { gettext } from "django";
import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html";
import { PBResponse } from "../../api/Client";
import { Outpost } from "../../api/Outposts";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import "./OutpostHealth";
import "../../elements/buttons/SpinnerButton";
import "../../elements/buttons/ModalButton";
@customElement("ak-outpost-list")
export class OutpostListPage extends TablePage<Outpost> {
pageTitle(): string {
return "Outposts";
}
pageDescription(): string | undefined {
return "Outposts are deployments of authentik components to support different environments and protocols, like reverse proxies.";
}
pageIcon(): string {
return "pf-icon pf-icon-zone";
}
searchEnabled(): boolean {
return true;
}
apiEndpoint(page: number): Promise<PBResponse<Outpost>> {
return Outpost.list({
ordering: this.order,
page: page,
search: this.search || "",
});
}
columns(): TableColumn[] {
return [
new TableColumn("Name", "name"),
new TableColumn("Providers"),
new TableColumn("Health and Version"),
new TableColumn(""),
];
}
@property()
order = "name";
row(item: Outpost): TemplateResult[] {
return [
html`${item.name}`,
html`<ul>${item.providers.map((p) => {
return html`<li><a href="#/providers/${p.pk}">${p.name}</a></li>`;
})}</ul>`,
html`<ak-outpost-health outpostId=${item.pk}></ak-outpost-health>`,
html`
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/update`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
${gettext("Edit")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${Outpost.adminUrl(`${item.pk}/delete`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
${gettext("Delete")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button>
<button slot="trigger" class="pf-c-button pf-m-tertiary">
${gettext('View Deployment Info')}
</button>
<div slot="modal">
<div class="pf-c-modal-box__header">
<h1 class="pf-c-title pf-m-2xl" id="modal-title">${gettext('Outpost Deployment Info')}</h1>
</div>
<div class="pf-c-modal-box__body" id="modal-description">
<p><a href="https://goauthentik.io/docs/outposts/outposts/#deploy">${gettext('View deployment documentation')}</a></p>
<form class="pf-c-form">
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_HOST</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="${document.location.toString()}" />
</div>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_TOKEN</span>
</label>
<div>
<ak-token-copy-button identifier="${item.token_identifier}">
${gettext('Click to copy token')}
</ak-token-copy-button>
</div>
</div>
<h3>${gettext('If your authentik Instance is using a self-signed certificate, set this value.')}</h3>
<div class="pf-c-form__group">
<label class="pf-c-form__label" for="help-text-simple-form-name">
<span class="pf-c-form__label-text">AUTHENTIK_INSECURE</span>
</label>
<input class="pf-c-form-control" readonly type="text" value="true" />
</div>
</form>
</div>
<footer class="pf-c-modal-box__footer pf-m-align-left">
<a class="pf-c-button pf-m-primary">${gettext('Close')}</a>
</footer>
</div>
</ak-modal-button>`,
];
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${Outpost.adminUrl("create/")}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Create")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View File

@ -14,6 +14,7 @@ import "./pages/events/RuleListPage";
import "./pages/providers/ProviderListPage"; import "./pages/providers/ProviderListPage";
import "./pages/providers/ProviderViewPage"; import "./pages/providers/ProviderViewPage";
import "./pages/property-mappings/PropertyMappingListPage"; import "./pages/property-mappings/PropertyMappingListPage";
import "./pages/outposts/OutpostListPage";
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
// Prevent infinite Shell loops // Prevent infinite Shell loops
@ -42,4 +43,5 @@ export const ROUTES: Route[] = [
new Route(new RegExp("^/events/transports$"), html`<ak-event-transport-list></ak-event-transport-list>`), new Route(new RegExp("^/events/transports$"), html`<ak-event-transport-list></ak-event-transport-list>`),
new Route(new RegExp("^/events/rules$"), html`<ak-event-rule-list></ak-event-rule-list>`), new Route(new RegExp("^/events/rules$"), html`<ak-event-rule-list></ak-event-rule-list>`),
new Route(new RegExp("^/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`), new Route(new RegExp("^/property-mappings$"), html`<ak-property-mapping-list></ak-property-mapping-list>`),
new Route(new RegExp("^/outposts$"), html`<ak-outpost-list></ak-outpost-list>`),
]; ];