web/admin: rework admin dashboard, add more links, remove user and group graphs (#4399)

This commit is contained in:
Jens L
2023-01-10 23:47:55 +01:00
committed by GitHub
parent 2a83d79ace
commit b424c5dd27
26 changed files with 823 additions and 669 deletions

View File

@ -1,15 +1,13 @@
import "@goauthentik/admin/admin-overview/TopApplicationsTable";
import "@goauthentik/admin/admin-overview/cards/AdminStatusCard";
import "@goauthentik/admin/admin-overview/cards/RecentEventsCard";
import "@goauthentik/admin/admin-overview/cards/SystemStatusCard";
import "@goauthentik/admin/admin-overview/cards/VersionStatusCard";
import "@goauthentik/admin/admin-overview/cards/WorkerStatusCard";
import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "@goauthentik/admin/admin-overview/charts/FlowStatusChart";
import "@goauthentik/admin/admin-overview/charts/GroupCountStatusChart";
import "@goauthentik/admin/admin-overview/charts/LDAPSyncStatusChart";
import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart";
import "@goauthentik/admin/admin-overview/charts/PolicyStatusChart";
import "@goauthentik/admin/admin-overview/charts/UserCountStatusChart";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
@ -28,6 +26,12 @@ import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
export function versionFamily(): string {
const parts = VERSION.split(".");
parts.pop();
return parts.join(".");
}
@customElement("ak-admin-overview")
export class AdminOverviewPage extends AKElement {
static get styles(): CSSResult[] {
@ -72,113 +76,100 @@ export class AdminOverviewPage extends AKElement {
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">
<!-- row 1 -->
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="fa fa-share"
header=${t`Quick actions`}
.isCenter=${false}
<div class="pf-l-grid__item pf-m-6-col pf-l-grid pf-m-gutter">
<div
class="pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl pf-m-4-col-on-2xl graph-container"
>
<ul class="pf-c-list">
<li>
<a
class="pf-u-mb-xl"
href=${paramURL("/core/applications", {
createForm: true,
})}
>${t`Create a new application`}</a
>
</li>
<li>
<a class="pf-u-mb-xl" href=${paramURL("/events/log")}
>${t`Check the logs`}</a
>
</li>
<li>
<a
class="pf-u-mb-xl"
target="_blank"
href="https://goauthentik.io/integrations/"
>${t`Explore integrations`}</a
>
</li>
</ul>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-process-automation"
header=${t`Flows`}
headerLink="#/flow/flows"
<ak-aggregate-card
icon="fa fa-share"
header=${t`Quick actions`}
.isCenter=${false}
>
<ul class="pf-c-list">
<li>
<a
class="pf-u-mb-xl"
href=${paramURL("/core/applications", {
createForm: true,
})}
>${t`Create a new application`}</a
>
</li>
<li>
<a class="pf-u-mb-xl" href=${paramURL("/events/log")}
>${t`Check the logs`}</a
>
</li>
<li>
<a
class="pf-u-mb-xl"
target="_blank"
href="https://goauthentik.io/integrations/"
>${t`Explore integrations`}</a
>
</li>
<li>
<a class="pf-u-mb-xl" href=${paramURL("/identity/users")}
>${t`Manage users`}</a
>
</li>
<li>
<a
class="pf-u-mb-xl"
target="_blank"
href="https://goauthentik.io/docs/releases/${versionFamily()}#fixed-in-${VERSION.replaceAll(
".",
"",
)}"
>${t`Check release notes`}</a
>
</li>
</ul>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl pf-m-4-col-on-2xl graph-container"
>
<ak-admin-status-chart-flow></ak-admin-status-chart-flow>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-zone"
header=${t`Outpost status`}
headerLink="#/outpost/outposts"
<ak-aggregate-card
icon="pf-icon pf-icon-zone"
header=${t`Outpost status`}
headerLink="#/outpost/outposts"
>
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-12-col pf-m-8-col-on-xl pf-m-4-col-on-2xl graph-container"
>
<ak-admin-status-chart-outpost></ak-admin-status-chart-outpost>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-user"
header=${t`Users`}
headerLink="#/identity/users"
<ak-aggregate-card
icon="fa fa-sync-alt"
header=${t`LDAP Sync status`}
headerLink="#/core/sources"
>
<ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-chart-user-count></ak-admin-status-chart-user-count>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-users"
header=${t`Groups`}
headerLink="#/identity/groups"
<ak-admin-status-system> </ak-admin-status-system>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-chart-group-count></ak-admin-status-chart-group-count>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="fa fa-sync-alt"
header=${t`LDAP Sync status`}
headerLink="#/core/sources"
<ak-admin-status-version> </ak-admin-status-version>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-chart-ldap-sync></ak-admin-status-chart-ldap-sync>
</ak-aggregate-card>
<ak-admin-status-card-workers> </ak-admin-status-card-workers>
</div>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
</div>
<!-- row 2 -->
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-system> </ak-admin-status-system>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-version> </ak-admin-status-version>
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
>
<ak-admin-status-card-workers> </ak-admin-status-card-workers>
<div class="pf-l-grid__item pf-m-6-col">
<ak-recent-events pageSize="6"></ak-recent-events>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />

View File

@ -1,7 +1,7 @@
import "@goauthentik/admin/admin-overview/charts/AdminModelPerDay";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/cards/AggregatePromiseCard";
import "@goauthentik/elements/charts/AdminModelPerDay";
import { t } from "@lingui/macro";

View File

@ -0,0 +1,104 @@
import "@goauthentik/admin/events/EventInfo";
import { ActionToLabel } from "@goauthentik/admin/events/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { Event, EventsApi } from "@goauthentik/api";
@customElement("ak-recent-events")
export class RecentEventsCard extends Table<Event> {
@property()
order = "-created";
@property()
pageSize = 10;
async apiEndpoint(page: number): Promise<PaginatedResponse<Event>> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
ordering: this.order,
page: page,
pageSize: this.pageSize,
search: this.search || "",
});
}
static get styles(): CSSResult[] {
return super.styles.concat(
PFCard,
css`
.pf-c-card__title {
--pf-c-card__title--FontFamily: var(
--pf-global--FontFamily--heading--sans-serif
);
--pf-c-card__title--FontSize: var(--pf-global--FontSize--md);
--pf-c-card__title--FontWeight: var(--pf-global--FontWeight--bold);
}
`,
);
}
columns(): TableColumn[] {
return [
new TableColumn(t`Action`, "action"),
new TableColumn(t`User`, "user"),
new TableColumn(t`Creation Date`, "created"),
new TableColumn(t`Client IP`, "client_ip"),
new TableColumn(t`Tenant`, "tenant_name"),
];
}
renderToolbar(): TemplateResult {
return html`<div class="pf-c-card__title">
<i class="pf-icon pf-icon-catalog"></i>&nbsp;${t`Recent events`}
</div>`;
}
row(item: EventWithContext): TemplateResult[] {
let geo: KeyUnknown | undefined = undefined;
if (Object.hasOwn(item.context, "geo")) {
geo = item.context.geo as KeyUnknown;
}
return [
html`<div><a href="${`#/events/log/${item.pk}`}">${ActionToLabel(item.action)}</a></div>
<small>${item.app}</small>`,
item.user?.username
? html`<div>
<a href="#/identity/users/${item.user.pk}"
>${item.user?.username.substring(0, 15)}</a
>
</div>
${item.user.on_behalf_of
? html`<small>
<a href="#/identity/users/${item.user.on_behalf_of.pk}"
>${t`On behalf of ${item.user.on_behalf_of.username}`}</a
>
</small>`
: html``}`
: html`-`,
html`<span>${item.created?.toLocaleString()}</span>`,
html` <div>${item.clientIp || t`-`}</div>
${geo ? html`<small>${geo.city}, ${geo.country}</small> ` : html``}`,
html`<span>${item.tenant?.name || t`-`}</span>`,
];
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${t`No Events found.`}>
<div slot="body">${t`No matching events could be found.`}</div>
</ak-empty-state>`);
}
}

