web: more admin overview components
This commit is contained in:
parent
b218ded241
commit
1779b4d888
55
swagger.yaml
55
swagger.yaml
|
@ -133,6 +133,42 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- audit
|
- audit
|
||||||
parameters: []
|
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}/:
|
/audit/events/{event_uuid}/:
|
||||||
get:
|
get:
|
||||||
operationId: audit_events_read
|
operationId: audit_events_read
|
||||||
|
@ -6509,6 +6545,25 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
readOnly: true
|
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:
|
Application:
|
||||||
description: Application Serializer
|
description: Application Serializer
|
||||||
required:
|
required:
|
||||||
|
|
16
web/src/api/events.ts
Normal file
16
web/src/api/events.ts
Normal 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;
|
||||||
|
}
|
48
web/src/elements/cards/AggregateCard.ts
Normal file
48
web/src/elements/cards/AggregateCard.ts
Normal 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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
web/src/elements/cards/AggregatePromiseCard.ts
Normal file
25
web/src/elements/cards/AggregatePromiseCard.ts
Normal 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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ import "./elements/sidebar/SidebarUser";
|
||||||
import "./elements/table/TablePagination";
|
import "./elements/table/TablePagination";
|
||||||
|
|
||||||
import "./elements/AdminLoginsChart";
|
import "./elements/AdminLoginsChart";
|
||||||
|
import "./elements/cards/AggregateCard";
|
||||||
|
import "./elements/cards/AggregatePromiseCard";
|
||||||
import "./elements/CodeMirror";
|
import "./elements/CodeMirror";
|
||||||
import "./elements/Messages";
|
import "./elements/Messages";
|
||||||
import "./elements/Spinner";
|
import "./elements/Spinner";
|
||||||
|
@ -23,7 +25,8 @@ import "./pages/generic/SiteShell";
|
||||||
|
|
||||||
import "./pages/router/RouterOutlet";
|
import "./pages/router/RouterOutlet";
|
||||||
|
|
||||||
import "./pages/AdminOverviewPage";
|
import "./pages/admin-overview/AdminOverviewPage";
|
||||||
|
import "./pages/admin-overview/TopApplicationsTable";
|
||||||
import "./pages/applications/ApplicationListPage";
|
import "./pages/applications/ApplicationListPage";
|
||||||
import "./pages/applications/ApplicationViewPage";
|
import "./pages/applications/ApplicationViewPage";
|
||||||
import "./pages/LibraryPage";
|
import "./pages/LibraryPage";
|
||||||
|
|
|
@ -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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
116
web/src/pages/admin-overview/AdminOverviewPage.ts
Normal file
116
web/src/pages/admin-overview/AdminOverviewPage.ts
Normal 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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
49
web/src/pages/admin-overview/TopApplicationsTable.ts
Normal file
49
web/src/pages/admin-overview/TopApplicationsTable.ts
Normal 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>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -66,193 +66,6 @@ select[multiple] {
|
||||||
height: initial;
|
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 with user */
|
||||||
.form-control-static {
|
.form-control-static {
|
||||||
margin-top: var(--pf-global--spacer--sm);
|
margin-top: var(--pf-global--spacer--sm);
|
||||||
|
@ -291,12 +104,6 @@ input[data-is-monospace] {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Aggregate Cards */
|
|
||||||
.pb-aggregate-card {
|
|
||||||
font-size: var(--pf-global--icon--FontSize--lg);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pf-c-content h1 {
|
.pf-c-content h1 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
Reference in a new issue