mirror of
https://github.com/goauthentik/authentik
synced 2026-04-25 17:15:26 +02:00
web: Close modal on route navigation (#21622)
* Close dialog on navigation. * web: update dialog, form, and sidebar styles with logical properties and scroll shadows Migrate dialog padding CSS variables from physical (top/right/bottom/left) to logical (block-start/inline-end/block-end/inline-start) naming. Add scroll shadow utility class (ak-m-scroll-shadows) for scrollable regions. Rework radio input and form control styles including transparent backgrounds, checkbox-style indicators, and improved hover states. Refactor FormGroup marker to use CSS custom properties for open/closed states. Move sidebar padding from nav container to scrollable list. * web: refine elements and components for accessibility, type safety, and consistency Add ARIA role and label to dialog body, apply scroll shadow classes to modal body, sidebar nav, and wizard main. Update ak-status-label to support tri-state (good/bad/null) rendering with ts-pattern matching and a neutral label. Simplify FormGroup by removing wrapper div around default slot, adding part attributes for header styling, and changing description to nullable type. Clean up LogViewer and StaticTable with proper access modifiers, override annotations, and nullable item types. Simplify ak-switch-input checked binding and remove unused slot attribute from ak-radio-input help text. * web: modernize application pages with modalInvoker and updated form patterns Refactor ApplicationCheckAccessForm to use static form metadata properties (verboseName, submitVerb, createLabel), formatAPISuccessMessage, and a private CoreApi instance. Migrate ApplicationViewPage from ak-forms-modal slots to the modalInvoker directive for both edit and check-access actions. Accept nullable input in createPaginatedResponse for better null-safety. Fix casing of dropdown menu items in ApplicationListPage. * web: migrate remaining view pages to modalInvoker (#21592) * Fix visibility check, search params. * Add scroll shadow. * Partial revert of input layout. * Tidy groups. * Fix check access form invoker, styles. * Optional sizing. * Lowercase * Revise checkbox style. * Close dialog on navigation. * Fix padding. * Touch up shadow heights. * Migrate remaining view pages to modalInvoker, add e2e coverage. * Fix alignment. * Fix click handler, add placeholders. * Fix issue where form field is not serialized.
This commit is contained in:
@@ -4,8 +4,12 @@ import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/SearchSelect/index";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { PFSize } from "#common/enums";
|
||||
import { APIMessage, MessageLevel } from "#common/messages";
|
||||
|
||||
import { Form } from "#elements/forms/Form";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import {
|
||||
Application,
|
||||
@@ -16,13 +20,24 @@ import {
|
||||
} from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
@customElement("ak-application-check-access-form")
|
||||
export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
||||
public static override verboseName = msg("Access");
|
||||
public static override submitVerb = msg("Check");
|
||||
public static override createLabel = msg("Check");
|
||||
public static override submittingVerb = msg("Checking");
|
||||
|
||||
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
|
||||
|
||||
#api = new CoreApi(DEFAULT_CONFIG);
|
||||
|
||||
public override size = PFSize.XLarge;
|
||||
|
||||
@property({ attribute: false })
|
||||
public application!: Application;
|
||||
|
||||
@@ -30,19 +45,27 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
||||
public result: PolicyTestResult | null = null;
|
||||
|
||||
@property({ attribute: false })
|
||||
public request?: number;
|
||||
public request: number | null = null;
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return msg("Successfully sent test-request.");
|
||||
public override formatAPISuccessMessage(): APIMessage {
|
||||
return {
|
||||
level: MessageLevel.success,
|
||||
message: msg("Successfully sent test-request."),
|
||||
};
|
||||
}
|
||||
|
||||
async send(data: { forUser: number }): Promise<PolicyTestResult> {
|
||||
protected override send(data: { forUser: number }): Promise<PolicyTestResult> {
|
||||
this.request = data.forUser;
|
||||
const result = await new CoreApi(DEFAULT_CONFIG).coreApplicationsCheckAccessRetrieve({
|
||||
slug: this.application?.slug,
|
||||
forUser: data.forUser,
|
||||
});
|
||||
return (this.result = result);
|
||||
|
||||
return this.#api
|
||||
.coreApplicationsCheckAccessRetrieve({
|
||||
slug: this.application?.slug,
|
||||
forUser: data.forUser,
|
||||
})
|
||||
.then((result) => {
|
||||
this.result = result;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
public override reset(): void {
|
||||
@@ -50,15 +73,14 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
|
||||
protected renderResult(): SlottedTemplateResult {
|
||||
const { passing, messages = [], logMessages = [] } = this.result || {};
|
||||
|
||||
renderResult(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Passing")}>
|
||||
return html`<ak-form-element-horizontal label=${msg("Passing")}>
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<span class="pf-c-form__label-text">
|
||||
<ak-status-label ?good=${this.result?.passing}></ak-status-label>
|
||||
<ak-status-label ?good=${ifPresent(passing)}></ak-status-label>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,54 +89,56 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
||||
<div class="pf-c-form__group-label">
|
||||
<div class="c-form__horizontal-group">
|
||||
<ul>
|
||||
${(this.result?.messages || []).length > 0
|
||||
? this.result?.messages?.map((m) => {
|
||||
return html`<li>
|
||||
<span class="pf-c-form__label-text">${m}</span>
|
||||
</li>`;
|
||||
})
|
||||
: html`<li>
|
||||
<span class="pf-c-form__label-text">-</span>
|
||||
</li>`}
|
||||
${messages.map((m) => {
|
||||
return html`<li>
|
||||
<span class="pf-c-form__label-text">${m}</span>
|
||||
</li>`;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Log messages")}>
|
||||
<ak-log-viewer .items=${this.result?.logMessages}></ak-log-viewer>
|
||||
</ak-form-element-horizontal>
|
||||
`;
|
||||
<ak-log-viewer .items=${logMessages}></ak-log-viewer>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
protected override renderForm(): TemplateResult {
|
||||
protected override renderForm(): SlottedTemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${msg("User")} required name="forUser">
|
||||
<ak-search-select
|
||||
placeholder=${msg("Select a user...")}
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
ordering: "username",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
|
||||
if (query) {
|
||||
args.search = query;
|
||||
}
|
||||
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
|
||||
|
||||
const users = await this.#api.coreUsersList(args);
|
||||
|
||||
return users.results;
|
||||
}}
|
||||
.renderElement=${(user: User): string => {
|
||||
return user.username;
|
||||
}}
|
||||
.renderDescription=${(user: User): TemplateResult => {
|
||||
.renderDescription=${(user: User): SlottedTemplateResult => {
|
||||
return html`${user.name}`;
|
||||
}}
|
||||
.value=${(user: User | undefined): number | undefined => {
|
||||
return user?.pk;
|
||||
}}
|
||||
.selected=${(user: User): boolean => {
|
||||
return user.pk.toString() === this.request?.toString();
|
||||
return (
|
||||
typeof this.request === "number" &&
|
||||
user.pk.toString() === this.request.toString()
|
||||
);
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
${this.result ? this.renderResult() : nothing}`;
|
||||
${this.renderResult()}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
"Opens the new application wizard, which will guide you through creating a new application with an existing provider.",
|
||||
)}
|
||||
>
|
||||
${msg("With New Provider...")}
|
||||
${msg("with New Provider...")}
|
||||
</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
@@ -214,7 +214,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage<Application>)
|
||||
"Opens the new application form, which will guide you through creating a new application with an existing provider.",
|
||||
)}
|
||||
>
|
||||
${msg("With Existing Provider...")}
|
||||
${msg("with Existing Provider...")}
|
||||
</button>
|
||||
</li>
|
||||
</menu>
|
||||
|
||||
@@ -16,11 +16,15 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { ApplicationCheckAccessForm } from "#admin/applications/ApplicationCheckAccessForm";
|
||||
import { ApplicationForm } from "#admin/applications/ApplicationForm";
|
||||
|
||||
import {
|
||||
Application,
|
||||
ContentTypeEnum,
|
||||
@@ -181,36 +185,28 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header"> ${msg("Update Application")} </span>
|
||||
<ak-application-form
|
||||
slot="form"
|
||||
.instancePk=${this.application.slug}
|
||||
>
|
||||
</ak-application-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-forms-modal .closeAfterSuccessfulSubmit=${false}>
|
||||
<span slot="submit">${msg("Check")}</span>
|
||||
<span slot="header"> ${msg("Check Application access")} </span>
|
||||
<ak-application-check-access-form
|
||||
slot="form"
|
||||
.application=${this.application}
|
||||
>
|
||||
</ak-application-check-access-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
>
|
||||
${msg("Check access")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
html`<button
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
${modalInvoker(ApplicationForm, {
|
||||
instancePk: this.application.slug,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary pf-m-block"
|
||||
${modalInvoker(
|
||||
ApplicationCheckAccessForm,
|
||||
{
|
||||
application: this.application,
|
||||
},
|
||||
{
|
||||
closedBy: "closerequest",
|
||||
},
|
||||
)}
|
||||
>
|
||||
${msg("Check access")}
|
||||
</button>
|
||||
${this.application.launchUrl
|
||||
? html`<a
|
||||
target="_blank"
|
||||
@@ -220,7 +216,7 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
|
||||
>
|
||||
${msg("Launch")}
|
||||
</a>`
|
||||
: nothing}`,
|
||||
: null}`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { SlottedTemplateResult } from "#elements/types";
|
||||
import { Provider, ProvidersApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-provider-table")
|
||||
@@ -23,29 +22,24 @@ export class ProviderTable extends Table<Provider> {
|
||||
|
||||
public override order = "name";
|
||||
|
||||
protected async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
|
||||
protected override async apiEndpoint(): Promise<PaginatedResponse<Provider>> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersAllList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
backchannel: this.backchannel,
|
||||
});
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
protected override columns: TableColumn[] = [
|
||||
// ---
|
||||
[msg("Name"), "username"],
|
||||
[msg("Type")],
|
||||
];
|
||||
|
||||
protected row(item: Provider): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div>
|
||||
<div>${item.name}</div>
|
||||
</div>`,
|
||||
html`${item.verboseName}`,
|
||||
];
|
||||
protected override row(item: Provider): SlottedTemplateResult[] {
|
||||
return [item.name, item.verboseName];
|
||||
}
|
||||
|
||||
protected renderSelectedChip(item: Provider): SlottedTemplateResult {
|
||||
protected override renderSelectedChip(item: Provider): SlottedTemplateResult {
|
||||
return item.name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ export class AkBackchannelProvidersInput extends AKElement {
|
||||
}}
|
||||
>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
|
||||
<ak-provider-table backchannel></ak-provider-table>
|
||||
</ak-form>
|
||||
`);
|
||||
@@ -100,7 +99,11 @@ export class AkBackchannelProvidersInput extends AKElement {
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>${map(this.providers, renderOneChip)}</ak-chip-group>
|
||||
<ak-chip-group
|
||||
@click=${this.openSelectBackchannelProvidersModal}
|
||||
placeholder=${msg("Select one or more backchannel providers...")}
|
||||
>${map(this.providers, renderOneChip)}</ak-chip-group
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
${this.help ? html`<p class="pf-c-form__helper-text">${this.help}</p>` : nothing}
|
||||
|
||||
@@ -20,6 +20,8 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-endpoints-device-form")
|
||||
export class EndpointDeviceForm extends ModelForm<EndpointDevice, string> {
|
||||
public static override verboseName = msg("Device");
|
||||
public static override verboseNamePlural = msg("Devices");
|
||||
loadInstance(pk: string): Promise<EndpointDevice> {
|
||||
return new EndpointsApi(DEFAULT_CONFIG).endpointsDevicesRetrieve({
|
||||
deviceUuid: pk,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import "#elements/cards/AggregateCard";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#admin/endpoints/devices/DeviceForm";
|
||||
import "#admin/endpoints/devices/DeviceAddHowTo";
|
||||
import "#elements/forms/ModalForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { TablePage } from "#elements/table/TablePage";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { EndpointDeviceForm } from "#admin/endpoints/devices/DeviceForm";
|
||||
|
||||
import { DeviceSummary, EndpointDevice, EndpointsApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -132,17 +133,14 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
|
||||
html`${item.facts.data.os?.name} ${item.facts.data.os?.version}`,
|
||||
html`${item.accessGroupObj?.name || "-"}`,
|
||||
item.facts.created ? Timestamp(item.facts.created) : html`-`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Device")}</span>
|
||||
<ak-endpoints-device-form slot="form" .instancePk=${item.deviceUuid}>
|
||||
</ak-endpoints-device-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
${modalInvoker(EndpointDeviceForm, { instancePk: item.deviceUuid })}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>`,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@ import "#admin/endpoints/devices/facts/DeviceProcessTable";
|
||||
import "#admin/endpoints/devices/facts/DeviceUserTable";
|
||||
import "#admin/endpoints/devices/facts/DeviceSoftwareTable";
|
||||
import "#admin/endpoints/devices/facts/DeviceGroupTable";
|
||||
import "#admin/endpoints/devices/DeviceForm";
|
||||
import "#admin/endpoints/devices/DeviceEvents";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/Tabs";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { APIError, parseAPIResponseError } from "#common/errors/network";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { Timestamp } from "#elements/table/shared";
|
||||
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
import renderDescriptionList, { DescriptionPair } from "#components/DescriptionList";
|
||||
|
||||
import { EndpointDeviceForm } from "#admin/endpoints/devices/DeviceForm";
|
||||
import { getSize, osFamilyToLabel, trySortNumerical } from "#admin/endpoints/devices/utils";
|
||||
|
||||
import { DeviceConnection, Disk, EndpointDeviceDetails, EndpointsApi } from "@goauthentik/api";
|
||||
@@ -124,18 +124,14 @@ export class DeviceViewPage extends AKElement {
|
||||
[msg("Device access group"), this.device?.accessGroupObj?.name ?? "-"],
|
||||
[
|
||||
msg("Actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Device")}</span>
|
||||
<ak-endpoints-device-form
|
||||
slot="form"
|
||||
.instancePk=${this.device?.deviceUuid}
|
||||
>
|
||||
</ak-endpoints-device-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
${modalInvoker(EndpointDeviceForm, {
|
||||
instancePk: this.device?.deviceUuid,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
],
|
||||
{ horizontal: true },
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "#admin/flows/BoundStagesList";
|
||||
import "#admin/flows/FlowDiagram";
|
||||
import "#admin/flows/FlowForm";
|
||||
import "#admin/policies/BoundPoliciesList";
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
@@ -11,11 +10,13 @@ import { AndNext, DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { isResponseErrorLike } from "#common/errors/network";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { FlowForm } from "#admin/flows/FlowForm";
|
||||
import { DesignationToLabel } from "#admin/flows/utils";
|
||||
|
||||
import { Flow, FlowsApi, ModelEnum } from "@goauthentik/api";
|
||||
@@ -97,21 +98,14 @@ export class FlowViewPage extends AKElement {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header"> ${msg("Update Flow")} </span>
|
||||
<ak-flow-form
|
||||
slot="form"
|
||||
.instancePk=${this.flow.slug}
|
||||
>
|
||||
</ak-flow-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-block pf-m-secondary"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
html`<button
|
||||
class="pf-c-button pf-m-block pf-m-secondary"
|
||||
${modalInvoker(FlowForm, {
|
||||
instancePk: this.flow.slug,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
<a
|
||||
class="pf-c-button pf-m-block pf-m-secondary"
|
||||
href=${this.flow.exportUrl}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "#admin/groups/ak-group-form";
|
||||
import "#admin/groups/RelatedUserList";
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/roles/ak-related-role-table";
|
||||
@@ -10,19 +9,21 @@ import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/ak-mdx/ak-mdx";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { GroupForm } from "#admin/groups/ak-group-form";
|
||||
|
||||
import { ContentTypeEnum, CoreApi, Group, ModelEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -150,18 +151,14 @@ export class GroupViewPage extends WithLicenseSummary(AKElement) {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-group-form slot="form" .instancePk=${this.group.pk}>
|
||||
</ak-group-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-m-primary pf-c-button pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
${modalInvoker(GroupForm, {
|
||||
instancePk: this.group.pk,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import "#admin/groups/ak-group-form";
|
||||
import "#admin/users/ak-user-group-table";
|
||||
import "#components/ak-status-label";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/HorizontalFormElement";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
|
||||
import { renderModal } from "#elements/dialogs";
|
||||
import { modalInvoker, renderModal } from "#elements/dialogs";
|
||||
import { AKFormSubmitEvent, Form } from "#elements/forms/Form";
|
||||
import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { GroupForm } from "#admin/groups/ak-group-form";
|
||||
|
||||
import { CoreApi, Group, User } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -23,6 +23,10 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@customElement("ak-group-related-add")
|
||||
export class RelatedGroupAdd extends Form<{ groups: string[] }> {
|
||||
public static override verboseName = msg("Group");
|
||||
public static override submitVerb = msg("Add");
|
||||
public static override createLabel = msg("Add");
|
||||
|
||||
@property({ attribute: false })
|
||||
public user?: User;
|
||||
|
||||
@@ -74,7 +78,10 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
<ak-chip-group
|
||||
@click=${this.openUserGroupSelectModal}
|
||||
placeholder=${msg("Select one or more groups...")}
|
||||
>
|
||||
${this.groupsToAdd.map((group) => {
|
||||
return html`<ak-chip
|
||||
removable
|
||||
@@ -151,40 +158,30 @@ export class RelatedGroupList extends Table<Group> {
|
||||
return [
|
||||
html`<a href="#/identity/groups/${item.pk}">${item.name}</a>`,
|
||||
html`<ak-status-label type="neutral" ?good=${item.isSuperuser}></ak-status-label>`,
|
||||
html` <ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Group")}</span>
|
||||
<ak-group-form slot="form" .instancePk=${item.pk}> </ak-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-plain"
|
||||
${modalInvoker(GroupForm, { instancePk: item.pk })}
|
||||
>
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
${this.targetUser
|
||||
? html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Add")}</span>
|
||||
<span slot="header">${msg("Add Group")}</span>
|
||||
<ak-group-related-add .user=${this.targetUser} slot="form">
|
||||
</ak-group-related-add>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Add to existing group")}
|
||||
</button>
|
||||
</ak-forms-modal>`
|
||||
? html`<button
|
||||
class="pf-c-button pf-m-primary"
|
||||
${modalInvoker(RelatedGroupAdd, { user: this.targetUser })}
|
||||
>
|
||||
${msg("Add to existing group")}
|
||||
</button>`
|
||||
: nothing}
|
||||
<ak-forms-modal>
|
||||
<span slot="submit">${msg("Create")}</span>
|
||||
<span slot="header">${msg("Create Group")}</span>
|
||||
<ak-group-form slot="form"> </ak-group-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-secondary">
|
||||
${msg("Add new group")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<button class="pf-c-button pf-m-secondary" ${modalInvoker(GroupForm)}>
|
||||
${msg("Add new group")}
|
||||
</button>
|
||||
${super.renderToolbar()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/oauth2/OAuth2ProviderForm";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
@@ -15,10 +14,13 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { OAuth2ProviderFormPage } from "#admin/providers/oauth2/OAuth2ProviderForm";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
CoreApi,
|
||||
@@ -238,21 +240,14 @@ export class OAuth2ProviderViewPage extends AKElement {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update OAuth2 Provider")}</span>
|
||||
<ak-provider-oauth2-form
|
||||
slot="form"
|
||||
.instancePk=${this.provider?.pk || 0}
|
||||
>
|
||||
</ak-provider-oauth2-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
${modalInvoker(OAuth2ProviderFormPage, {
|
||||
instancePk: this.provider?.pk || 0,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "#admin/providers/RelatedApplicationButton";
|
||||
import "#admin/providers/ssf/SSFProviderFormPage";
|
||||
import "#admin/providers/ssf/StreamTable";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
@@ -14,10 +13,13 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { SSFProviderFormPage } from "#admin/providers/ssf/SSFProviderFormPage";
|
||||
|
||||
import { ModelEnum, ProvidersApi, SSFProvider } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -155,15 +157,14 @@ export class SSFProviderViewPage extends AKElement {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update SSF Provider")}</span>
|
||||
<ak-provider-ssf-form slot="form" .instancePk=${this.provider.pk}>
|
||||
</ak-provider-ssf-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary pf-m-block">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
${modalInvoker(SSFProviderFormPage, {
|
||||
instancePk: this.provider.pk,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,10 @@ export class AddRelatedRoleForm extends Form<{ roles: string[] }> {
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
<ak-chip-group
|
||||
@click=${this.openRolesSelectionModal}
|
||||
placeholder=${msg("Select one or more roles...")}
|
||||
>
|
||||
${this.rolesToAdd.map((role) => {
|
||||
return html`<ak-chip
|
||||
removable
|
||||
|
||||
@@ -85,7 +85,10 @@ export class RolePermissionForm extends ModelForm<RolePermissionAssign, number>
|
||||
</button>
|
||||
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
<ak-chip-group
|
||||
@click=${this.openSelectPermissionsModal}
|
||||
placeholder=${msg("Select one or more permissions...")}
|
||||
>
|
||||
${this.permissionsToAdd.map((permission) => {
|
||||
return html`<ak-chip
|
||||
removable
|
||||
|
||||
@@ -2,21 +2,22 @@ import "#admin/groups/RelatedGroupList";
|
||||
import "#admin/groups/RelatedUserList";
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/lifecycle/ObjectLifecyclePage";
|
||||
import "#admin/roles/ak-role-form";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#admin/events/UserEvents";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/forms/ModalForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { WithLicenseSummary } from "#elements/mixins/license";
|
||||
|
||||
import { setPageDetails } from "#components/ak-page-navbar";
|
||||
import { renderDescriptionList } from "#components/DescriptionList";
|
||||
|
||||
import { RoleForm } from "#admin/roles/ak-role-form";
|
||||
|
||||
import { ContentTypeEnum, ModelEnum, RbacApi, Role } from "@goauthentik/api";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
@@ -99,21 +100,14 @@ export class RoleViewPage extends WithLicenseSummary(AKElement) {
|
||||
[msg("Name"), this.targetRole.name],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update Role")}</span>
|
||||
<ak-role-form
|
||||
slot="form"
|
||||
.instancePk=${this.targetRole.pk}
|
||||
>
|
||||
</ak-role-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
${modalInvoker(RoleForm, {
|
||||
instancePk: this.targetRole.pk,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { ModelForm } from "#elements/forms/ModelForm";
|
||||
import { msg } from "@lit/localize";
|
||||
|
||||
export abstract class BaseSourceForm<T extends object = object> extends ModelForm<T, string> {
|
||||
public static override verboseName = msg("Source");
|
||||
public static override verboseNamePlural = msg("Sources");
|
||||
getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated source.")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/sources/ldap/LDAPSourceConnectivity";
|
||||
import "#admin/sources/ldap/LDAPSourceForm";
|
||||
import "#admin/sources/ldap/LDAPSourceUserList";
|
||||
import "#admin/sources/ldap/LDAPSourceGroupList";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
@@ -8,7 +7,6 @@ import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/ActionButton/index";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "#elements/sync/SyncStatusCard";
|
||||
import "#elements/tasks/ScheduleList";
|
||||
|
||||
@@ -16,10 +14,13 @@ import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { LDAPSourceForm } from "#admin/sources/ldap/LDAPSourceForm";
|
||||
|
||||
import { LDAPSource, ModelEnum, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -105,23 +106,14 @@ export class LDAPSourceViewPage extends AKElement {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header"
|
||||
>${msg("Update LDAP Source")}</span
|
||||
>
|
||||
<ak-source-ldap-form
|
||||
slot="form"
|
||||
.instancePk=${this.source?.slug}
|
||||
>
|
||||
</ak-source-ldap-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
${modalInvoker(LDAPSourceForm, {
|
||||
instancePk: this.source?.slug,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
],
|
||||
{ twocolumn: true },
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import "#admin/policies/BoundPoliciesList";
|
||||
import "#admin/rbac/ak-rbac-object-permission-page";
|
||||
import "#admin/sources/oauth/OAuthSourceDiagram";
|
||||
import "#admin/sources/oauth/OAuthSourceForm";
|
||||
import "#admin/events/ObjectChangelog";
|
||||
import "#elements/CodeMirror";
|
||||
import "#elements/Tabs";
|
||||
import "#elements/buttons/SpinnerButton/index";
|
||||
import "#elements/forms/ModalForm";
|
||||
|
||||
import { DEFAULT_CONFIG } from "#common/api/config";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { modalInvoker } from "#elements/dialogs";
|
||||
import { sourceBindingTypeNotices } from "#elements/sources/utils";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import renderDescriptionList from "#components/DescriptionList";
|
||||
|
||||
import { OAuthSourceForm } from "#admin/sources/oauth/OAuthSourceForm";
|
||||
|
||||
import { ModelEnum, OAuthSource, ProviderTypeEnum, SourcesApi } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@@ -137,21 +138,14 @@ export class OAuthSourceViewPage extends AKElement {
|
||||
],
|
||||
[
|
||||
msg("Related actions"),
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit">${msg("Save Changes")}</span>
|
||||
<span slot="header">${msg("Update OAuth Source")}</span>
|
||||
<ak-source-oauth-form
|
||||
slot="form"
|
||||
.instancePk=${this.source.slug}
|
||||
>
|
||||
</ak-source-oauth-form>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>`,
|
||||
html`<button
|
||||
class="pf-c-button pf-m-primary pf-m-block"
|
||||
${modalInvoker(OAuthSourceForm, {
|
||||
instancePk: this.source.slug,
|
||||
})}
|
||||
>
|
||||
${msg("Edit")}
|
||||
</button>`,
|
||||
],
|
||||
])}
|
||||
</div>
|
||||
|
||||
@@ -251,10 +251,19 @@ export class UserListPage extends WithBrandConfig(
|
||||
const displayName = formatUserDisplayName(item);
|
||||
|
||||
return [
|
||||
html`<img class="pf-c-avatar pf-m-hidden pf-m-visible-on-xl" src=${item.avatar} />`,
|
||||
html`<a href="#/identity/users/${item.pk}">
|
||||
<div>${item.username}</div>
|
||||
<small>${item.name ? item.name : html`<${msg("No name set")}>`}</small>
|
||||
html`<img
|
||||
class="pf-c-avatar pf-m-hidden pf-m-visible-on-xl"
|
||||
src=${item.avatar}
|
||||
alt=${msg(str`Avatar for ${displayName}`)}
|
||||
/>`,
|
||||
html`<a
|
||||
href="#/identity/users/${item.pk}"
|
||||
aria-label=${msg(str`View details for ${displayName}`)}
|
||||
>
|
||||
<div aria-label=${msg(str`Username: ${item.username}`)}>${item.username}</div>
|
||||
<small aria-label=${msg(str`Display name: ${displayName || msg("No name set")}`)}
|
||||
>${displayName ? item.name : html`<${msg("No name set")}>`}</small
|
||||
>
|
||||
</a>`,
|
||||
html`<ak-status-label ?good=${item.isActive}></ak-status-label>`,
|
||||
Timestamp(item.lastLogin),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SlottedTemplateResult } from "#elements/types";
|
||||
import { CoreApi, Group } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing } from "lit";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
@@ -26,44 +26,44 @@ export class UserGroupTable extends Table<Group> {
|
||||
|
||||
public override order = "name";
|
||||
|
||||
protected async apiEndpoint(): Promise<PaginatedResponse<Group>> {
|
||||
protected override async apiEndpoint(): Promise<PaginatedResponse<Group>> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreGroupsList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
includeUsers: false,
|
||||
});
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
protected override columns: TableColumn[] = [
|
||||
[msg("Name"), "username"],
|
||||
[msg("Superuser"), "is_superuser"],
|
||||
[msg("Members"), ""],
|
||||
];
|
||||
|
||||
protected row(item: Group): SlottedTemplateResult[] {
|
||||
protected override row(item: Group): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div>${item.name}</div>`,
|
||||
item.name,
|
||||
html`<ak-status-label type="neutral" ?good=${item.isSuperuser}></ak-status-label>`,
|
||||
html`${(item.users || []).length}`,
|
||||
item.users?.length || 0,
|
||||
];
|
||||
}
|
||||
|
||||
protected renderSelectedChip(item: Group): SlottedTemplateResult {
|
||||
protected override renderSelectedChip(item: Group): SlottedTemplateResult {
|
||||
return item.name;
|
||||
}
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const willSuperuser = this.selectedElements.filter((g) => g.isSuperuser).length;
|
||||
const willSuperuser = this.selectedElements.some((g) => g.isSuperuser);
|
||||
|
||||
return html`${willSuperuser
|
||||
? html`
|
||||
<div class="pf-c-banner pf-m-warning">
|
||||
${msg(
|
||||
"Warning: Adding the user to the selected group(s) will give them superuser permissions.",
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${super.render()}`;
|
||||
if (!willSuperuser) {
|
||||
return super.render();
|
||||
}
|
||||
|
||||
return html`<div class="pf-c-banner pf-m-warning">
|
||||
${msg(
|
||||
"Warning: Adding the user to the selected group(s) will give them superuser permissions.",
|
||||
)}
|
||||
</div>
|
||||
${super.render()}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ export interface PaginatedResponse<T, A extends object = object> {
|
||||
* @param input An iterable of items to include in the results array.
|
||||
*/
|
||||
export function createPaginatedResponse<T = unknown, A extends object = object>(
|
||||
input: Iterable<T> = [],
|
||||
input?: Iterable<T> | null,
|
||||
): PaginatedResponse<T, A> {
|
||||
const results = Array.from(input);
|
||||
const results = Array.from(input ?? []);
|
||||
|
||||
return {
|
||||
pagination: {
|
||||
|
||||
@@ -34,12 +34,7 @@ export class AkRadioInput<T extends Jsonifiable> extends HorizontalLightComponen
|
||||
const helpText = this.help?.trim();
|
||||
|
||||
return html`${helpText
|
||||
? html`<p
|
||||
part="radio-help"
|
||||
class="pf-c-form__helper-radio"
|
||||
id=${this.helpID}
|
||||
slot="label-end"
|
||||
>
|
||||
? html`<p part="radio-help" class="pf-c-form__helper-radio" id=${this.helpID}>
|
||||
${helpText}
|
||||
</p>`
|
||||
: null}<ak-radio
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import Styles from "#components/ak-status-label.css";
|
||||
|
||||
import { P4Disposition } from "#styles/patternfly/constants";
|
||||
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@@ -43,30 +46,35 @@ export class AkStatusLabel extends AKElement {
|
||||
static styles = [PFLabel, Styles];
|
||||
|
||||
@property({ type: Boolean })
|
||||
good = false;
|
||||
public good: boolean | null = null;
|
||||
|
||||
@property({ type: String, attribute: "good-label" })
|
||||
goodLabel = msg("Yes");
|
||||
public goodLabel = msg("Yes");
|
||||
|
||||
@property({ type: String, attribute: "bad-label" })
|
||||
badLabel = msg("No");
|
||||
public badLabel = msg("No");
|
||||
|
||||
@property({ type: String, attribute: "neutral-label" })
|
||||
public neutralLabel = msg("-");
|
||||
|
||||
@property({ type: Boolean })
|
||||
compact = false;
|
||||
public compact = false;
|
||||
|
||||
@property({ type: String })
|
||||
type: P4Disposition = P4Disposition.Error;
|
||||
public type: P4Disposition = P4Disposition.Error;
|
||||
|
||||
render() {
|
||||
protected override render(): SlottedTemplateResult {
|
||||
const details = statusToDetails.get(this.type);
|
||||
|
||||
if (!details) {
|
||||
throw new TypeError(`Bad status type [${this.type}] passed to ak-status-label`);
|
||||
}
|
||||
|
||||
const [label, color, icon] = this.good
|
||||
? [this.goodLabel, "pf-m-green", "fa-check"]
|
||||
: [this.badLabel, ...details];
|
||||
const [label, color, icon] = match(this.good)
|
||||
.with(P.nullish, () => [this.neutralLabel, "pf-m-gray", "fa-question"] as const)
|
||||
.with(true, () => [this.goodLabel, "pf-m-green", "fa-check"] as const)
|
||||
.with(false, () => [this.badLabel, ...details] as const)
|
||||
.exhaustive();
|
||||
|
||||
const classes = {
|
||||
"pf-c-label": true,
|
||||
|
||||
@@ -60,8 +60,6 @@ export class AkSwitchInput extends AKElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const doCheck = this.checked ? this.checked : undefined;
|
||||
|
||||
return html`<ak-form-element-horizontal name=${this.name} ?required=${this.required}>
|
||||
<label class="pf-c-switch" for="${this.#fieldID}">
|
||||
<input
|
||||
@@ -69,7 +67,7 @@ export class AkSwitchInput extends AKElement {
|
||||
aria-describedby="${this.#fieldID}-help"
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${doCheck}
|
||||
?checked=${this.checked}
|
||||
/>
|
||||
<span class="pf-c-switch__toggle">
|
||||
<span class="pf-c-switch__toggle-icon">
|
||||
|
||||
@@ -335,7 +335,7 @@ export abstract class WizardStep extends AKElement {
|
||||
</aside>
|
||||
<main
|
||||
part="wizard-main"
|
||||
class="pf-c-wizard__main ak-m-thin-scrollbar"
|
||||
class="pf-c-wizard__main ak-m-thin-scrollbar ak-m-scroll-shadows"
|
||||
aria-label=${msg("Wizard content")}
|
||||
>
|
||||
<div id="main-content" class="pf-c-wizard__main-body">
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
padding-top: var(--ak-c-dialog__header--PaddingTop);
|
||||
padding-bottom: var(--ak-c-dialog__header--PaddingBottom);
|
||||
padding-right: var(--ak-c-dialog__header--PaddingRight);
|
||||
padding-left: var(--ak-c-dialog__header--PaddingLeft);
|
||||
padding-block-start: var(--ak-c-dialog__header--PaddingBlockStart);
|
||||
padding-block-end: var(--ak-c-dialog__header--PaddingBlockEnd);
|
||||
padding-inline-end: var(--ak-c-dialog__header--PaddingInlineEnd);
|
||||
padding-inline-start: var(--ak-c-dialog__header--PaddingInlineStart);
|
||||
}
|
||||
|
||||
.ak-c-dialog__header.pf-m-help {
|
||||
@@ -32,12 +32,7 @@
|
||||
}
|
||||
|
||||
.ak-c-dialog__header:last-child {
|
||||
padding-bottom: var(--ak-c-dialog__header--last-child--PaddingBottom);
|
||||
}
|
||||
|
||||
.ak-c-dialog__header + .ak-c-dialog__body,
|
||||
.ak-c-dialog__header + slot + .ak-c-dialog__body {
|
||||
--ak-c-dialog__body--PaddingTop: 0;
|
||||
padding-block-end: var(--ak-c-dialog__header--last-child--PaddingBlockEnd);
|
||||
}
|
||||
|
||||
.ak-c-dialog__header-main {
|
||||
@@ -73,7 +68,7 @@
|
||||
}
|
||||
|
||||
.ak-c-dialog__description {
|
||||
padding-top: var(--ak-c-dialog__description--PaddingTop);
|
||||
padding-block-start: var(--ak-c-dialog__description--PaddingBlockStart);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -82,9 +77,10 @@
|
||||
|
||||
.ak-c-dialog__body {
|
||||
min-height: var(--ak-c-dialog__body--MinHeight);
|
||||
padding-top: var(--ak-c-dialog__body--PaddingTop);
|
||||
padding-right: var(--ak-c-dialog__body--PaddingRight);
|
||||
padding-left: var(--ak-c-dialog__body--PaddingLeft);
|
||||
padding-block-start: var(--ak-c-dialog__body--PaddingBlockStart);
|
||||
padding-inline-end: var(--ak-c-dialog__body--PaddingInlineEnd);
|
||||
padding-inline-start: var(--ak-c-dialog__body--PaddingInlineStart);
|
||||
padding-block-end: var(--ak-c-dialog__body--PaddingBlockEnd);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
@@ -93,7 +89,7 @@
|
||||
}
|
||||
|
||||
.ak-c-dialog__body:last-child {
|
||||
padding-bottom: var(--ak-c-dialog__body--last-child--PaddingBottom);
|
||||
padding-block-end: var(--ak-c-dialog__body--last-child--PaddingBlockEnd);
|
||||
}
|
||||
|
||||
.ak-c-dialog__footer {
|
||||
@@ -102,14 +98,11 @@
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
margin-block-start: var(--pf-global--spacer--md);
|
||||
padding-block-start: var(--ak-c-dialog__footer--PaddingBlockStart);
|
||||
padding-inline-end: var(--ak-c-dialog__footer--PaddingInlineEnd);
|
||||
padding-block-end: var(--ak-c-dialog__footer--PaddingBlockEnd);
|
||||
padding-inline-start: var(--ak-c-dialog__footer--PaddingInlineStart);
|
||||
|
||||
padding-top: var(--ak-c-dialog__footer--PaddingTop);
|
||||
padding-right: var(--ak-c-dialog__footer--PaddingRight);
|
||||
padding-bottom: var(--ak-c-dialog__footer--PaddingBottom);
|
||||
padding-left: var(--ak-c-dialog__footer--PaddingLeft);
|
||||
|
||||
box-shadow: var(--ak-c-dialog__footer--BoxShadow);
|
||||
gap: var(--pf-global--spacer--xs);
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
|
||||
@@ -312,9 +312,16 @@ export class AKModal extends AKElement implements TransclusionParentElement {
|
||||
this.beforeBodySlot.name = "before-body";
|
||||
|
||||
this.dialogBody = this.ownerDocument.createElement("div");
|
||||
this.dialogBody.classList.add("ak-c-dialog__body", "ak-m-thin-scrollbar");
|
||||
this.dialogBody.classList.add(
|
||||
"ak-c-dialog__body",
|
||||
"ak-m-thin-scrollbar",
|
||||
"ak-m-scroll-shadows",
|
||||
);
|
||||
this.dialogBody.setAttribute("part", "body");
|
||||
|
||||
this.dialogBody.role = "region";
|
||||
this.dialogBody.ariaLabel = msg("Dialog content");
|
||||
|
||||
this.dialogBody.appendChild(this.defaultSlot);
|
||||
|
||||
this.addEventListener("command", (event) => {
|
||||
@@ -379,6 +386,9 @@ export class AKModal extends AKElement implements TransclusionParentElement {
|
||||
nextSlottedElement.visible = true;
|
||||
}
|
||||
|
||||
nextSlottedElement.classList.add("ak-c-dialog__slotted-content");
|
||||
|
||||
this.slottedElement?.classList.remove("ak-c-dialog__slotted-content");
|
||||
this.slottedElement = nextSlottedElement;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,12 +83,12 @@
|
||||
/* #region Header */
|
||||
|
||||
.ak-c-dialog {
|
||||
--ak-c-dialog__header--PaddingTop: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--PaddingRight: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--last-child--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--body--PaddingTop: var(--pf-global--spacer--md);
|
||||
--ak-c-dialog__header--PaddingBlockStart: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--PaddingBlockEnd: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--PaddingInlineEnd: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--PaddingInlineStart: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--last-child--PaddingBlockEnd: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__header--body--PaddingBlockStart: var(--pf-global--spacer--md);
|
||||
|
||||
--ak-c-dialog__title--LineHeight: var(--pf-global--LineHeight--sm);
|
||||
--ak-c-dialog__title--FontFamily: var(--pf-global--FontFamily--heading--sans-serif);
|
||||
@@ -96,7 +96,7 @@
|
||||
--ak-c-dialog__title-icon--MarginRight: var(--pf-global--spacer--sm);
|
||||
--ak-c-dialog__title-icon--Color: var(--pf-global--Color--100);
|
||||
|
||||
--ak-c-dialog__description--PaddingTop: var(--pf-global--spacer--xs);
|
||||
--ak-c-dialog__description--PaddingBlockStart: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -108,10 +108,11 @@
|
||||
var(--pf-global--FontSize--md) * var(--pf-global--LineHeight--md)
|
||||
);
|
||||
|
||||
--ak-c-dialog__body--PaddingTop: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__body--PaddingRight: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__body--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__body--last-child--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__body--PaddingBlockStart: var(--pf-global--spacer--md);
|
||||
--ak-c-dialog__body--PaddingBlockEnd: var(--pf-global--spacer--md);
|
||||
--ak-c-dialog__body--PaddingInlineEnd: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__body--PaddingInlineStart: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__body--last-child--PaddingBlockEnd: var(--pf-global--spacer--lg);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -119,12 +120,10 @@
|
||||
/* #region Footer */
|
||||
|
||||
.ak-c-dialog {
|
||||
--ak-c-dialog__footer--PaddingTop: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--PaddingRight: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--PaddingBottom: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--PaddingLeft: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--BoxShadow: inset 0 0.5px 0
|
||||
var(--pf-global--BackgroundColor--dark-transparent-200);
|
||||
--ak-c-dialog__footer--PaddingBlockStart: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--PaddingInlineEnd: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--PaddingBlockEnd: var(--pf-global--spacer--lg);
|
||||
--ak-c-dialog__footer--PaddingInlineStart: var(--pf-global--spacer--lg);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
@@ -283,11 +282,14 @@
|
||||
}
|
||||
|
||||
.ak-c-dialog__content:has([part*="wizard"]) {
|
||||
--ak-c-dialog__body--PaddingInlineEnd: 0;
|
||||
--ak-c-dialog__body--PaddingInlineStart: 0;
|
||||
--ak-c-dialog__body--last-child--PaddingBlockEnd: 0;
|
||||
--ak-c-dialog__body--PaddingBlockStart: 0;
|
||||
--ak-c-dialog__body--PaddingBlockEnd: 0;
|
||||
--ak-m-scroll-shadows--BorderWidth: 0;
|
||||
|
||||
grid-template-rows: [body] auto;
|
||||
--ak-c-dialog__body--PaddingRight: 0;
|
||||
--ak-c-dialog__body--PaddingLeft: 0;
|
||||
--ak-c-dialog__body--last-child--PaddingBottom: 0;
|
||||
--ak-c-dialog__body--PaddingTop: 0;
|
||||
|
||||
::part(body) {
|
||||
overscroll-behavior: none;
|
||||
|
||||
@@ -7,6 +7,7 @@ import "#elements/dialogs/ak-modal";
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
|
||||
import { DialogInit } from "#elements/dialogs/shared";
|
||||
import { RouteChangeEvent } from "#elements/router/events";
|
||||
import { ifPresent } from "#elements/utils/attributes";
|
||||
|
||||
import { html, render } from "lit";
|
||||
@@ -89,6 +90,8 @@ export function renderDialog(
|
||||
onDispose,
|
||||
}: DialogInit = {},
|
||||
): Promise<void> {
|
||||
const eventAbortController = new AbortController();
|
||||
|
||||
const dialog = ownerDocument.createElement("dialog");
|
||||
dialog.classList.add("ak-c-dialog", ...classList);
|
||||
dialog.closedBy = closedBy;
|
||||
@@ -121,8 +124,15 @@ export function renderDialog(
|
||||
setDialogCountAttribute(-1, ownerDocument);
|
||||
|
||||
onDispose?.(event);
|
||||
eventAbortController.abort();
|
||||
};
|
||||
|
||||
window.addEventListener(RouteChangeEvent.eventName, dispose, {
|
||||
passive: true,
|
||||
once: true,
|
||||
signal: eventAbortController.signal,
|
||||
});
|
||||
|
||||
dialog.addEventListener("close", dispose, {
|
||||
passive: true,
|
||||
once: true,
|
||||
|
||||
@@ -11,16 +11,16 @@ import { SlottedTemplateResult } from "#elements/types";
|
||||
import { LogEvent, LogLevelEnum } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
||||
import { CSSResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
@customElement("ak-log-viewer")
|
||||
export class LogViewer extends StaticTable<LogEvent> {
|
||||
expandable = true;
|
||||
public static styles: CSSResult[] = [...super.styles, PFDescriptionList];
|
||||
|
||||
static styles: CSSResult[] = [...super.styles, PFDescriptionList];
|
||||
public override expandable = true;
|
||||
|
||||
protected override renderEmpty(): SlottedTemplateResult {
|
||||
return super.renderEmpty(
|
||||
@@ -28,7 +28,7 @@ export class LogViewer extends StaticTable<LogEvent> {
|
||||
);
|
||||
}
|
||||
|
||||
renderExpanded(item: LogEvent): TemplateResult {
|
||||
protected override renderExpanded(item: LogEvent): SlottedTemplateResult {
|
||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
@@ -53,8 +53,8 @@ export class LogViewer extends StaticTable<LogEvent> {
|
||||
</dl>`;
|
||||
}
|
||||
|
||||
renderToolbarContainer(): SlottedTemplateResult {
|
||||
return nothing;
|
||||
protected override renderToolbarContainer(): SlottedTemplateResult {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
@@ -64,7 +64,7 @@ export class LogViewer extends StaticTable<LogEvent> {
|
||||
[msg("Logger")],
|
||||
];
|
||||
|
||||
statusForItem(item: LogEvent): string {
|
||||
protected statusForItem(item: LogEvent): string {
|
||||
switch (item.logLevel) {
|
||||
case LogLevelEnum.Critical:
|
||||
case LogLevelEnum.Error:
|
||||
@@ -82,15 +82,15 @@ export class LogViewer extends StaticTable<LogEvent> {
|
||||
return formatElapsedTime(item.timestamp);
|
||||
}
|
||||
|
||||
row(item: LogEvent): SlottedTemplateResult[] {
|
||||
protected override row(item: LogEvent): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<ak-timestamp .timestamp=${item.timestamp} refresh></ak-timestamp>`,
|
||||
html`<ak-status-label
|
||||
type=${this.statusForItem(item)}
|
||||
bad-label=${item.logLevel}
|
||||
></ak-status-label>`,
|
||||
html`${item.event}`,
|
||||
html`${item.logger}`,
|
||||
item.event,
|
||||
item.logger,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,10 +467,16 @@ export class Form<T = Record<string, unknown>, D = T>
|
||||
|
||||
const assignedElements = this.defaultSlot.assignedElements({ flatten: true });
|
||||
|
||||
const [firstAssignedElement] = assignedElements;
|
||||
const formFields = assignedElements.filter(isFormField);
|
||||
|
||||
if (assignedElements.length === 1 && isFormField(firstAssignedElement)) {
|
||||
return firstAssignedElement.toJSON() as D;
|
||||
if (formFields.length) {
|
||||
if (formFields.length === 1) {
|
||||
return formFields[0].toJSON() as D;
|
||||
}
|
||||
|
||||
throw new TypeError(
|
||||
`Multiple form-associated elements found in the form, but no "ak-form-element-horizontal" elements found. Unable to determine which element(s) to serialize.`,
|
||||
);
|
||||
}
|
||||
|
||||
const namedElements = assignedElements.filter((element): element is AKElement => {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
:host([theme="dark"]) {
|
||||
--ak-c-form-group__marker--Color: var(--pf-global--Color--300);
|
||||
--ak-c-form-group__marker--ColorHover: var(--pf-global--Color--200);
|
||||
:host {
|
||||
--ak-c-form-group__marker--Color: var(--pf-global--Color--200);
|
||||
--ak-c-form-group__marker--ColorHover: var(--pf-global--Color--100);
|
||||
--ak-c-form-group__marker--Opacity: 75%;
|
||||
|
||||
--ak-c-form-group--marker--Content: "\f105";
|
||||
--ak-c-form-group--marker--ContentOpen: "\f107";
|
||||
}
|
||||
|
||||
:host([theme="dark"]) {
|
||||
details {
|
||||
@media (prefers-contrast: more) {
|
||||
background: var(--pf-global--BackgroundColor--150);
|
||||
@@ -10,63 +16,79 @@
|
||||
}
|
||||
|
||||
details {
|
||||
&[open] {
|
||||
--ak-c-form-group--marker--Content: var(--ak-c-form-group--marker--ContentOpen);
|
||||
--ak-c-form-group__marker--Opacity: 25%;
|
||||
}
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
border: 1px solid var(--pf-global--BorderColor--200);
|
||||
background: var(--pf-global--BackgroundColor--150);
|
||||
}
|
||||
}
|
||||
|
||||
&::details-content {
|
||||
padding-inline-start: calc(
|
||||
var(--pf-global--spacer--lg) + var(--pf-global--spacer--form-element)
|
||||
);
|
||||
padding-inline-end: var(--pf-global--spacer--md);
|
||||
padding-block-end: var(--pf-global--spacer--sm);
|
||||
}
|
||||
details::details-content {
|
||||
padding-inline-start: calc(
|
||||
var(--pf-global--spacer--lg) + var(--pf-global--spacer--form-element)
|
||||
);
|
||||
padding-inline-end: var(--pf-global--spacer--md);
|
||||
padding-block-end: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
& > summary {
|
||||
backdrop-filter: var(--ak-c-dialog__backdrop--BackdropFilter);
|
||||
details > summary:hover {
|
||||
--ak-c-form-group__marker--Color: var(--ak-c-form-group__marker--ColorHover);
|
||||
}
|
||||
|
||||
inset-block-start: 0;
|
||||
position: sticky;
|
||||
background: var(--ak-c-form-group--BackgroundColor, transparent);
|
||||
z-index: var(--ak-c-form-group--ZIndex, 1);
|
||||
details[open] > summary:hover {
|
||||
--ak-c-form-group__marker--Opacity: 100%;
|
||||
}
|
||||
|
||||
list-style-position: outside;
|
||||
margin-inline-start: var(--pf-global--spacer--sm);
|
||||
padding-inline-start: calc(var(--pf-global--spacer--md) + 0.25rem);
|
||||
padding-block: var(--pf-global--spacer-xs);
|
||||
padding-inline: var(--pf-global--spacer--md);
|
||||
list-style-type: "\f105";
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
details > summary {
|
||||
background: var(--ak-c-form-group--BackgroundColor, transparent);
|
||||
|
||||
font-weight: bold;
|
||||
list-style-position: outside;
|
||||
margin-inline-start: var(--pf-global--spacer--sm);
|
||||
padding-inline-start: calc(var(--pf-global--spacer--md) + 0.25rem);
|
||||
padding-block: var(--pf-global--spacer-xs);
|
||||
padding-inline: var(--pf-global--spacer--md);
|
||||
list-style-type: var(--ak-c-form-group--marker--Content);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
@media (prefers-contrast: more) {
|
||||
text-decoration: underline;
|
||||
margin-inline-start: var(--pf-global--spacer--lg);
|
||||
padding-block: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
&::marker {
|
||||
color: var(--ak-c-form-group__marker--Color, var(--pf-global--Color--200));
|
||||
transition: var(--pf-c-form__field-group-toggle-icon--Transition);
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
&:hover::marker {
|
||||
color: var(--ak-c-form-group__marker--ColorHover, var(--pf-global--Color--100));
|
||||
}
|
||||
}
|
||||
|
||||
&[open] summary {
|
||||
list-style-type: "\f107";
|
||||
@media (prefers-contrast: more) {
|
||||
text-decoration: underline;
|
||||
margin-inline-start: var(--pf-global--spacer--lg);
|
||||
padding-block: var(--pf-global--spacer--sm);
|
||||
}
|
||||
}
|
||||
|
||||
details summary::marker {
|
||||
content: var(--ak-c-form-group--marker--Content, "\f105");
|
||||
color: color-mix(
|
||||
var(--ak-c-form-group__marker--Color) var(--ak-c-form-group__marker--Opacity),
|
||||
transparent
|
||||
);
|
||||
transition: var(--pf-c-form__field-group-toggle-icon--Transition);
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
opacity: var(--ak-c-form-group__marker--Opacity);
|
||||
}
|
||||
|
||||
[part="group-header-title"] {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
[part="form-group-header-title"] {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
gap: var(--pf-global--spacer--sm);
|
||||
backdrop-filter: var(--ak-c-dialog__backdrop--BackdropFilter);
|
||||
}
|
||||
|
||||
[part="label"] {
|
||||
padding: var(--pf-global--spacer--xs);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pf-c-form__field-group-header-description {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class AKFormGroup extends AKElement {
|
||||
public label = msg("Details");
|
||||
|
||||
@property({ type: String, reflect: true })
|
||||
public description?: string;
|
||||
public description: string | null = null;
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -126,10 +126,14 @@ export class AKFormGroup extends AKElement {
|
||||
aria-describedby="form-group-expandable-content-description"
|
||||
>
|
||||
<summary @click=${this.toggle}>
|
||||
<div class="pf-c-form__field-group-header-main">
|
||||
<header class="pf-c-form__field-group-header-title">
|
||||
<div class="pf-c-form__field-group-header-main" part="group-header">
|
||||
<header
|
||||
class="pf-c-form__field-group-header-title"
|
||||
part="group-header-title"
|
||||
>
|
||||
<div
|
||||
class="pf-c-form__field-group-header-title-text"
|
||||
part="form-group-header-title"
|
||||
id="form-group-header-title"
|
||||
role="heading"
|
||||
aria-level="3"
|
||||
@@ -138,7 +142,6 @@ export class AKFormGroup extends AKElement {
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="pf-c-form__field-group-header-description"
|
||||
data-test-id="form-group-header-description"
|
||||
@@ -149,9 +152,7 @@ export class AKFormGroup extends AKElement {
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<div id="form-group-expandable-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -22,4 +22,5 @@
|
||||
|
||||
.pf-c-radio__description {
|
||||
text-wrap: balance;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
}
|
||||
|
||||
.pf-c-nav {
|
||||
--ak-m-scroll-shadows--BorderStyle: none;
|
||||
--ak-c-nav__item--BorderColor: var(--pf-global--BorderColor--100);
|
||||
--pf-c-nav__subnav__link--hover--after--BorderColor: var(--ak-accent);
|
||||
|
||||
@@ -25,7 +26,6 @@
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
padding-block-start: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-c-nav__section + .pf-c-nav__section {
|
||||
@@ -35,6 +35,7 @@
|
||||
.pf-c-nav__list {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding-block-start: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-c-nav__link.pf-m-current::after,
|
||||
|
||||
@@ -29,7 +29,7 @@ export class Sidebar extends AKElement {
|
||||
?hidden=${this.hidden}
|
||||
aria-label=${msg("Global navigation")}
|
||||
role="navigation"
|
||||
class="pf-c-nav__list"
|
||||
class="pf-c-nav__list ak-m-thin-scrollbar ak-m-scroll-shadows"
|
||||
part="list"
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
@@ -4,31 +4,31 @@ import { PaginatedResponse, Table } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { PropertyValues } from "lit";
|
||||
import { html, nothing } from "lit-html";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
export abstract class StaticTable<T extends object> extends Table<T> {
|
||||
protected override searchEnabled = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
items?: T[] = [];
|
||||
public items: T[] | null = [];
|
||||
|
||||
protected override async apiEndpoint(): Promise<PaginatedResponse<T, object>> {
|
||||
return createPaginatedResponse(this.items ?? []);
|
||||
}
|
||||
|
||||
protected override renderToolbar(): SlottedTemplateResult {
|
||||
return html`${this.renderObjectCreate()}`;
|
||||
return this.renderObjectCreate();
|
||||
}
|
||||
|
||||
protected override renderTablePagination(): SlottedTemplateResult {
|
||||
return nothing;
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has("items")) {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
super.willUpdate(changedProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,3 +181,67 @@
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
.ak-m-scroll-shadows {
|
||||
--ak-m-scroll-shadows--BorderWidth--light: 0.5px;
|
||||
--ak-m-scroll-shadows--BorderWidth--dark: 1px;
|
||||
--ak-m-scroll-shadows--Translucency: 75%;
|
||||
--ak-m-scroll-shadows--BackgroundColor: var(--pf-global--BackgroundColor--100, white);
|
||||
--ak-m-scroll-shadows--ShadowColor: var(--pf-global--Color--400);
|
||||
--ak-m-scroll-shadows--ShadowColorMix: color-mix(
|
||||
var(--ak-m-scroll-shadows--ShadowColor),
|
||||
transparent var(--ak-m-scroll-shadows--Translucency)
|
||||
);
|
||||
|
||||
--ak-m-scroll-shadows--ShadowLength: 2.75rem;
|
||||
--ak-m-scroll-shadow--ShadowDepth: 1rem;
|
||||
|
||||
background-image:
|
||||
linear-gradient(var(--ak-m-scroll-shadows--BackgroundColor) 30%, transparent),
|
||||
linear-gradient(transparent, var(--ak-m-scroll-shadows--BackgroundColor) 70%),
|
||||
radial-gradient(
|
||||
farthest-side at 50% 0px,
|
||||
var(--ak-m-scroll-shadows--ShadowColorMix),
|
||||
transparent
|
||||
),
|
||||
radial-gradient(
|
||||
farthest-side at 50% 100%,
|
||||
var(--ak-m-scroll-shadows--ShadowColorMix),
|
||||
transparent
|
||||
);
|
||||
|
||||
background-position-x: center;
|
||||
background-position-y: top, bottom;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size:
|
||||
100% var(--ak-m-scroll-shadows--ShadowLength),
|
||||
100% var(--ak-m-scroll-shadows--ShadowLength),
|
||||
100% var(--ak-m-scroll-shadow--ShadowDepth),
|
||||
100% var(--ak-m-scroll-shadow--ShadowDepth);
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
|
||||
border-block-color: var(
|
||||
--ak-m-scroll-shadows--BorderColor,
|
||||
color-mix(
|
||||
var(--ak-m-scroll-shadows--ShadowColor),
|
||||
transparent var(--ak-m-scroll-shadows--Translucency)
|
||||
)
|
||||
);
|
||||
border-block-style: var(--ak-m-scroll-shadows--BorderStyle, solid);
|
||||
border-block-width: var(
|
||||
--ak-m-scroll-shadows--BorderWidth,
|
||||
var(--ak-m-scroll-shadows--BorderWidth--light)
|
||||
);
|
||||
}
|
||||
|
||||
html[data-theme="dark"],
|
||||
:host([theme="dark"]) {
|
||||
.ak-m-scroll-shadows {
|
||||
--ak-m-scroll-shadow--ShadowDepth: 0;
|
||||
border-block-width: var(
|
||||
--ak-m-scroll-shadows--BorderWidth,
|
||||
var(--ak-m-scroll-shadows--BorderWidth--dark)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
/* #region Form controls */
|
||||
|
||||
.pf-c-form-control {
|
||||
--pf-c-form-control--BackgroundColor: transparent !important;
|
||||
--pf-c-form-control--BorderTopColor: transparent !important;
|
||||
--pf-c-form-control--BorderRightColor: transparent !important;
|
||||
--pf-c-form-control--BorderLeftColor: transparent !important;
|
||||
@@ -174,7 +175,9 @@ fieldset {
|
||||
}
|
||||
|
||||
label.pf-c-radio {
|
||||
--pf-c-radio--BorderColor: var(--pf-global--BorderColor--300);
|
||||
--pf-c-radio--checkmark--BorderColor: var(--pf-global--BorderColor--300);
|
||||
--pf-c-radio--checkmark--Color: transparent;
|
||||
--pf-c-radio--BorderColor: transparent;
|
||||
--pf-c-radio--BoxShadowColor: transparent;
|
||||
|
||||
--pf-c-radio--checked--BackgroundColor: transparent;
|
||||
@@ -182,21 +185,26 @@ label.pf-c-radio {
|
||||
--pf-c-radio--hover--BorderColor: var(--pf-global--active-color--200);
|
||||
|
||||
--pf-c-radio--disabled--BackgroundColor: var(--pf-global--BackgroundColor--150);
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
color 0.2s;
|
||||
|
||||
padding-inline: var(--pf-global--spacer--md);
|
||||
padding-block: var(--pf-global--spacer--md);
|
||||
padding-block: var(--pf-global--spacer--sm);
|
||||
cursor: pointer;
|
||||
background-color: var(--pf-c-radio--BackgroundColor, transparent);
|
||||
border: 1px solid var(--pf-c-radio--BorderColor);
|
||||
border-radius: var(--pf-global--BorderRadius--sm);
|
||||
position: relative;
|
||||
|
||||
align-content: center;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"input ."
|
||||
"input .";
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: var(--pf-global--spacer--md);
|
||||
row-gap: 0;
|
||||
|
||||
.pf-c-radio__input {
|
||||
grid-area: input;
|
||||
@@ -206,8 +214,17 @@ label.pf-c-radio {
|
||||
margin-inline-start: var(--pf-global--spacer--sm);
|
||||
appearance: none;
|
||||
content: none;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
padding: 0.25em;
|
||||
border: 1px solid;
|
||||
color: var(--pf-c-radio--checkmark--BorderColor, transparent);
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
@@ -216,7 +233,8 @@ label.pf-c-radio {
|
||||
content: "\f00c";
|
||||
display: block;
|
||||
line-height: 1;
|
||||
color: var(--pf-c-radio--checkmark--Color, transparent);
|
||||
color: var(--pf-c-radio--checkmark--Color);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,14 +252,15 @@ label.pf-c-radio {
|
||||
inset-block-end: 0;
|
||||
}
|
||||
|
||||
&:has(.pf-c-radio__input:checked) {
|
||||
&:has(.pf-c-radio__input:checked:not(:disabled)) {
|
||||
--pf-c-radio--BackgroundColor: var(--pf-c-radio--checked--BackgroundColor);
|
||||
--pf-c-radio--BorderColor: var(--pf-c-radio--checked--BorderColor);
|
||||
--pf-c-radio--checkmark--Color: var(--pf-global--active-color--300);
|
||||
--pf-c-radio--checkmark--BorderColor: var(--pf-global--active-color--300);
|
||||
--pf-c-radio--checkmark--Color: var(--pf-c-radio--checked--BorderColor);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--pf-c-radio--BorderColor: var(--pf-c-radio--hover--BorderColor);
|
||||
&:hover:not(:has(.pf-c-radio__input:checked)):not(:has(.pf-c-radio__input:disabled)) {
|
||||
--pf-c-radio--checkmark--BorderColor: var(--pf-c-radio--hover--BorderColor);
|
||||
--pf-c-radio--checkmark--Color: var(--pf-c-radio--hover--BorderColor);
|
||||
}
|
||||
|
||||
@@ -250,6 +269,18 @@ label.pf-c-radio {
|
||||
|
||||
--pf-c-radio--BackgroundColor: var(--pf-c-radio--disabled--BackgroundColor) !important;
|
||||
}
|
||||
|
||||
.pf-c-radio__description {
|
||||
margin-block-start: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
||||
.pf-c-radio__label {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&:has(.pf-c-radio__input:last-child) .pf-c-radio__label {
|
||||
grid-row: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.pf-c-form__helper-radio:first-child {
|
||||
@@ -257,44 +288,27 @@ label.pf-c-radio {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
:has(ak-radio) {
|
||||
&::part(form-group) {
|
||||
grid-template-columns: [label] auto 1fr;
|
||||
}
|
||||
|
||||
&::part(form-group) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
&::part(group-control) {
|
||||
grid-column: label / -label;
|
||||
}
|
||||
}
|
||||
|
||||
::part(radio-help) {
|
||||
padding-block-start: var(--pf-global--spacer--form-element);
|
||||
padding-block-start: var(--pf-c-form--m-horizontal__group-label--md--PaddingTop);
|
||||
font-size: var(--pf-c-form__label--FontSize);
|
||||
line-height: var(--pf-global--LineHeight--md);
|
||||
font-style: italic;
|
||||
color: var(--pf-c-form__group-label-help--Color);
|
||||
text-align: end;
|
||||
line-height: var(--pf-c-form__label--LineHeight);
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
|
||||
ak-switch-input {
|
||||
padding-inline: var(--pf-global--spacer--form-element);
|
||||
|
||||
::part(form-group) {
|
||||
grid-template-columns: 1fr;
|
||||
::part(group-control) {
|
||||
grid-area: control;
|
||||
}
|
||||
|
||||
.pf-c-switch {
|
||||
grid-template-columns: [label] 1fr [control] 1fr;
|
||||
grid-template-columns: [label] auto [control] auto;
|
||||
grid-template-rows: [primary] auto [secondary] auto;
|
||||
justify-content: space-between;
|
||||
justify-content: start;
|
||||
padding-block: var(--pf-global--spacer--xs);
|
||||
width: 100%;
|
||||
column-gap: var(--pf-global--spacer--sm);
|
||||
}
|
||||
|
||||
.pf-c-switch__label {
|
||||
@@ -353,8 +367,6 @@ ak-switch-input {
|
||||
--pf-global--link--Color: var(--pf-global--link--Color--light) !important;
|
||||
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover) !important;
|
||||
|
||||
--pf-c-form-control--BackgroundColor: var(--ak-dark-background-light) !important;
|
||||
|
||||
--pf-c-form-control--readonly--BackgroundColor: var(--ak-dark-background-light) !important;
|
||||
--pf-c-form-control--disabled--BackgroundColor: var(--ak-dark-background-light) !important;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.pf-c-select {
|
||||
--pf-c-select__toggle--BackgroundColor: transparent;
|
||||
--pf-c-select__toggle--before--BorderTopColor: transparent;
|
||||
--pf-c-select__toggle--before--BorderRightColor: transparent;
|
||||
--pf-c-select__toggle--before--BorderLeftColor: transparent;
|
||||
|
||||
@@ -179,5 +179,123 @@ test.describe("Groups", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Edit group from view page", async ({ navigator, form, pointer, page }, testInfo) => {
|
||||
const groupName = groupNames.get(testInfo.testId)!;
|
||||
|
||||
const { fill, search } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
const newGroupDialog = page.getByRole("dialog", { name: "New Group" });
|
||||
const editGroupDialog = page.getByRole("dialog", { name: "Edit Group" });
|
||||
|
||||
await test.step("Create group", async () => {
|
||||
await click("New Group", "button");
|
||||
|
||||
await expect(newGroupDialog, "Dialog opens").toBeVisible();
|
||||
|
||||
await fill(/^Group Name/, groupName, newGroupDialog);
|
||||
|
||||
await newGroupDialog.getByRole("button", { name: "Create Group" }).click();
|
||||
|
||||
await expect(newGroupDialog, "Dialog closes after creating group").toBeHidden({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Navigate to group view page", async () => {
|
||||
const $group = await search(groupName);
|
||||
|
||||
await expect($group, "Group is visible").toBeVisible();
|
||||
|
||||
const viewLink = $group.getByRole("link", { name: "view details" });
|
||||
await expect(viewLink, "View details link is visible").toBeVisible();
|
||||
|
||||
await viewLink.click();
|
||||
});
|
||||
|
||||
const updatedName = `${groupName} Edited`;
|
||||
|
||||
await test.step("Edit group from view page", async () => {
|
||||
await expect(editGroupDialog, "Edit dialog is initially closed").toBeHidden();
|
||||
|
||||
await click("Edit", "button");
|
||||
|
||||
await expect(editGroupDialog, "Edit dialog opens").toBeVisible();
|
||||
|
||||
const nameInput = editGroupDialog.getByRole("textbox", { name: /Group Name/ });
|
||||
|
||||
await expect(nameInput, "Name input is visible").toBeVisible();
|
||||
await expect(nameInput, "Name is pre-filled").toHaveValue(groupName);
|
||||
|
||||
await nameInput.fill(updatedName);
|
||||
|
||||
await editGroupDialog.getByRole("button", { name: "Save Changes" }).click();
|
||||
|
||||
await expect(editGroupDialog, "Edit dialog closes after saving").toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Verify group name updated on view page", async () => {
|
||||
await expect(
|
||||
page.getByRole("heading", { name: updatedName }).first(),
|
||||
"Updated group name is visible on view page",
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test("Edit group from related group list", async ({
|
||||
navigator,
|
||||
form,
|
||||
pointer,
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const groupName = groupNames.get(testInfo.testId)!;
|
||||
|
||||
const { fill, search } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
const newGroupDialog = page.getByRole("dialog", { name: "New Group" });
|
||||
|
||||
await test.step("Create group with admin user", async () => {
|
||||
await click("New Group", "button");
|
||||
|
||||
await expect(newGroupDialog, "Dialog opens").toBeVisible();
|
||||
|
||||
await fill(/^Group Name/, groupName, newGroupDialog);
|
||||
|
||||
await newGroupDialog.getByRole("button", { name: "Create Group" }).click();
|
||||
|
||||
await expect(newGroupDialog, "Dialog closes").toBeHidden({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
await test.step("Navigate to admin user", async () => {
|
||||
await navigator.navigate("/if/admin/#/identity/users");
|
||||
|
||||
const $adminUser = await search(adminUsername);
|
||||
|
||||
await expect($adminUser, "Admin user is visible").toBeVisible();
|
||||
|
||||
const viewLink = $adminUser.getByRole("link", {
|
||||
name: "View details for authentik Default Admin",
|
||||
});
|
||||
await expect(viewLink, "View details link is visible").toBeVisible();
|
||||
|
||||
await viewLink.click();
|
||||
});
|
||||
|
||||
await test.step("Add user to group via related group list", async () => {
|
||||
await click("Groups", "tab");
|
||||
|
||||
const groupsPanel = page.getByRole("tabpanel", { name: "Groups" });
|
||||
|
||||
const addGroupDialog = page.getByRole("dialog", { name: "Add Group" });
|
||||
|
||||
await expect(addGroupDialog, "Add dialog is initially closed").toBeHidden();
|
||||
|
||||
await groupsPanel.getByRole("button", { name: "Add to existing group" }).click();
|
||||
|
||||
await expect(addGroupDialog, "Add dialog opens").toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
});
|
||||
|
||||
@@ -106,5 +106,67 @@ test.describe("Roles", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Edit role from view page", async ({ navigator, form, pointer, page }, testInfo) => {
|
||||
const roleName = roleNames.get(testInfo.testId)!;
|
||||
|
||||
const { fill, search } = form;
|
||||
const { click } = pointer;
|
||||
|
||||
const newRoleDialog = page.getByRole("dialog", { name: "New Role" });
|
||||
const editRoleDialog = page.getByRole("dialog", { name: "Edit Role" });
|
||||
|
||||
await test.step("Create role", async () => {
|
||||
await click("New Role", "button");
|
||||
|
||||
await expect(newRoleDialog, "Dialog opens").toBeVisible();
|
||||
|
||||
await fill(/^Role Name/, roleName, newRoleDialog);
|
||||
|
||||
await newRoleDialog.getByRole("button", { name: "Create Role" }).click();
|
||||
|
||||
await expect(newRoleDialog, "Dialog closes after creating role").toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Navigate to role view page", async () => {
|
||||
const $role = await search(roleName);
|
||||
|
||||
await expect($role, "Role is visible").toBeVisible();
|
||||
|
||||
const viewLink = $role.getByRole("link", { name: "view details" });
|
||||
await expect(viewLink, "View details link is visible").toBeVisible();
|
||||
|
||||
const viewURL = await viewLink.evaluate((el: HTMLAnchorElement) => el.href);
|
||||
await navigator.navigate(viewURL);
|
||||
});
|
||||
|
||||
const updatedName = `${roleName} View Edited`;
|
||||
|
||||
await test.step("Edit role from view page", async () => {
|
||||
await expect(editRoleDialog, "Edit dialog is initially closed").toBeHidden();
|
||||
|
||||
await click("Edit", "button");
|
||||
|
||||
await expect(editRoleDialog, "Edit dialog opens").toBeVisible();
|
||||
|
||||
const nameInput = editRoleDialog.getByRole("textbox", { name: /Role Name/ });
|
||||
|
||||
await expect(nameInput, "Name input is visible").toBeVisible();
|
||||
await expect(nameInput, "Name is pre-filled").toHaveValue(roleName);
|
||||
|
||||
await nameInput.fill(updatedName);
|
||||
|
||||
await editRoleDialog.getByRole("button", { name: "Save Changes" }).click();
|
||||
|
||||
await expect(editRoleDialog, "Edit dialog closes after saving").toBeHidden();
|
||||
});
|
||||
|
||||
await test.step("Verify role name updated on view page", async () => {
|
||||
await expect(
|
||||
page.getByText(updatedName),
|
||||
"Updated role name is visible on view page",
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
//#endregion
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user