View File

@ -72,6 +72,7 @@ export class SystemStatusCard extends AdminStatusCard<System> {
message: html`${t`Server and client are further than 5 seconds apart.`}`,
});
}
this.header = t`OK`;
return Promise.resolve<AdminStatus>({
icon: "fa fa-check-circle pf-m-success",
message: html`${t`Everything is ok.`}`,

View File

@ -0,0 +1,51 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData, Tick } from "chart.js";
import { t } from "@lingui/macro";
import { customElement, property } from "lit/decorators.js";
import { Coordinate, EventActions, EventsApi } from "@goauthentik/api";
@customElement("ak-charts-admin-model-per-day")
export class AdminModelPerDay extends AKChart<Coordinate[]> {
@property()
action: EventActions = EventActions.ModelCreated;
@property({ attribute: false })
query?: { [key: string]: unknown } | undefined;
async apiRequest(): Promise<Coordinate[]> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsPerMonthList({
action: this.action,
query: JSON.stringify(this.query || {}),
});
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return t`${ago} days ago`;
}
getChartData(data: Coordinate[]): ChartData {
return {
datasets: [
{
label: t`Objects created`,
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data:
data.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
],
};
}
}

View File

@ -1,61 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import "@goauthentik/elements/forms/ConfirmationForm";
import { ChartData, ChartOptions } from "chart.js";
import { t } from "@lingui/macro";
import { customElement } from "lit/decorators.js";
import { FlowsApi } from "@goauthentik/api";
interface FlowMetrics {
count: number;
cached: number;
}
@customElement("ak-admin-status-chart-flow")
export class PolicyStatusChart extends AKChart<FlowMetrics> {
getChartType(): string {
return "doughnut";
}
getOptions(): ChartOptions {
return {
plugins: {
legend: {
display: false,
},
},
maintainAspectRatio: false,
};
}
async apiRequest(): Promise<FlowMetrics> {
const api = new FlowsApi(DEFAULT_CONFIG);
const cached = (await api.flowsInstancesCacheInfoRetrieve()).count || 0;
const count = (
await api.flowsInstancesList({
pageSize: 1,
})
).pagination.count;
this.centerText = count.toString();
return {
count: count - cached,
cached: cached,
};
}
getChartData(data: FlowMetrics): ChartData {
return {
labels: [t`Total flows`, t`Cached flows`],
datasets: [
{
backgroundColor: ["#2b9af3", "#3e8635"],
spanGaps: true,
data: [data.count, data.cached],
},
],
};
}
}

View File

@ -1,64 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData, ChartOptions } from "chart.js";
import { t } from "@lingui/macro";
import { customElement } from "lit/decorators.js";
import { CoreApi } from "@goauthentik/api";
interface GroupMetrics {
count: number;
superusers: number;
}
@customElement("ak-admin-status-chart-group-count")
export class GroupCountStatusChart extends AKChart<GroupMetrics> {
getChartType(): string {
return "doughnut";
}
getOptions(): ChartOptions {
return {
plugins: {
legend: {
display: false,
},
},
maintainAspectRatio: false,
};
}
async apiRequest(): Promise<GroupMetrics> {
const api = new CoreApi(DEFAULT_CONFIG);
const count = (
await api.coreGroupsList({
pageSize: 1,
})
).pagination.count;
const superusers = (
await api.coreGroupsList({
isSuperuser: true,
})
).pagination.count;
this.centerText = count.toString();
return {
count: count - superusers,
superusers,
};
}
getChartData(data: GroupMetrics): ChartData {
return {
labels: [t`Total groups`, t`Superuser-groups`],
datasets: [
{
backgroundColor: ["#2b9af3", "#3e8635"],
spanGaps: true,
data: [data.count, data.superusers],
},
],
};
}
}

