web: more admin overview components

This commit is contained in:
Jens Langhammer 2020-12-01 22:17:07 +01:00
parent b218ded241
commit 1779b4d888
9 changed files with 313 additions and 323 deletions

View File

@ -133,6 +133,42 @@ paths:
tags:
- audit
parameters: []
/audit/events/top_per_user/:
get:
operationId: audit_events_top_per_user
description: Get the top_n events grouped by user count
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: Response object of Event's top_per_user
schema:
description: ''
type: array
items:
$ref: '#/definitions/EventTopPerUserSerialier'
tags:
- audit
parameters: []
/audit/events/{event_uuid}/:
get:
operationId: audit_events_read
@ -6509,6 +6545,25 @@ definitions:
type: string
format: date-time
readOnly: true
EventTopPerUserSerialier:
description: Response object of Event's top_per_user
required:
- application
- counted_events
- unique_users
type: object
properties:
application:
title: Application
type: object
additionalProperties:
type: string
counted_events:
title: Counted events
type: integer
unique_users:
title: Unique users
type: integer
Application:
description: Application Serializer
required:

16
web/src/api/events.ts Normal file
View File

@ -0,0 +1,16 @@
import { DefaultClient } from "./client";
export class AuditEvent {
//audit/events/top_per_user/?filter_action=authorize_application
static topForUser(action: string): Promise<TopNEvent[]> {
return DefaultClient.fetch<TopNEvent[]>(["audit", "events", "top_per_user"], {
"filter_action": action,
});
}
}
export interface TopNEvent {
application: { [key: string]: string};
counted_events: number;
unique_users: number;
}

View File

@ -0,0 +1,48 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
@customElement("pb-aggregate-card")
export class AggregateCard extends LitElement {
@property()
icon?: string;
@property()
header?: string;
@property()
headerLink?: string;
static get styles(): CSSResult[] {
return COMMON_STYLES.concat([css`
.center-value {
font-size: var(--pf-global--icon--FontSize--lg);
text-align: center;
}
.subtext {
font-size: var(--pf-global--FontSize--sm);
}
`]);
}
renderInner(): TemplateResult {
return html`<slot></slot>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card pf-c-card-aggregate">
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="${this.icon}"></i> ${this.header ? gettext(this.header) : ""}
</div>
${this.headerLink ? html`<a href="${this.headerLink}">
<i class="fa fa-external-link-alt"> </i>
</a>` : ""}
</div>
<div class="pf-c-card__body center-value">
${this.renderInner()}
</div>
</div>`;
}
}

View File

