diff --git a/swagger.yaml b/swagger.yaml index 93792e159..7671c9ffe 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -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: diff --git a/web/src/api/events.ts b/web/src/api/events.ts new file mode 100644 index 000000000..80ce3334f --- /dev/null +++ b/web/src/api/events.ts @@ -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 { + return DefaultClient.fetch(["audit", "events", "top_per_user"], { + "filter_action": action, + }); + } +} + +export interface TopNEvent { + application: { [key: string]: string}; + counted_events: number; + unique_users: number; +} diff --git a/web/src/elements/cards/AggregateCard.ts b/web/src/elements/cards/AggregateCard.ts new file mode 100644 index 000000000..ad06b1e87 --- /dev/null +++ b/web/src/elements/cards/AggregateCard.ts @@ -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``; + } + + render(): TemplateResult { + return html`
+
+
+ ${this.header ? gettext(this.header) : ""} +
+ ${this.headerLink ? html` + + ` : ""} +
+
+ ${this.renderInner()} +
+
`; + } + +} diff --git a/web/src/elements/cards/AggregatePromiseCard.ts b/web/src/elements/cards/AggregatePromiseCard.ts new file mode 100644 index 000000000..9ca2bc2eb --- /dev/null +++ b/web/src/elements/cards/AggregatePromiseCard.ts @@ -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; + + promiseProxy(): Promise { + if (!this.promise) { + return new Promise(() => html``); + } + return this.promise.then(s => { + return html` ${s}`; + }); + } + + renderInner(): TemplateResult { + return html`

+ ${until(this.promiseProxy(), html``)} +

`; + } + +} diff --git a/web/src/main.ts b/web/src/main.ts index 3d27f2dac..a87a5424e 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -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"; diff --git a/web/src/pages/AdminOverviewPage.ts b/web/src/pages/AdminOverviewPage.ts deleted file mode 100644 index 9866a5332..000000000 --- a/web/src/pages/AdminOverviewPage.ts +++ /dev/null @@ -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``; - } - - render(): TemplateResult { - return html``; - } - -} - -@customElement("pb-aggregate-card-promise") -export class AggregatePromiseCard extends AggregateCard { - @property() - promise?: Promise; - - renderInner(): TemplateResult { - return html`

- ${until(this.promise, html``)} -

`; - } - -} - -@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`
-
-

${gettext("System Overview")}

-
-
-
- -
`; - } - -} diff --git a/web/src/pages/admin-overview/AdminOverviewPage.ts b/web/src/pages/admin-overview/AdminOverviewPage.ts new file mode 100644 index 000000000..4dcd64722 --- /dev/null +++ b/web/src/pages/admin-overview/AdminOverviewPage.ts @@ -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``; + } + + renderGood(): TemplateResult { + return html`

+ ${this.value} +

`; + } + + renderBad(): TemplateResult { + return html`

+ ${this.value} +

+

${this.warningText ? gettext(this.warningText) : ""}

`; + } + + 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; + + static get styles(): CSSResult[] { + return COMMON_STYLES; + } + + firstUpdated(): void { + AdminOverview.get().then(value => this.data = value); + this.users = User.count(); + } + + render(): TemplateResult { + return html`
+
+

${gettext("System Overview")}

+
+
+
+ +
`; + } + +} diff --git a/web/src/pages/admin-overview/TopApplicationsTable.ts b/web/src/pages/admin-overview/TopApplicationsTable.ts new file mode 100644 index 000000000..9f756459c --- /dev/null +++ b/web/src/pages/admin-overview/TopApplicationsTable.ts @@ -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` + + ${event.application.name} + + + ${event.counted_events} + + + + + `; + } + + render(): TemplateResult { + return html` + + + + + + + + + ${this.topN ? this.topN.map((e) => this.renderRow(e)) : html``} + +
${gettext("Application")}${gettext("Logins")}
`; + } + +} diff --git a/web/src/passbook.css b/web/src/passbook.css index 916d6712a..d9f05521a 100644 --- a/web/src/passbook.css +++ b/web/src/passbook.css @@ -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;