Files
authentik/web/src/elements/sync/SyncStatusCard.ts
2025-04-07 18:37:03 +00:00

194 lines
6.4 KiB
TypeScript

import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { formatElapsedTime } from "@goauthentik/common/temporal";
import "@goauthentik/components/ak-status-label";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/events/LogViewer";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SyncStatus, SystemTask, SystemTaskStatusEnum } from "@goauthentik/api";
@customElement("ak-sync-status-table")
export class SyncStatusTable extends Table<SystemTask> {
@property({ attribute: false })
tasks: SystemTask[] = [];
expandable = true;
static get styles() {
return super.styles.concat(css`
code:not(:last-of-type)::after {
content: "-";
margin: 0 0.25rem;
}
`);
}
async apiEndpoint(): Promise<PaginatedResponse<SystemTask>> {
if (this.tasks.length === 1) {
this.expandedElements = this.tasks;
}
return {
pagination: {
next: 0,
previous: 0,
count: this.tasks.length,
current: 1,
totalPages: 1,
startIndex: 0,
endIndex: this.tasks.length,
},
results: this.tasks,
};
}
columns(): TableColumn[] {
return [
new TableColumn(msg("Task")),
new TableColumn(msg("Status")),
new TableColumn(msg("Finished")),
];
}
row(item: SystemTask): TemplateResult[] {
const nameParts = item.fullName.split(":");
nameParts.shift();
return [
html`<div>${item.name}</div>
<small>${nameParts.map((part) => html`<code>${part}</code>`)}</small>`,
html`<ak-status-label
?good=${item.status === SystemTaskStatusEnum.Successful}
good-label=${msg("Finished successfully")}
bad-label=${msg("Finished with errors")}
></ak-status-label>`,
html`<div>${formatElapsedTime(item.finishTimestamp)}</div>
<small>${item.finishTimestamp.toLocaleString()}</small>`,
];
}
renderExpanded(item: SystemTask): TemplateResult {
return html`<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<ak-log-viewer .logs=${item?.messages}></ak-log-viewer>
</div>
</td>`;
}
renderToolbarContainer() {
return html``;
}
renderTablePagination() {
return html``;
}
}
@customElement("ak-sync-status-card")
export class SyncStatusCard extends AKElement {
@state()
syncState?: SyncStatus;
@state()
loading = false;
@property({ attribute: false })
fetch!: () => Promise<SyncStatus>;
@property({ attribute: false })
triggerSync!: () => Promise<unknown>;
static get styles(): CSSResult[] {
return [PFBase, PFButton, PFCard, PFTable];
}
firstUpdated() {
this.loading = true;
this.fetch().then((status) => {
this.syncState = status;
this.loading = false;
});
}
renderSyncStatus(): TemplateResult {
if (this.loading) {
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
}
if (!this.syncState) {
return html`${msg("No sync status.")}`;
}
if (this.syncState.isRunning) {
return html`${msg("Sync currently running.")}`;
}
if (this.syncState.tasks.length < 1) {
return html`${msg("Not synced yet.")}`;
}
return html`<ak-sync-status-table .tasks=${this.syncState.tasks}></ak-sync-status-table>`;
}
render(): TemplateResult {
return html`<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__actions">
<button
class="pf-c-button pf-m-plain"
type="button"
@click=${() => {
this.fetch();
}}
>
<i class="fa fa-sync"></i>
</button>
</div>
<div class="pf-c-card__title">${msg("Sync status")}</div>
</div>
<div class="pf-c-card__body">${this.renderSyncStatus()}</div>
<div class="pf-c-card__footer">
<ak-action-button
class="pf-m-secondary"
?disabled=${this.syncState?.isRunning}
.apiRequest=${() => {
if (this.syncState) {
// This is a bit of a UX cheat, we set isRunning to true to disable the button
// and change the text even before we hear back from the backend
this.syncState = {
...this.syncState,
isRunning: true,
};
}
this.triggerSync().then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
this.firstUpdated();
});
}}
>
${this.syncState?.isRunning
? msg("Sync currently running")
: msg("Run sync again")}
</ak-action-button>
</div>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-sync-status-table": SyncStatusTable;
"ak-sync-status-card": SyncStatusCard;
}
}