View File

@ -1,71 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import "@goauthentik/elements/forms/ConfirmationForm";
import { ChartData, ChartOptions } from "chart.js";
import { t } from "@lingui/macro";
import { customElement } from "lit/decorators.js";
import { PoliciesApi } from "@goauthentik/api";
interface PolicyMetrics {
count: number;
cached: number;
unbound: number;
}
@customElement("ak-admin-status-chart-policy")
export class PolicyStatusChart extends AKChart<PolicyMetrics> {
getChartType(): string {
return "doughnut";
}
getOptions(): ChartOptions {
return {
plugins: {
legend: {
display: false,
},
},
maintainAspectRatio: false,
};
}
async apiRequest(): Promise<PolicyMetrics> {
const api = new PoliciesApi(DEFAULT_CONFIG);
const cached = (await api.policiesAllCacheInfoRetrieve()).count || 0;
const count = (
await api.policiesAllList({
pageSize: 1,
})
).pagination.count;
const unbound = (
await api.policiesAllList({
bindingsIsnull: true,
promptstageIsnull: true,
})
).pagination.count;
this.centerText = count.toString();
return {
// If we have more cache than total policies, only show that
// otherwise show count without unbound
count: cached >= count ? cached : count - unbound,
cached: cached,
unbound: unbound,
};
}
getChartData(data: PolicyMetrics): ChartData {
return {
labels: [t`Total policies`, t`Cached policies`, t`Unbound policies`],
datasets: [
{
backgroundColor: ["#2b9af3", "#3e8635", "#f0ab00"],
spanGaps: true,
data: [data.count, data.cached, data.unbound],
},
],
};
}
}

