diff --git a/web/src/api/admin_overview.ts b/web/src/api/admin_overview.ts
deleted file mode 100644
index 28185eb3d..000000000
--- a/web/src/api/admin_overview.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { DefaultClient } from "./client";
-
-export class AdminOverview {
- version: string;
- version_latest: string;
- worker_count: number;
- providers_without_application: number;
- policies_without_binding: number;
- cached_policies: number;
- cached_flows: number;
-
- constructor() {
- throw Error();
- }
-
- static get(): Promise {
- return DefaultClient.fetch(["admin", "overview"]);
- }
-
-}
diff --git a/web/src/api/version.ts b/web/src/api/version.ts
new file mode 100644
index 000000000..f747bccb8
--- /dev/null
+++ b/web/src/api/version.ts
@@ -0,0 +1,21 @@
+import { DefaultClient } from "./client";
+
+export class Version {
+
+ version_current: string;
+ version_latest: string;
+ outdated: boolean;
+
+ constructor() {
+ throw Error();
+ }
+
+ static get(): Promise {
+ return DefaultClient.fetch(["admin", "version"]);
+ }
+
+ toString(): string {
+ return this.version_current;
+ }
+
+}
diff --git a/web/src/elements/sidebar/Sidebar.ts b/web/src/elements/sidebar/Sidebar.ts
index 54f191323..bf639b84e 100644
--- a/web/src/elements/sidebar/Sidebar.ts
+++ b/web/src/elements/sidebar/Sidebar.ts
@@ -75,10 +75,10 @@ export class SidebarItem {
}
return html`
${this.path ?
- html`
+ html`
${this.name}
` :
- html`
+ html`
${this.name}
diff --git a/web/src/pages/admin-overview/AdminOverviewPage.ts b/web/src/pages/admin-overview/AdminOverviewPage.ts
index 8ea91f457..3825738ae 100644
--- a/web/src/pages/admin-overview/AdminOverviewPage.ts
+++ b/web/src/pages/admin-overview/AdminOverviewPage.ts
@@ -1,20 +1,22 @@
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 { SpinnerSize } from "../../elements/Spinner";
import "../../elements/AdminLoginsChart";
+import "../../elements/cards/AggregatePromiseCard";
import "./TopApplicationsTable";
-import "./OverviewCards";
+import "./cards/AdminStatusCard";
+import "./cards/FlowCacheStatusCard";
+import "./cards/PolicyCacheStatusCard";
+import "./cards/PolicyUnboundStatusCard";
+import "./cards/ProviderStatusCard";
+import "./cards/VersionStatusCard";
+import "./cards/WorkerStatusCard";
@customElement("ak-admin-overview")
export class AdminOverviewPage extends LitElement {
- @property({attribute: false})
- data?: AdminOverview;
-
@property({attribute: false})
users?: Promise;
@@ -23,7 +25,6 @@ export class AdminOverviewPage extends LitElement {
}
firstUpdated(): void {
- AdminOverview.get().then(value => this.data = value);
this.users = User.count();
}
@@ -51,19 +52,10 @@ export class AdminOverviewPage extends LitElement {
headerLink="#/administration/users/"
.promise=${this.users}>
-
-
- ${this.data ?
- this.data?.worker_count < 1 ?
- html`
- ${this.data?.worker_count}
-
- ${gettext("No workers connected.")}
` :
- html`
- ${this.data?.worker_count}
-
`
- : html``}
-
+
+
+
+
diff --git a/web/src/pages/admin-overview/OverviewCards.ts b/web/src/pages/admin-overview/OverviewCards.ts
deleted file mode 100644
index c35bd98c6..000000000
--- a/web/src/pages/admin-overview/OverviewCards.ts
+++ /dev/null
@@ -1,153 +0,0 @@
-import { gettext } from "django";
-import { customElement, property } from "lit-element";
-import { html, TemplateResult } from "lit-html";
-import { until } from "lit-html/directives/until";
-import { Flow } from "../../api/flow";
-import { Policy } from "../../api/policy";
-import { Provider } from "../../api/provider";
-import { AggregateCard } from "../../elements/cards/AggregateCard";
-import { SpinnerSize } from "../../elements/Spinner";
-
-interface AdminStatus {
- icon: string;
- message?: string;
-}
-
-abstract class AdminStatusCard extends AggregateCard {
-
- abstract getPrimaryCounter(): Promise;
-
- abstract getStatus(counter: number): Promise;
-
- @property({type: Number})
- counter = 0;
-
- renderInner(): TemplateResult {
- return html`
- ${until(this.getPrimaryCounter().then((c) => {
- this.counter = c;
- return this.getStatus(c);
- }).then((status) => {
- return html`
- ${this.counter}
-
- ${status.message ? html`${status.message}
` : html``}`;
- }), html``)}
-
`;
- }
-}
-
-@customElement("ak-admin-status-card-provider")
-export class ProviderStatusCard extends AdminStatusCard {
-
- getPrimaryCounter(): Promise {
- return Provider.list({
- "application__isnull": true
- }).then((response) => {
- return response.pagination.count;
- });
- }
-
- getStatus(counter: number): Promise {
- if (counter > 0) {
- return Promise.resolve({
- icon: "fa fa-exclamation-triangle",
- message: gettext("Warning: At least one Provider has no application assigned."),
- });
- } else {
- return Promise.resolve({
- icon: "fa fa-check-circle"
- });
- }
- }
-
-}
-
-@customElement("ak-admin-status-card-policy-unbound")
-export class PolicyUnboundStatusCard extends AdminStatusCard {
-
- getPrimaryCounter(): Promise {
- return Policy.list({
- "bindings__isnull": true,
- "promptstage__isnull": true,
- }).then((response) => {
- return response.pagination.count;
- });
- }
-
- getStatus(counter: number): Promise {
- if (counter > 0) {
- return Promise.resolve({
- icon: "fa fa-exclamation-triangle",
- message: gettext("Policies without binding exist."),
- });
- } else {
- return Promise.resolve({
- icon: "fa fa-check-circle"
- });
- }
- }
-
-}
-
-@customElement("ak-admin-status-card-policy-cache")
-export class PolicyCacheStatusCard extends AdminStatusCard {
-
- getPrimaryCounter(): Promise {
- return Policy.cached();
- }
-
- getStatus(counter: number): Promise {
- if (counter < 1) {
- return Promise.resolve({
- icon: "fa fa-exclamation-triangle",
- message: gettext("No policies cached. Users may experience slow response times."),
- });
- } else {
- return Promise.resolve({
- icon: "fa fa-check-circle"
- });
- }
- }
-
- renderHeaderLink(): TemplateResult {
- return html`
-
-
-
-
- `;
- }
-
-}
-
-@customElement("ak-admin-status-card-flow-cache")
-export class FlowCacheStatusCard extends AdminStatusCard {
-
- getPrimaryCounter(): Promise {
- return Flow.cached();
- }
-
- getStatus(counter: number): Promise {
- if (counter < 1) {
- return Promise.resolve({
- icon: "fa fa-exclamation-triangle",
- message: gettext("No flows cached."),
- });
- } else {
- return Promise.resolve({
- icon: "fa fa-check-circle"
- });
- }
- }
-
- renderHeaderLink(): TemplateResult {
- return html`
-
-
-
-
- `;
- }
-
-}
diff --git a/web/src/pages/admin-overview/cards/AdminStatusCard.ts b/web/src/pages/admin-overview/cards/AdminStatusCard.ts
new file mode 100644
index 000000000..c27c3afb3
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/AdminStatusCard.ts
@@ -0,0 +1,33 @@
+import { html, TemplateResult } from "lit-html";
+import { until } from "lit-html/directives/until";
+import { AggregateCard } from "../../../elements/cards/AggregateCard";
+import { SpinnerSize } from "../../../elements/Spinner";
+
+export interface AdminStatus {
+ icon: string;
+ message?: string;
+}
+
+export abstract class AdminStatusCard extends AggregateCard {
+
+ abstract getPrimaryValue(): Promise;
+
+ abstract getStatus(value: T): Promise;
+
+ value?: T;
+
+ renderInner(): TemplateResult {
+ return html`
+ ${until(this.getPrimaryValue().then((v) => {
+ this.value = v;
+ return this.getStatus(v);
+ }).then((status) => {
+ return html`
+ ${this.value}
+
+ ${status.message ? html`${status.message}
` : html``}`;
+ }), html``)}
+ `;
+ }
+}
+
diff --git a/web/src/pages/admin-overview/cards/FlowCacheStatusCard.ts b/web/src/pages/admin-overview/cards/FlowCacheStatusCard.ts
new file mode 100644
index 000000000..1b34310c2
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/FlowCacheStatusCard.ts
@@ -0,0 +1,36 @@
+import { gettext } from "django";
+import { customElement, html, TemplateResult } from "lit-element";
+import { Flow } from "../../../api/flow";
+import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
+import "../../../elements/buttons/ModalButton";
+
+@customElement("ak-admin-status-card-flow-cache")
+export class FlowCacheStatusCard extends AdminStatusCard {
+
+ getPrimaryValue(): Promise {
+ return Flow.cached();
+ }
+
+ getStatus(value: number): Promise {
+ if (value < 1) {
+ return Promise.resolve({
+ icon: "fa fa-exclamation-triangle",
+ message: gettext("No flows cached."),
+ });
+ } else {
+ return Promise.resolve({
+ icon: "fa fa-check-circle"
+ });
+ }
+ }
+
+ renderHeaderLink(): TemplateResult {
+ return html`
+
+
+
+
+ `;
+ }
+
+}
diff --git a/web/src/pages/admin-overview/cards/PolicyCacheStatusCard.ts b/web/src/pages/admin-overview/cards/PolicyCacheStatusCard.ts
new file mode 100644
index 000000000..85920a551
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/PolicyCacheStatusCard.ts
@@ -0,0 +1,37 @@
+import { gettext } from "django";
+import { customElement } from "lit-element";
+import { TemplateResult, html } from "lit-html";
+import { Policy } from "../../../api/policy";
+import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
+import "../../../elements/buttons/ModalButton";
+
+@customElement("ak-admin-status-card-policy-cache")
+export class PolicyCacheStatusCard extends AdminStatusCard {
+
+ getPrimaryValue(): Promise {
+ return Policy.cached();
+ }
+
+ getStatus(value: number): Promise {
+ if (value < 1) {
+ return Promise.resolve({
+ icon: "fa fa-exclamation-triangle",
+ message: gettext("No policies cached. Users may experience slow response times."),
+ });
+ } else {
+ return Promise.resolve({
+ icon: "fa fa-check-circle"
+ });
+ }
+ }
+
+ renderHeaderLink(): TemplateResult {
+ return html`
+
+
+
+
+ `;
+ }
+
+}
diff --git a/web/src/pages/admin-overview/cards/PolicyUnboundStatusCard.ts b/web/src/pages/admin-overview/cards/PolicyUnboundStatusCard.ts
new file mode 100644
index 000000000..37a74a598
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/PolicyUnboundStatusCard.ts
@@ -0,0 +1,31 @@
+import { gettext } from "django";
+import { customElement } from "lit-element";
+import { Policy } from "../../../api/policy";
+import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
+
+@customElement("ak-admin-status-card-policy-unbound")
+export class PolicyUnboundStatusCard extends AdminStatusCard {
+
+ getPrimaryValue(): Promise {
+ return Policy.list({
+ "bindings__isnull": true,
+ "promptstage__isnull": true,
+ }).then((response) => {
+ return response.pagination.count;
+ });
+ }
+
+ getStatus(value: number): Promise {
+ if (value > 0) {
+ return Promise.resolve({
+ icon: "fa fa-exclamation-triangle",
+ message: gettext("Policies without binding exist."),
+ });
+ } else {
+ return Promise.resolve({
+ icon: "fa fa-check-circle"
+ });
+ }
+ }
+
+}
diff --git a/web/src/pages/admin-overview/cards/ProviderStatusCard.ts b/web/src/pages/admin-overview/cards/ProviderStatusCard.ts
new file mode 100644
index 000000000..b19404dd6
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/ProviderStatusCard.ts
@@ -0,0 +1,30 @@
+import { gettext } from "django";
+import { customElement } from "lit-element";
+import { Provider } from "../../../api/provider";
+import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
+
+@customElement("ak-admin-status-card-provider")
+export class ProviderStatusCard extends AdminStatusCard {
+
+ getPrimaryValue(): Promise {
+ return Provider.list({
+ "application__isnull": true
+ }).then((response) => {
+ return response.pagination.count;
+ });
+ }
+
+ getStatus(value: number): Promise {
+ if (value > 0) {
+ return Promise.resolve({
+ icon: "fa fa-exclamation-triangle",
+ message: gettext("Warning: At least one Provider has no application assigned."),
+ });
+ } else {
+ return Promise.resolve({
+ icon: "fa fa-check-circle"
+ });
+ }
+ }
+
+}
diff --git a/web/src/pages/admin-overview/cards/VersionStatusCard.ts b/web/src/pages/admin-overview/cards/VersionStatusCard.ts
new file mode 100644
index 000000000..a61e6abd9
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/VersionStatusCard.ts
@@ -0,0 +1,27 @@
+import { gettext } from "django";
+import { customElement } from "lit-element";
+import { Version } from "../../../api/version";
+import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
+
+@customElement("ak-admin-status-version")
+export class VersionStatusCard extends AdminStatusCard {
+
+ getPrimaryValue(): Promise {
+ return Version.get();
+ }
+
+ getStatus(value: Version): Promise {
+ if (value.outdated) {
+ return Promise.resolve({
+ icon: "fa fa-exclamation-triangle",
+ message: gettext(`${value.version_latest} is available!`),
+ });
+ } else {
+ return Promise.resolve({
+ icon: "fa fa-check-circle",
+ message: gettext("Up-to-date!")
+ });
+ }
+ }
+
+}
diff --git a/web/src/pages/admin-overview/cards/WorkerStatusCard.ts b/web/src/pages/admin-overview/cards/WorkerStatusCard.ts
new file mode 100644
index 000000000..fab55d433
--- /dev/null
+++ b/web/src/pages/admin-overview/cards/WorkerStatusCard.ts
@@ -0,0 +1,28 @@
+import { gettext } from "django";
+import { customElement } from "lit-element";
+import { DefaultClient, PBResponse } from "../../../api/client";
+import { AdminStatus, AdminStatusCard } from "./AdminStatusCard";
+
+@customElement("ak-admin-status-card-workers")
+export class WorkersStatusCard extends AdminStatusCard {
+
+ getPrimaryValue(): Promise {
+ return DefaultClient.fetch>(["admins", "workers"]).then((r) => {
+ return r.pagination.count;
+ });
+ }
+
+ getStatus(value: number): Promise {
+ if (value < 1) {
+ return Promise.resolve({
+ icon: "fa fa-exclamation-triangle",
+ message: gettext("No workers connected. Background tasks will not run."),
+ });
+ } else {
+ return Promise.resolve({
+ icon: "fa fa-check-circle"
+ });
+ }
+ }
+
+}