Sessions(actions dropdown): Adds actions dropdown - "revocation" and "log out all sessions" (#896)
* rebase and resolve conflicts/comments * remove badgeid
This commit is contained in:
parent
b5e2fb802c
commit
412e4c1f6b
9 changed files with 470 additions and 29 deletions
|
@ -42,5 +42,27 @@ describe("Sessions test", function () {
|
|||
it("Select 'Service account' dropdown option", () => {
|
||||
sessionsPage.selectServiceAccount();
|
||||
});
|
||||
|
||||
it("Set revocation notBefore", () => {
|
||||
sessionsPage.setToNow();
|
||||
});
|
||||
|
||||
it("Check if notBefore saved", () => {
|
||||
sessionsPage.checkNotBeforeValueExists();
|
||||
});
|
||||
|
||||
it("Clear revocation notBefore", () => {
|
||||
sessionsPage.clearNotBefore();
|
||||
});
|
||||
|
||||
it("Check if notBefore cleared", () => {
|
||||
sessionsPage.checkNotBeforeCleared();
|
||||
});
|
||||
|
||||
it("logout all sessions", () => {
|
||||
sessionsPage.logoutAllSessions();
|
||||
|
||||
cy.get("#kc-page-title").contains("Sign in to your account");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
export default class SessionsPage {
|
||||
sessionTypeDrpDwn: string;
|
||||
sessionTypeList: string;
|
||||
allSessionTypesOption: string;
|
||||
regularSSOOption: string;
|
||||
offlineOption: string;
|
||||
directGrantOption: string;
|
||||
serviceAccountOption: string;
|
||||
selectedType: string;
|
||||
|
||||
constructor() {
|
||||
this.sessionTypeDrpDwn = ".pf-c-select__toggle";
|
||||
this.sessionTypeList = ".pf-c-select__toggle + ul";
|
||||
this.allSessionTypesOption = "all-sessions-option";
|
||||
this.regularSSOOption = "regular-sso-option";
|
||||
this.offlineOption = "offline-option";
|
||||
this.directGrantOption = "direct-grant-option";
|
||||
this.serviceAccountOption = "service-account-option";
|
||||
this.selectedType = ".pf-c-select__toggle-text";
|
||||
}
|
||||
sessionTypeDrpDwn = ".pf-c-select__toggle";
|
||||
sessionTypeList = ".pf-c-select__toggle + ul";
|
||||
allSessionTypesOption = "all-sessions-option";
|
||||
regularSSOOption = "regular-sso-option";
|
||||
offlineOption = "offline-option";
|
||||
directGrantOption = "direct-grant-option";
|
||||
serviceAccountOption = "service-account-option";
|
||||
selectedType = ".pf-c-select__toggle-text";
|
||||
revocationActionItem = "revocation";
|
||||
setToNowButton = "set-to-now-button";
|
||||
actionDropdown = "action-dropdown";
|
||||
clearNotBeforeButton = "clear-not-before-button";
|
||||
notBeforeInput = "not-before-input";
|
||||
logoutAll = "logout-all";
|
||||
logoutAllConfirm = "logout-all-confirm-button";
|
||||
|
||||
shouldDisplay() {
|
||||
cy.get(this.sessionTypeDrpDwn).should("exist");
|
||||
|
@ -33,30 +29,60 @@ export default class SessionsPage {
|
|||
selectAllSessionsType() {
|
||||
cy.get(this.sessionTypeDrpDwn).click();
|
||||
cy.getId(this.allSessionTypesOption).click();
|
||||
cy.get(this.selectedType).should('have.text', 'All session types');
|
||||
cy.get(this.selectedType).should("have.text", "All session types");
|
||||
}
|
||||
|
||||
selectRegularSSO() {
|
||||
cy.get(this.sessionTypeDrpDwn).click();
|
||||
cy.getId(this.regularSSOOption).click();
|
||||
cy.get(this.selectedType).should('have.text', 'Regular SSO');
|
||||
cy.get(this.selectedType).should("have.text", "Regular SSO");
|
||||
}
|
||||
|
||||
selectOffline() {
|
||||
cy.get(this.sessionTypeDrpDwn).click();
|
||||
cy.getId(this.offlineOption).click();
|
||||
cy.get(this.selectedType).should('have.text', 'Offline');
|
||||
cy.get(this.selectedType).should("have.text", "Offline");
|
||||
}
|
||||
|
||||
selectDirectGrant() {
|
||||
cy.get(this.sessionTypeDrpDwn).click();
|
||||
cy.getId(this.directGrantOption).click();
|
||||
cy.get(this.selectedType).should('have.text', 'Direct grant');
|
||||
cy.get(this.selectedType).should("have.text", "Direct grant");
|
||||
}
|
||||
|
||||
selectServiceAccount() {
|
||||
cy.get(this.sessionTypeDrpDwn).click();
|
||||
cy.getId(this.serviceAccountOption).click();
|
||||
cy.get(this.selectedType).should('have.text', 'Service account');
|
||||
cy.get(this.selectedType).should("have.text", "Service account");
|
||||
}
|
||||
|
||||
setToNow() {
|
||||
cy.getId(this.actionDropdown).click();
|
||||
cy.getId(this.revocationActionItem).click();
|
||||
cy.getId(this.setToNowButton).click();
|
||||
}
|
||||
|
||||
checkNotBeforeValueExists() {
|
||||
cy.getId(this.actionDropdown).click();
|
||||
cy.getId(this.revocationActionItem).click();
|
||||
cy.getId(this.notBeforeInput).should("not.have.value", "None");
|
||||
}
|
||||
|
||||
clearNotBefore() {
|
||||
cy.getId(this.actionDropdown).click();
|
||||
cy.getId(this.revocationActionItem).click();
|
||||
cy.getId(this.clearNotBeforeButton).click();
|
||||
}
|
||||
|
||||
checkNotBeforeCleared() {
|
||||
cy.getId(this.actionDropdown).click();
|
||||
cy.getId(this.revocationActionItem).click();
|
||||
cy.getId(this.notBeforeInput).should("have.value", "None");
|
||||
}
|
||||
|
||||
logoutAllSessions() {
|
||||
cy.getId(this.actionDropdown).click();
|
||||
cy.getId(this.logoutAll).click();
|
||||
cy.getId(this.logoutAllConfirm).click();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import { HelpItem } from "../help-enabler/HelpItem";
|
|||
export type ViewHeaderProps = {
|
||||
titleKey: string;
|
||||
badges?: ViewHeaderBadge[];
|
||||
isDropdownDisabled?: boolean;
|
||||
subKey?: string | ReactNode;
|
||||
actionsDropdownId?: string;
|
||||
subKeyLinkProps?: FormattedLinkProps;
|
||||
|
@ -54,6 +55,7 @@ export const ViewHeader = ({
|
|||
actionsDropdownId,
|
||||
titleKey,
|
||||
badges,
|
||||
isDropdownDisabled,
|
||||
subKey,
|
||||
subKeyLinkProps,
|
||||
dropdownItems,
|
||||
|
@ -138,6 +140,7 @@ export const ViewHeader = ({
|
|||
position={DropdownPosition.right}
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
isDisabled={isDropdownDisabled}
|
||||
id={actionsDropdownId}
|
||||
onToggle={onDropdownToggle}
|
||||
>
|
||||
|
|
|
@ -21,6 +21,10 @@ export class WhoAmI {
|
|||
return this.me.displayName;
|
||||
}
|
||||
|
||||
public getRealm() {
|
||||
return this.me?.realm ?? "";
|
||||
}
|
||||
|
||||
public getUserId(): string {
|
||||
if (this.me === undefined) return "";
|
||||
|
||||
|
|
73
src/sessions/LogoutAllSessionsModal.tsx
Normal file
73
src/sessions/LogoutAllSessionsModal.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
TextContent,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
type RevocationModalProps = {
|
||||
handleModalToggle: () => void;
|
||||
};
|
||||
|
||||
export const LogoutAllSessionsModal = ({
|
||||
handleModalToggle,
|
||||
}: RevocationModalProps) => {
|
||||
const { t } = useTranslation("sessions");
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const { realm: realmName } = useRealm();
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
const logoutAllSessions = async () => {
|
||||
try {
|
||||
await adminClient.realms.logoutAll({ realm: realmName });
|
||||
adminClient.keycloak.logout({ redirectUri: "" });
|
||||
} catch (error) {
|
||||
addAlert(t("logoutAllSessionsError", { error }), AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("signOutAllActiveSessionsQuestion")}
|
||||
isOpen={true}
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="logout-all-confirm-button"
|
||||
key="set-to-now"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
logoutAllSessions();
|
||||
handleModalToggle();
|
||||
}}
|
||||
form="revocation-modal-form"
|
||||
>
|
||||
{t("realm-settings:confirm")}
|
||||
</Button>,
|
||||
<Button
|
||||
id="modal-cancel"
|
||||
key="cancel"
|
||||
variant={ButtonVariant.link}
|
||||
onClick={() => {
|
||||
handleModalToggle();
|
||||
}}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<TextContent className="kc-logout-all-description-text">
|
||||
{t("logoutAllDescription")}
|
||||
</TextContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
220
src/sessions/RevocationModal.tsx
Normal file
220
src/sessions/RevocationModal.tsx
Normal file
|
@ -0,0 +1,220 @@
|
|||
import React, { useState } from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Form,
|
||||
FormGroup,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
TextContent,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { emailRegexPattern } from "../util";
|
||||
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
|
||||
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import type GlobalRequestResult from "keycloak-admin/lib/defs/globalRequestResult";
|
||||
|
||||
type RevocationModalProps = {
|
||||
handleModalToggle: () => void;
|
||||
activeClients: ClientRepresentation[];
|
||||
save: () => void;
|
||||
};
|
||||
|
||||
export const RevocationModal = ({
|
||||
handleModalToggle,
|
||||
save,
|
||||
}: RevocationModalProps) => {
|
||||
const { t } = useTranslation("sessions");
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const { realm: realmName } = useRealm();
|
||||
const adminClient = useAdminClient();
|
||||
const { register, errors, handleSubmit } = useForm();
|
||||
const [realm, setRealm] = useState<RealmRepresentation>();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
|
||||
const refresh = () => {
|
||||
setKey(new Date().getTime());
|
||||
};
|
||||
|
||||
useFetch(
|
||||
() => adminClient.realms.findOne({ realm: realmName }),
|
||||
(realm) => {
|
||||
setRealm(realm);
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
const parseResult = (result: GlobalRequestResult, prefixKey: string) => {
|
||||
const successCount = result.successRequests?.length || 0;
|
||||
const failedCount = result.failedRequests?.length || 0;
|
||||
|
||||
if (successCount === 0 && failedCount === 0) {
|
||||
addAlert(t("clients:noAdminUrlSet"), AlertVariant.warning);
|
||||
} else if (failedCount > 0) {
|
||||
addAlert(
|
||||
t("clients:" + prefixKey + "Success", {
|
||||
successNodes: result.successRequests,
|
||||
}),
|
||||
AlertVariant.success
|
||||
);
|
||||
addAlert(
|
||||
t("clients:" + prefixKey + "Fail", {
|
||||
failedNodes: result.failedRequests,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
} else {
|
||||
addAlert(
|
||||
t("clients:" + prefixKey + "Success", {
|
||||
successNodes: result.successRequests,
|
||||
}),
|
||||
AlertVariant.success
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setToNow = async () => {
|
||||
try {
|
||||
await adminClient.realms.update(
|
||||
{ realm: realmName },
|
||||
{
|
||||
realm: realmName,
|
||||
notBefore: Date.now() / 1000,
|
||||
}
|
||||
);
|
||||
|
||||
addAlert(t("notBeforeSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(t("setToNowError", { error }), AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
const clearNotBefore = async () => {
|
||||
try {
|
||||
await adminClient.realms.update(
|
||||
{ realm: realmName },
|
||||
{
|
||||
realm: realmName,
|
||||
notBefore: 0,
|
||||
}
|
||||
);
|
||||
addAlert(t("notBeforeClearedSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(t("notBeforeError", { error }), AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
const push = async () => {
|
||||
const result = adminClient.realms.pushRevocation({
|
||||
realm: realmName,
|
||||
}) as unknown as GlobalRequestResult;
|
||||
parseResult(result, "notBeforePush");
|
||||
|
||||
refresh();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("revocation")}
|
||||
isOpen={true}
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="set-to-now-button"
|
||||
key="set-to-now"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setToNow();
|
||||
handleModalToggle();
|
||||
}}
|
||||
form="revocation-modal-form"
|
||||
>
|
||||
{t("setToNow")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="clear-not-before-button"
|
||||
key="clear"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
clearNotBefore();
|
||||
handleModalToggle();
|
||||
}}
|
||||
form="revocation-modal-form"
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="modal-test-connection-button"
|
||||
key="push"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
push();
|
||||
handleModalToggle();
|
||||
}}
|
||||
form="revocation-modal-form"
|
||||
>
|
||||
{t("push")}
|
||||
</Button>,
|
||||
<Button
|
||||
id="modal-cancel"
|
||||
key="cancel"
|
||||
variant={ButtonVariant.link}
|
||||
onClick={() => {
|
||||
handleModalToggle();
|
||||
}}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<TextContent className="kc-revocation-description-text">
|
||||
{t("revocationDescription")}
|
||||
</TextContent>
|
||||
<Form
|
||||
id="revocation-modal-form"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(save)}
|
||||
>
|
||||
<FormGroup
|
||||
className="kc-revocation-modal-form-group"
|
||||
label={t("notBefore")}
|
||||
name="notBefore"
|
||||
fieldId="not-before"
|
||||
validated={
|
||||
errors.email ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
data-testid="not-before-input"
|
||||
ref={register({ required: true, pattern: emailRegexPattern })}
|
||||
autoFocus
|
||||
isReadOnly
|
||||
value={
|
||||
realm?.notBefore === 0
|
||||
? (t("none") as string)
|
||||
: new Date(realm?.notBefore! * 1000).toString()
|
||||
}
|
||||
type="text"
|
||||
id="not-before"
|
||||
name="notBefore"
|
||||
validated={
|
||||
errors.email ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -2,3 +2,7 @@
|
|||
margin-right: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.pf-c-form__group.kc-revocation-modal-form-group {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import moment from "moment";
|
||||
import {
|
||||
DropdownItem,
|
||||
PageSection,
|
||||
Select,
|
||||
SelectOption,
|
||||
|
@ -16,7 +17,11 @@ import { ViewHeader } from "../components/view-header/ViewHeader";
|
|||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
|
||||
import { CubesIcon } from "@patternfly/react-icons";
|
||||
import "./SessionsSection.css";
|
||||
import { RevocationModal } from "./RevocationModal";
|
||||
import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { LogoutAllSessionsModal } from "./LogoutAllSessionsModal";
|
||||
|
||||
const Clients = (row: UserSessionRepresentation) => {
|
||||
return (
|
||||
|
@ -34,15 +39,30 @@ export const SessionsSection = () => {
|
|||
const { t } = useTranslation("sessions");
|
||||
const adminClient = useAdminClient();
|
||||
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
|
||||
const [revocationModalOpen, setRevocationModalOpen] = useState(false);
|
||||
const [logoutAllSessionsModalOpen, setLogoutAllSessionsModalOpen] =
|
||||
useState(false);
|
||||
const [activeClientDetails, setActiveClientDetails] = useState<
|
||||
ClientRepresentation[]
|
||||
>([]);
|
||||
const [filterType, setFilterType] = useState(
|
||||
t("sessionsType.allSessions").toString()
|
||||
);
|
||||
const [key, setKey] = useState(0);
|
||||
const [noSessions, setNoSessions] = useState(false);
|
||||
|
||||
const refresh = () => {
|
||||
setKey(new Date().getTime());
|
||||
};
|
||||
|
||||
const handleRevocationModalToggle = () => {
|
||||
setRevocationModalOpen(!revocationModalOpen);
|
||||
};
|
||||
|
||||
const handleLogoutAllSessionsModalToggle = () => {
|
||||
setLogoutAllSessionsModalOpen(!logoutAllSessionsModalOpen);
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const activeClients = await adminClient.sessions.find();
|
||||
const clientSessions = (
|
||||
|
@ -53,6 +73,16 @@ export const SessionsSection = () => {
|
|||
)
|
||||
).flat();
|
||||
|
||||
setNoSessions(clientSessions.length === 0);
|
||||
|
||||
const allClients = await adminClient.clients.find();
|
||||
|
||||
const getActiveClientDetails = allClients.filter((x) =>
|
||||
activeClients.map((y) => y.id).includes(x.id)
|
||||
);
|
||||
|
||||
setActiveClientDetails(getActiveClientDetails);
|
||||
|
||||
const userIds = Array.from(
|
||||
new Set(clientSessions.map((session) => session.userId))
|
||||
);
|
||||
|
@ -65,10 +95,48 @@ export const SessionsSection = () => {
|
|||
return userSessions;
|
||||
};
|
||||
|
||||
const dropdownItems = [
|
||||
<DropdownItem
|
||||
key="toggle-modal"
|
||||
data-testid="revocation"
|
||||
component="button"
|
||||
onClick={() => handleRevocationModalToggle()}
|
||||
>
|
||||
{t("revocation")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="delete-role"
|
||||
data-testid="logout-all"
|
||||
component="button"
|
||||
isDisabled={noSessions}
|
||||
onClick={() => handleLogoutAllSessionsModalToggle()}
|
||||
>
|
||||
{t("signOutAllActiveSessions")}
|
||||
</DropdownItem>,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey="sessions:title" subKey="sessions:sessionExplain" />
|
||||
<ViewHeader
|
||||
dropdownItems={dropdownItems}
|
||||
titleKey="sessions:title"
|
||||
subKey="sessions:sessionExplain"
|
||||
/>
|
||||
<PageSection variant="light" className="pf-u-p-0">
|
||||
{revocationModalOpen && (
|
||||
<RevocationModal
|
||||
handleModalToggle={handleRevocationModalToggle}
|
||||
activeClients={activeClientDetails}
|
||||
save={() => {
|
||||
handleRevocationModalToggle();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{logoutAllSessionsModalOpen && (
|
||||
<LogoutAllSessionsModal
|
||||
handleModalToggle={handleLogoutAllSessionsModalToggle}
|
||||
/>
|
||||
)}
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
|
@ -135,8 +203,10 @@ export const SessionsSection = () => {
|
|||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("emptyTitle")}
|
||||
instructions={t("emptyInstructions")}
|
||||
hasIcon
|
||||
icon={CubesIcon}
|
||||
message={t("noSessions")}
|
||||
instructions={t("noSessionsDescription")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -14,7 +14,26 @@ export default {
|
|||
directGrant: "Direct grant",
|
||||
serviceAccount: "Service account",
|
||||
},
|
||||
emptyTitle: "No sessions",
|
||||
emptyInstructions: "There are no active sessions on this realm",
|
||||
revocation: "Revocation",
|
||||
revocationDescription:
|
||||
"This is a way to revoke all active sessions and access tokens. Not before means you can revoke any tokens issued before the date.",
|
||||
notBefore: "Not before",
|
||||
notBeforeSuccess: 'Success! "Not before" set for realm',
|
||||
notBeforeError: 'Error clearing "Not Before" for realm: {{error}}',
|
||||
notBeforeClearedSuccess: 'Success! "Not Before" cleared for realm.',
|
||||
signOutAllActiveSessions: "Sign out all active sessions",
|
||||
signOutAllActiveSessionsQuestion: "Sign out all active sessions?",
|
||||
setToNow: "Set to now",
|
||||
logoutAllDescription:
|
||||
"If you sign out all active sessions, active subjects in this realm will be signed out.",
|
||||
logoutAllSessionsError:
|
||||
"Error! Failed to log out of all sessions: {{error}}.",
|
||||
setToNowError: "Error! Failed to set notBefore to current date and time.",
|
||||
clear: "Clear",
|
||||
push: "Push",
|
||||
none: "None",
|
||||
noSessions: "No sessions",
|
||||
noSessionsDescription:
|
||||
"There are currently no active sessions in this realm.",
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue