diff --git a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts index 52b81580d12b..b4d751e69885 100644 --- a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts +++ b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts @@ -44,23 +44,25 @@ export class ConnectorsListPage extends TablePage { return [ html`${item.name}`, html`${item.verboseName}`, - html` - ${msg("Update")} - ${msg("Update Connector")} - - - - `, + html`
+ + ${msg("Update")} + ${msg("Update Connector")} + + + + +
`, ]; } diff --git a/web/src/admin/flows/FlowListPage.ts b/web/src/admin/flows/FlowListPage.ts index c6c0ecef22e2..5ce60ad7dbfe 100644 --- a/web/src/admin/flows/FlowListPage.ts +++ b/web/src/admin/flows/FlowListPage.ts @@ -85,7 +85,8 @@ export class FlowListPage extends TablePage { html`${item.name}`, html`${Array.from(item.stages || []).length}`, html`${Array.from(item.policies || []).length}`, - html` + html`
+ ${msg("Update")} ${msg("Update Flow")} @@ -121,7 +122,8 @@ export class FlowListPage extends TablePage { - `, + +
`, ]; } diff --git a/web/src/elements/ak-progress-bar.ts b/web/src/elements/ak-progress-bar.ts index bfefa3583824..50cd88009365 100644 --- a/web/src/elements/ak-progress-bar.ts +++ b/web/src/elements/ak-progress-bar.ts @@ -1,11 +1,11 @@ import { PFSize } from "#common/enums"; import { AKElement } from "#elements/Base"; - -import { spread } from "@open-wc/lit-helpers"; +import { ifPresent } from "#elements/utils/attributes"; import { css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; +import { styleMap } from "lit/directives/style-map.js"; import PFProgress from "@patternfly/patternfly/components/Progress/progress.css"; @@ -21,6 +21,7 @@ export class ProgressBar extends AKElement { .pf-c-progress { overflow: hidden; } + .pf-c-progress.pf-m-indeterminate { --pf-c-progress__bar--Height: 2px; --pf-c-progress--GridGap: 0; @@ -28,12 +29,27 @@ export class ProgressBar extends AKElement { z-index: 1; position: relative; } - .pf-c-progress.pf-m-indeterminate .pf-c-progress__bar .pf-c-progress__indicator { - width: 100%; - height: 100%; - animation: indeterminateAnimation 1s infinite linear; - transform-origin: 0% 50%; + + .pf-c-progress.pf-m-indeterminate { + transition: opacity 0.2s linear; + transition-delay: 0.2s; + + .pf-c-progress__bar .pf-c-progress__indicator { + width: 100%; + height: 100%; + animation: indeterminateAnimation 1s infinite linear; + transform-origin: 0% 50%; + } } + + :host([inert]) .pf-c-progress.pf-m-indeterminate { + opacity: 0; + + .pf-c-progress__bar .pf-c-progress__indicator { + animation-iteration-count: 1; + } + } + @keyframes indeterminateAnimation { 0% { transform: translateX(0) scaleX(0); @@ -48,32 +64,29 @@ export class ProgressBar extends AKElement { `, ]; - @property({ type: Number }) - min = 0; - @property({ type: Number }) - max = 100; - @property({ type: Number }) - value = 0; - - @property({ type: Boolean }) - indeterminate = false; - - @property() - size: PFSize = PFSize.Medium; - - render() { - const barAttrs: { [key: string]: unknown } = {}; - const indicatorAttrs: { [key: string]: unknown } = {}; - if (!this.indeterminate) { - barAttrs["aria-valuemin"] = this.min; - barAttrs["aria-valuemax"] = this.max; - barAttrs["aria-valuenow"] = this.value; - indicatorAttrs.style = `"width:${Math.min(this.value, 100)}%;";`; - } + @property({ type: Number, reflect: true, useDefault: true }) + public min = 0; + + @property({ type: Number, reflect: true, useDefault: true }) + public max = 100; + + @property({ type: Number, reflect: true, useDefault: true }) + public value = 0; + + @property({ type: Boolean, reflect: true, useDefault: true }) + public indeterminate = false; + + @property({ type: String, reflect: true, useDefault: true }) + public size: PFSize = PFSize.Medium; + + @property({ type: String }) + public label = ""; + + protected render() { return html`
${this.hasSlotted("description") ? html` @@ -91,8 +104,21 @@ export class ProgressBar extends AKElement {
` : nothing} -
-
+
+
`; } diff --git a/web/src/elements/forms/DeleteBulkForm.ts b/web/src/elements/forms/DeleteBulkForm.ts index f9298188aff8..e6ff7e5dcbff 100644 --- a/web/src/elements/forms/DeleteBulkForm.ts +++ b/web/src/elements/forms/DeleteBulkForm.ts @@ -12,7 +12,7 @@ import { SlottedTemplateResult } from "#elements/types"; import { UsedBy, UsedByActionEnum } from "@goauthentik/api"; import { msg, str } from "@lit/localize"; -import { CSSResult, html, nothing, TemplateResult } from "lit"; +import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { until } from "lit/directives/until.js"; @@ -79,9 +79,9 @@ export class DeleteObjectsTable extends Table { return nothing; } - firstUpdated(): void { + firstUpdated(changedProperties: PropertyValues): void { this.expandable = this.usedBy !== undefined; - super.firstUpdated(); + super.firstUpdated(changedProperties); } renderExpanded(item: T): TemplateResult { diff --git a/web/src/elements/table/Table.css b/web/src/elements/table/Table.css index f82c315b6a6b..093c22fe65d6 100644 --- a/web/src/elements/table/Table.css +++ b/web/src/elements/table/Table.css @@ -146,6 +146,7 @@ time { } :host { + display: block; container-type: inline-size; } diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 789356f2ffe7..f1615ad2f39b 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -15,6 +15,7 @@ import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/error import { GroupResult } from "#common/utils"; import { AKElement } from "#elements/Base"; +import { intersectionObserver } from "#elements/decorators/intersection-observer"; import { WithLicenseSummary } from "#elements/mixins/license"; import { WithSession } from "#elements/mixins/session"; import { getURLParam, updateURLParams } from "#elements/router/RouteMatch"; @@ -24,6 +25,8 @@ import { ifPresent } from "#elements/utils/attributes"; import { isInteractiveElement } from "#elements/utils/interactivity"; import { isEventTargetingListener } from "#elements/utils/pointer"; +import { ConsoleLogger, Logger } from "#logger/browser"; + import { Pagination } from "@goauthentik/api"; import { kebabCase } from "change-case"; @@ -43,7 +46,6 @@ import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css"; import PFTable from "@patternfly/patternfly/components/Table/table.css"; import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css"; import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; export * from "./shared.js"; export * from "./TableColumn.js"; @@ -76,7 +78,6 @@ export abstract class Table implements TableLike { static styles: CSSResult[] = [ - PFBase, PFTable, PFBullseye, PFButton, @@ -87,6 +88,8 @@ export abstract class Table Styles, ]; + //#region Abstract members + protected abstract apiEndpoint(): Promise>; /** * The columns to display in the table. @@ -102,11 +105,50 @@ export abstract class Table */ protected abstract row(item: T): SlottedTemplateResult[]; + //#endregion + + //#region Protected Properties + /** * Customize the "No objects found" message. */ protected emptyStateMessage = msg("No objects found."); + /** + * Whether the table is currently fetching data. + */ + @state() + protected loading = false; + + /** + * A timestamp of the last attempt to refresh the table data. + */ + @state() + protected lastRefreshedAt: Date | null = null; + + /** + * Logger instance for this table. + */ + protected logger: Logger; + + /** + * A cached grouping of the last fetched results. + * + * @see {@linkcode Table.fetch} + */ + @state() + protected groups: GroupResult[] = []; + + @state() + protected expandedElements = new Set(); + + @state() + protected error: APIError | null = null; + + //#endregion + + //#region Private Members + /** * The total number of defined and additional columns in the table. */ @@ -133,32 +175,40 @@ export abstract class Table } /** - * Whether the table is currently fetching data. + * Timestamp of when a fetch was requested, but deferred due to the table not being visible. */ - @state() - protected loading = false; + #deferredRefreshRequestAt: Date | null = null; - /** - * A timestamp of the last attempt to refresh the table data. - */ - @state() - protected lastRefreshedAt: Date | null = null; + #synchronizeRefreshSchedule(): Promise { + if (!this.visible) { + if (!this.#deferredRefreshRequestAt) { + this.#deferredRefreshRequestAt = new Date(); + } + + return Promise.resolve(); + } + + if (!this.#deferredRefreshRequestAt) { + return Promise.resolve(); + } + + return this.fetch(); + } + + readonly #pageParam: string; + readonly #searchParam: string; /** - * A cached grouping of the last fetched results. - * - * @see {@linkcode Table.fetch} + * A mapping of the current items to their respective identifiers. */ - @state() - protected groups: GroupResult[] = []; + #itemKeys = new WeakMap(); - #pageParam = `${this.tagName.toLowerCase()}-page`; - #searchParam = `${this.tagName.toLowerCase()}-search`; + //#endregion @property({ type: Boolean }) public supportsQL: boolean = false; - //#region Properties + //#region Public Properties @property({ type: String }) public toolbarLabel: string | null = null; @@ -170,7 +220,7 @@ export abstract class Table public data: PaginatedResponse | null = null; @property({ type: Number, useDefault: true }) - public page = getURLParam(this.#pageParam, 1); + public page: number; /** * Set if your `selectedElements` use of the selection box is to enable bulk-delete, @@ -200,9 +250,10 @@ export abstract class Table public checkboxChip = false; /** - * A mapping of the current items to their respective identifiers. + * Whether the table is visible in the viewport. */ - #itemKeys = new WeakMap(); + @intersectionObserver() + public visible = false; /** * A mapping of item keys to selected items. @@ -230,18 +281,23 @@ export abstract class Table //#region Lifecycle - @state() - protected expandedElements = new Set(); - - @state() - protected error: APIError | null = null; - #selectAllCheckboxRef = createRef(); #refreshListener = () => { return this.fetch(); }; + constructor() { + super(); + const tagName = this.tagName.toLowerCase(); + + this.#pageParam = `${tagName}-page`; + this.#searchParam = `${tagName}-search`; + this.page = getURLParam(this.#pageParam, 1); + + this.logger = ConsoleLogger.prefix(tagName); + } + public override connectedCallback(): void { super.connectedCallback(); this.addEventListener(EVENT_REFRESH, this.#refreshListener); @@ -284,15 +340,20 @@ export abstract class Table ) { this.#synchronizeColumnProperties(); } + + if (changedProperties.has("visible") && this.hasUpdated) { + this.#synchronizeRefreshSchedule(); + } } - firstUpdated(): void { - this.fetch(); + protected override firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + this.#synchronizeRefreshSchedule(); } //#endregion - async defaultEndpointConfig(): Promise { + protected async defaultEndpointConfig(): Promise { return { ordering: this.order, page: this.page, @@ -301,7 +362,27 @@ export abstract class Table }; } - public fetch(): Promise { + /** + * Fetch data from the API endpoint. + * + * @see {@linkcode Table.apiEndpoint} + * @todo Make this protected. + */ + public async fetch(): Promise { + if (!this.visible) { + if (this.#deferredRefreshRequestAt) { + this.logger.debug("Skipping fetch; already scheduled"); + + return Promise.resolve(); + } + + this.logger.debug("Scheduling fetch for when table becomes visible"); + + this.#deferredRefreshRequestAt = new Date(); + + return Promise.resolve(); + } + if (this.loading) { return Promise.resolve(); } @@ -351,20 +432,25 @@ export abstract class Table .finally(() => { this.loading = false; this.lastRefreshedAt = new Date(); + this.#deferredRefreshRequestAt = null; this.requestUpdate(); }); } //#region Render - protected renderLoading(): TemplateResult { - return html` - -
- -
- - `; + protected renderLoading(): SlottedTemplateResult { + return guard( + [this.loading, this.#columnCount], + () => + html` + +
+ +
+ + `, + ); } protected renderEmpty(inner?: SlottedTemplateResult): TemplateResult { @@ -447,7 +533,7 @@ export abstract class Table if (this.error) { return this.renderEmpty(this.renderError()); } - if (this.loading && this.data === null) { + if (!this.visible || (this.loading && this.data === null)) { return this.renderLoading(); } @@ -668,13 +754,8 @@ export abstract class Table //#region Toolbar protected renderToolbar(): TemplateResult { - return html` ${this.renderObjectCreate()} - { - return this.fetch(); - }} - class="pf-m-secondary" - > + return html`${this.renderObjectCreate()} + ${msg("Refresh")}`; } @@ -867,9 +948,19 @@ export abstract class Table `; } - protected renderLoadingBar() { - if (!this.loading) return nothing; - return html``; + protected renderLoadingBar(): SlottedTemplateResult { + return guard([this.loading, this.label], () => { + if (!this.loading) return nothing; + + return html``; + }); } protected renderTable(): TemplateResult { @@ -889,6 +980,8 @@ export abstract class Table ${this.renderToolbarContainer()}
${guard([this.paginated, this.lastRefreshedAt], renderBottomPagination)}`; } - render(): TemplateResult { + protected override render(): SlottedTemplateResult { return this.renderTable(); } } diff --git a/web/src/elements/utils/pointer.ts b/web/src/elements/utils/pointer.ts index 0fd5c5f58819..f42051d77f4f 100644 --- a/web/src/elements/utils/pointer.ts +++ b/web/src/elements/utils/pointer.ts @@ -1,5 +1,5 @@ const InteractiveElementsQuery = - "[href],input,button,[role='button'],select,[tabindex]:not([tabindex='-1'])"; + "[href],input,button,i,[role='button'],select,[tabindex]:not([tabindex='-1'])"; /** * Whether a pointer event is targeting the element itself or one of its children. diff --git a/web/src/styles/authentik/base/common.css b/web/src/styles/authentik/base/common.css index ab65d58c55eb..ab8f12364294 100644 --- a/web/src/styles/authentik/base/common.css +++ b/web/src/styles/authentik/base/common.css @@ -67,6 +67,29 @@ /* #endregion */ +/* #region Animations */ + +@keyframes ak-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.ak-fade-in { + opacity: 0; + animation-fill-mode: forwards; + animation-name: ak-fade-in; + animation-duration: 150ms; + animation-iteration-count: 1; + animation-timing-function: linear; + animation-delay: 500ms; +} + +/* #endregion */ + /* #region Dark Theme */ /** diff --git a/web/src/styles/authentik/components/Page/page.css b/web/src/styles/authentik/components/Page/page.css index b51b9e7d28fc..5002a2b53dbc 100644 --- a/web/src/styles/authentik/components/Page/page.css +++ b/web/src/styles/authentik/components/Page/page.css @@ -2,6 +2,8 @@ * Extensions of PF4's sidebar color to better match our existing dark theme. */ .pf-c-page { + --pf-c-page__header--ZIndex: auto; + --pf-c-page__main--ZIndex: auto; --pf-c-page__main-nav--BackgroundColor: var(--pf-global--BackgroundColor--200); --pf-c-page__sidebar--m-dark--BackgroundColor: var(--pf-global--BackgroundColor--100); @@ -53,7 +55,7 @@ ak-tabs::part(row) { --pf-global--BackgroundColor--300 ); --pf-c-page__sidebar--BackgroundColor: var(--pf-c-page__sidebar--m-dark--BackgroundColor); - --pf-c-page__header--BackgroundColor: var(--pf-global--palette--black-1000); + --pf-c-page__header--BackgroundColor: transparent; /** * cf. Color reversal. diff --git a/web/src/user/LibraryPage/ak-library-impl.css b/web/src/user/LibraryPage/ak-library-impl.css index 06452dfe270d..65bf4bbd1db2 100644 --- a/web/src/user/LibraryPage/ak-library-impl.css +++ b/web/src/user/LibraryPage/ak-library-impl.css @@ -5,7 +5,7 @@ .pf-c-page { --pf-c-page--BackgroundColor: transparent; - --pf-c-page__main-section--BackgroundColor: transparent; + --pf-c-page__main-section--BackgroundColor: transparent !important; } .pf-c-page__header { @@ -45,7 +45,6 @@ } .pf-c-page__main-section { - background-color: transparent; padding-inline: 0; } diff --git a/web/src/user/LibraryPage/ak-library.ts b/web/src/user/LibraryPage/ak-library.ts index d44920dc78df..d129f96cb42a 100644 --- a/web/src/user/LibraryPage/ak-library.ts +++ b/web/src/user/LibraryPage/ak-library.ts @@ -107,7 +107,10 @@ export class LibraryPage extends AKElement { `; } - return html``; + return html``; } } diff --git a/web/src/user/index.entrypoint.css b/web/src/user/index.entrypoint.css index b83685e77483..6eb46a1d76d2 100644 --- a/web/src/user/index.entrypoint.css +++ b/web/src/user/index.entrypoint.css @@ -1,19 +1,12 @@ -.pf-c-drawer__panel { - z-index: var(--pf-global--ZIndex--md); -} - -.pf-c-page__main, -.pf-c-drawer__content, -.pf-c-page__drawer { - z-index: auto !important; - background-color: transparent !important; +.pf-c-page { + --pf-c-page__header--BackgroundColor: transparent; + --pf-c-page--BackgroundColor: transparent !important; + --pf-c-page__main-section--BackgroundColor: transparent !important; + --ak-user-interface--slant-m-light: var(--pf-global--BackgroundColor--100); + --ak-user-interface--slant-m-dark: var(--pf-global--BackgroundColor--200); } .pf-c-page__header { - background-color: transparent !important; - box-shadow: none !important; - color: black !important; - .pf-c-button { --pf-c-button--m-secondary--Color: var(--pf-global--primary-color--100); --pf-c-button--m-secondary--hover--Color: var(--pf-global--primary-color--100); @@ -22,12 +15,6 @@ } } -.pf-c-page { - background-color: transparent !important; - --ak-user-interface--slant-m-light: var(--pf-global--BackgroundColor--100); - --ak-user-interface--slant-m-dark: var(--pf-global--BackgroundColor--200); -} - .display-none { display: none; } @@ -46,7 +33,7 @@ color: #2b9af3; } -.background-wrapper { +[part="background-wrapper"] { height: 100dvh; width: 100%; position: fixed; @@ -56,7 +43,7 @@ background-color: var(--ak-user-interface--slant-m-dark); } -.background-default-slant { +[part="background-default-slant"] { background-color: var(--ak-user-interface--slant-m-light); clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 5vw)); height: 50dvh; @@ -67,6 +54,12 @@ --ak-user-interface--slant-m-dark: var(--pf-global--BackgroundColor--100); } +.pf-c-drawer { + /* TODO: Revisit this after native modals are implemented. */ + --pf-c-drawer__panel--ZIndex: auto; + --pf-c-drawer__content--ZIndex: auto; +} + .pf-c-drawer__main { min-height: calc(100vh - 76px); max-height: calc(100vh - 76px);