View File

@ -1,64 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData, ChartOptions } from "chart.js";
import { t } from "@lingui/macro";
import { customElement } from "lit/decorators.js";
import { CoreApi } from "@goauthentik/api";
interface UserMetrics {
count: number;
superusers: number;
}
@customElement("ak-admin-status-chart-user-count")
export class UserCountStatusChart extends AKChart<UserMetrics> {
getChartType(): string {
return "doughnut";
}
getOptions(): ChartOptions {
return {
plugins: {
legend: {
display: false,
},
},
maintainAspectRatio: false,
};
}
async apiRequest(): Promise<UserMetrics> {
const api = new CoreApi(DEFAULT_CONFIG);
const count = (
await api.coreUsersList({
pageSize: 1,
})
).pagination.count;
const superusers = (
await api.coreUsersList({
isSuperuser: true,
})
).pagination.count;
this.centerText = count.toString();
return {
count: count - superusers,
superusers,
};
}
getChartData(data: UserMetrics): ChartData {
return {
labels: [t`Total users`, t`Superusers`],
datasets: [
{
backgroundColor: ["#2b9af3", "#3e8635"],
spanGaps: true,
data: [data.count, data.superusers],
},
],
};
}
}

View File

@ -0,0 +1,40 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { t } from "@lingui/macro";
import { customElement, property } from "lit/decorators.js";
import { Coordinate, CoreApi } from "@goauthentik/api";
@customElement("ak-charts-application-authorize")
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
@property()
applicationSlug!: string;
async apiRequest(): Promise<Coordinate[]> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({
slug: this.applicationSlug,
});
}
getChartData(data: Coordinate[]): ChartData {
return {
datasets: [
{
label: t`Authorizations`,
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data:
data.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
],
};
}
}

View File

@ -1,3 +1,4 @@
import "@goauthentik/admin/applications/ApplicationAuthorizeChart";
import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/policies/BoundPoliciesList";
@ -7,7 +8,6 @@ import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/charts/ApplicationAuthorizeChart";
import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";

View File

@ -0,0 +1,64 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { t } from "@lingui/macro";
import { customElement, property } from "lit/decorators.js";
import { CoreApi, UserMetrics } from "@goauthentik/api";
@customElement("ak-charts-user")
export class UserChart extends AKChart<UserMetrics> {
@property({ type: Number })
userId?: number;
async apiRequest(): Promise<UserMetrics> {
return new CoreApi(DEFAULT_CONFIG).coreUsersMetricsRetrieve({
id: this.userId || 0,
});
}
getChartData(data: UserMetrics): ChartData {
return {
datasets: [
{
label: t`Failed Logins`,
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
data:
data.loginsFailedPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
{
label: t`Successful Logins`,
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data:
data.loginsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
{
label: t`Application authorizations`,
backgroundColor: "rgba(43, 154, 243, .5)",
spanGaps: true,
data:
data.authorizationsPer1h?.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
],
};
}
}

View File

@ -1,5 +1,6 @@
import "@goauthentik/admin/groups/RelatedGroupList";
import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserChart";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
@ -13,7 +14,6 @@ import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/charts/UserChart";
import "@goauthentik/elements/events/ObjectChangelog";
import "@goauthentik/elements/events/UserEvents";
import "@goauthentik/elements/forms/ModalForm";