@ -0,0 +1,25 @@
import { customElement, html, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { AggregateCard } from "./AggregateCard";
@customElement("pb-aggregate-card-promise")
export class AggregatePromiseCard extends AggregateCard {
@property()
promise?: Promise<string>;
promiseProxy(): Promise<TemplateResult> {
if (!this.promise) {
return new Promise<TemplateResult>(() => html``);
}
return this.promise.then(s => {
return html`<i class="fa fa-check-circle"></i> ${s}`;
});
}
renderInner(): TemplateResult {
return html`<p class="center-value">
${until(this.promiseProxy(), html`<pb-spinner size="large"></pb-spinner>`)}
</p>`;
}
}

View File

@ -13,6 +13,8 @@ import "./elements/sidebar/SidebarUser";
import "./elements/table/TablePagination";
import "./elements/AdminLoginsChart";
import "./elements/cards/AggregateCard";
import "./elements/cards/AggregatePromiseCard";
import "./elements/CodeMirror";
import "./elements/Messages";
import "./elements/Spinner";
@ -23,7 +25,8 @@ import "./pages/generic/SiteShell";
import "./pages/router/RouterOutlet";
import "./pages/AdminOverviewPage";
import "./pages/admin-overview/AdminOverviewPage";
import "./pages/admin-overview/TopApplicationsTable";
import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage";
import "./pages/LibraryPage";

View File

@ -1,129 +0,0 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { until } from "lit-html/directives/until";
import { AdminOverview } from "../api/admin_overview";
import { DefaultClient } from "../api/client";
import { User } from "../api/user";
import { COMMON_STYLES } from "../common/styles";
@customElement("pb-aggregate-card")
export class AggregateCard extends LitElement {
@property()
icon?: string;
@property()
header?: string;
@property()
headerLink?: string;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
renderInner(): TemplateResult {
return html`<slot></slot>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" >
<div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
<div class="pf-c-card__header-main">
<i class="${this.icon}"></i> ${this.header ? gettext(this.header) : ""}
</div>
${this.headerLink ? html`<a href="${this.headerLink}">
<i class="fa fa-external-link-alt"> </i>
</a>` : ""}
</div>
<div class="pf-c-card__body">
${this.renderInner()}
</div>
</div>`;
}
}
@customElement("pb-aggregate-card-promise")
export class AggregatePromiseCard extends AggregateCard {
@property()
promise?: Promise<string>;
renderInner(): TemplateResult {
return html`<p class="pb-aggregate-card">
${until(this.promise, html`<pb-spinner></pb-spinner>`)}
</p>`;
}
}
@customElement("pb-admin-overview")
export class AdminOverviewPage extends LitElement {
@property()
data?: AdminOverview;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
firstUpdated(): void {
AdminOverview.get().then(value => this.data = value);
}
render(): TemplateResult {
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>${gettext("System Overview")}</h1>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<pb-aggregate-card icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;">
<pb-admin-logins-chart url="${DefaultClient.makeUrl(["admin", "metrics"])}"></pb-admin-logins-chart>
</pb-aggregate-card>
<pb-aggregate-card icon="pf-icon pf-icon-server" header="Workers">
${this.data ?
this.data?.worker_count < 1 ?
html`<p class="pb-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> ${this.data.worker_count}
</p>
<p>${gettext("No workers connected.")}</p>` :
html`<p class="pb-aggregate-card">
<i class="fa fa-check-circle"></i> ${this.data.worker_count}
</p>`
: html`<pb-spinner></pb-spinner>`}
</pb-aggregate-card>
<pb-aggregate-card icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/administration/providers/">
${this.data ?
this.data?.providers_without_application < 1 ?
html`<p class="pb-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> 0
</p>
<p>${gettext("At least one Provider has no application assigned.")}</p>` :
html`<p class="pb-aggregate-card">
<i class="fa fa-check-circle"></i> 0
</p>`
: html`<pb-spinner></pb-spinner>`}
</pb-aggregate-card>
<pb-aggregate-card icon="pf-icon pf-icon-plugged" header="Policies" headerLink="#/administration/policies/">
${this.data ?
this.data?.policies_without_binding < 1 ?
html`<p class="pb-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> 0
</p>
<p>${gettext("Policies without binding exist.")}</p>` :
html`<p class="pb-aggregate-card">
<i class="fa fa-check-circle"></i> 0
</p>`
: html`<pb-spinner></pb-spinner>`}
</pb-aggregate-card>
<pb-aggregate-card-promise
icon="pf-icon pf-icon-user"
header="Users"
headerLink="#/administration/users/"
.promise=${User.count()}>
</pb-aggregate-card-promise>
</div>
</section>`;
}
}

View File

@ -0,0 +1,116 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { AdminOverview } from "../../api/admin_overview";
import { DefaultClient } from "../../api/client";
import { User } from "../../api/user";
import { COMMON_STYLES } from "../../common/styles";
import { AggregatePromiseCard } from "../../elements/cards/AggregatePromiseCard";
@customElement("pb-admin-status-card")
export class AdminStatusCard extends AggregatePromiseCard {
@property()
value?: number;
@property()
warningText?: string;
@property()
lessThanThreshold?: number;
renderNone(): TemplateResult {
return html`<pb-spinner size="large"></pb-spinner>`;
}
renderGood(): TemplateResult {
return html`<p class="pb-aggregate-card">
<i class="fa fa-check-circle"></i> ${this.value}
</p>`;
}
renderBad(): TemplateResult {
return html`<p class="pb-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> ${this.value}
</p>
<p class="subtext">${this.warningText ? gettext(this.warningText) : ""}</p>`;
}
renderInner(): TemplateResult {
if (!this.value) {
return this.renderNone();
}
return html``;
}
}
@customElement("pb-admin-overview")
export class AdminOverviewPage extends LitElement {
@property()
data?: AdminOverview;
@property()
users?: Promise<number>;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
firstUpdated(): void {
AdminOverview.get().then(value => this.data = value);
this.users = User.count();
}
render(): TemplateResult {
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>${gettext("System Overview")}</h1>
</div>
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<pb-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Logins over the last 24 hours" style="grid-column-end: span 3;grid-row-end: span 2;">
<pb-admin-logins-chart url="${DefaultClient.makeUrl(["admin", "metrics"])}"></pb-admin-logins-chart>
</pb-aggregate-card>
<pb-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Apps with most usage" style="grid-column-end: span 2;grid-row-end: span 3;">
<pb-top-applications-table></pb-top-applications-table>
</pb-aggregate-card>
<pb-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-server" header="Workers">
</pb-aggregate-card>
<pb-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Providers" headerLink="#/administration/providers/">
${this.data ?
this.data?.providers_without_application > 1 ?
html`<p class="pb-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> 0
</p>
<p class="subtext">${gettext("At least one Provider has no application assigned.")}</p>` :
html`<p class="pb-aggregate-card">
<i class="fa fa-check-circle"></i> 0
</p>`
: html`<pb-spinner size="large"></pb-spinner>`}
</pb-aggregate-card>
<pb-aggregate-card class="pf-l-gallery__item pf-m-4-col" icon="pf-icon pf-icon-plugged" header="Policies" headerLink="#/administration/policies/">
${this.data ?
this.data?.policies_without_binding > 1 ?
html`<p class="pb-aggregate-card">
<i class="fa fa-exclamation-triangle"></i> 0
</p>
<p class="subtext">${gettext("Policies without binding exist.")}</p>` :
html`<p class="pb-aggregate-card">
<i class="fa fa-check-circle"></i> 0
</p>`
: html`<pb-spinner size="large"></pb-spinner>`}
</pb-aggregate-card>
<pb-aggregate-card-promise
icon="pf-icon pf-icon-user"
header="Users"
headerLink="#/administration/users/"
.promise=${this.users}>
</pb-aggregate-card-promise>
</div>
</section>`;
}
}

View File

@ -0,0 +1,49 @@
import { gettext } from "django";
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { AuditEvent, TopNEvent } from "../../api/events";
import { COMMON_STYLES } from "../../common/styles";
@customElement("pb-top-applications-table")
export class TopApplicationsTable extends LitElement {
@property()
topN?: TopNEvent[];
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
firstUpdated(): void {
AuditEvent.topForUser("authorize_application").then(events => this.topN = events);
}
renderRow(event: TopNEvent): TemplateResult {
return html`<tr role="row">
<td role="cell">
${event.application.name}
</td>
<td role="cell">
${event.counted_events}
</td>
<td role="cell">
<progress value="${event.counted_events}" max="${this.topN ? this.topN[0].counted_events : 0}"></progress>
</td>
</tr>`;
}
render(): TemplateResult {
return html`<table class="pf-c-table pf-m-compact" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">${gettext("Application")}</th>
<th role="columnheader" scope="col">${gettext("Logins")}</th>
<th role="columnheader" scope="col"></th>
</tr>
</thead>
<tbody role="rowgroup">
${this.topN ? this.topN.map((e) => this.renderRow(e)) : html`<pb-spinner></pb-spinner>`}
</tbody>
</table>`;
}
}

View File

@ -66,193 +66,6 @@ select[multiple] {
height: initial;
}
/* Selector */
.selector {
display: flex;
width: 100%;
height: 45vh;
}
.selector .selector-filter {
display: flex;
align-items: center;
}
.selector .selector-filter label {
margin: 0 8px 0 0;
}
.selector .selector-filter input {
width: auto;
min-height: 0;
flex: 1 1;
}
.selector-available,
.selector-chosen {
width: auto;
flex: 1 1;
display: flex;
flex-direction: column;
}
.selector select {
width: 100%;
flex: 1 0 auto;
margin-bottom: 5px;
}
.selector ul.selector-chooser {
width: 26px;
height: 52px;
padding: 2px 0;
margin: auto 15px;
border-radius: 20px;
transform: translateY(-10px);
list-style: none;
}
.selector-add,
.selector-remove {
width: 20px;
height: 20px;
background-size: 20px auto;
}
.selector-add {
background-position: 0 -120px;
}
.selector-remove {
background-position: 0 -80px;
}
a.selector-chooseall,
a.selector-clearall {
align-self: center;
}
.stacked {
flex-direction: column;
max-width: 480px;
}
.stacked > * {
flex: 0 1 auto;
}
.stacked select {
margin-bottom: 0;
}
.stacked .selector-available,
.stacked .selector-chosen {
width: auto;
}
.stacked ul.selector-chooser {
width: 52px;
height: 26px;
padding: 0 2px;
margin: 15px auto;
transform: none;
}
.stacked .selector-chooser li {
padding: 3px;
}
.stacked .selector-add,
.stacked .selector-remove {
background-size: 20px auto;
}
.stacked .selector-add {
background-position: 0 -40px;
}
.stacked .active.selector-add {
background-position: 0 -60px;
}
.stacked .selector-remove {
background-position: 0 0;
}
.stacked .active.selector-remove {
background-position: 0 -20px;
}
.help-tooltip,
.selector .help-icon {
display: none;
}
form .form-row p.datetime {
width: 100%;
}
.datetime input {
width: 50%;
max-width: 120px;
}
.datetime span {
font-size: 13px;
}
.datetime .timezonewarning {
display: block;
font-size: 11px;
color: #999;
}
.datetimeshortcuts {
color: #ccc;
}
.inline-group {
overflow: auto;
}
.selector-add,
.selector-remove {
width: 16px;
height: 16px;
display: block;
text-indent: -3000px;
overflow: hidden;
cursor: default;
opacity: 0.3;
}
.active.selector-add,
.active.selector-remove {
opacity: 1;
}
.active.selector-add:hover,
.active.selector-remove:hover {
cursor: pointer;
}
.selector-add {
background: url(../admin/img/selector-icons.svg) 0 -96px no-repeat;
}
.active.selector-add:focus,
.active.selector-add:hover {
background-position: 0 -112px;
}
.selector-remove {
background: url(../admin/img/selector-icons.svg) 0 -64px no-repeat;
}
input[data-is-monospace] {
font-family: monospace;
}
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
@ -291,12 +104,6 @@ input[data-is-monospace] {
white-space: pre-wrap;
}
/* Aggregate Cards */
.pb-aggregate-card {
font-size: var(--pf-global--icon--FontSize--lg);
text-align: center;
}
.pf-c-content h1 {
display: flex;
align-items: flex-start;