Added the signing-in page (#4255)
This commit is contained in:
parent
a83300e514
commit
038122ca12
5 changed files with 295 additions and 2 deletions
|
@ -9,10 +9,12 @@
|
|||
"applicationsIntroMessage": "Track and manage your app permission to access your account",
|
||||
"applicationType": "Application type",
|
||||
"avatar": "Avatar",
|
||||
"basic-authentication": "Basic authentication",
|
||||
"cancel": "Cancel",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"close": "Close",
|
||||
"credentialCreatedAt": "<0>Created</0> {{date}}.",
|
||||
"currentSession": "Current session",
|
||||
"description": "Description",
|
||||
"device-activity": "Device activity",
|
||||
|
@ -40,7 +42,13 @@
|
|||
"myResources": "My Resources",
|
||||
"name": "Name",
|
||||
"notInUse": "Not in use",
|
||||
"notSetUp": "{{0}} is not set up.",
|
||||
"offlineAccess": "Offline access",
|
||||
"otp-display-name": "Authenticator application",
|
||||
"otp-help-text": "Enter a verification code from authenticator application.",
|
||||
"password-display-name": "Password",
|
||||
"password-help-text": "Sign in by entering your password.",
|
||||
"password": "My password",
|
||||
"permissionRequest": "Permission requests - {{0}}",
|
||||
"permissionRequests": "Permission requests",
|
||||
"permissions": "Permissions",
|
||||
|
@ -49,6 +57,8 @@
|
|||
"privacyPolicy": "Privacy policy",
|
||||
"refreshPage": "Refresh the page",
|
||||
"removeButton": "Remove access",
|
||||
"removeCred": "Remove {{0}}",
|
||||
"removeCredAriaLabel": "Remove credential",
|
||||
"removeModalMessage": "This will remove the currently granted access permission for {{0}}. You will need to grant access again if you want to use this app.",
|
||||
"removeModalTitle": "Remove access",
|
||||
"requestor": "Requestor",
|
||||
|
@ -60,6 +70,7 @@
|
|||
"resourceSharedWith_one": "Resource is shared with <0>{{username}}</0>",
|
||||
"resourceSharedWith_other": "Resource is shared with <0>{{username}}</0> and <1>{{other}}</1> other users",
|
||||
"resourceSharedWith_zero": "This resource is not shared.",
|
||||
"setUpNew": "Set up {{0}}",
|
||||
"share": "Share",
|
||||
"sharedWithMe": "Shared with Me",
|
||||
"shareTheResource": "Share the resource - {{0}}",
|
||||
|
@ -69,6 +80,7 @@
|
|||
"signedInDevicesExplanation": "Sign out of any unfamiliar devices.",
|
||||
"signedOutSession": "Signed out {{0}}/{{1}}",
|
||||
"signingIn": "Signing in",
|
||||
"signingInDescription": "Configure ways to sign in.",
|
||||
"signOut": "Sign out",
|
||||
"signOutAllDevices": "Sign out all devices",
|
||||
"signOutAllDevicesWarning": "This action will sign out all the devices that have signed in to your account, including the current device you are using.",
|
||||
|
@ -76,12 +88,16 @@
|
|||
"somethingWentWrongDescription": "Sorry, an unexpected error has occurred.",
|
||||
"started": "Started",
|
||||
"status": "Status",
|
||||
"stopUsingCred": "Stop using {{0}}?",
|
||||
"termsOfService": "Terms of service",
|
||||
"thirdPartyApp": "Third-party",
|
||||
"tryAgain": "Try again",
|
||||
"two-factor": "Two-factor authentication",
|
||||
"unknownOperatingSystem": "Unknown operating system",
|
||||
"unknownUser": "Anonymous",
|
||||
"unShare": "Unshare all",
|
||||
"update": "Update",
|
||||
"updateCredAriaLabel": "Update credential",
|
||||
"user": "User",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Username or email",
|
||||
|
|
|
@ -1,5 +1,236 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
import {
|
||||
Button,
|
||||
DataList,
|
||||
DataListAction,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
KebabToggle,
|
||||
PageSection,
|
||||
Spinner,
|
||||
Split,
|
||||
SplitItem,
|
||||
Title,
|
||||
} from "@patternfly/react-core";
|
||||
import { TFuncKey } from "i18next";
|
||||
import { useState, CSSProperties } from "react";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { deleteCredentials, getCredentials } from "../api/methods";
|
||||
import {
|
||||
CredentialContainer,
|
||||
CredentialMetadataRepresentation,
|
||||
CredentialRepresentation,
|
||||
} from "../api/representations";
|
||||
import { useAlerts } from "../components/alerts/Alerts";
|
||||
import { ContinueCancelModal } from "../components/continue-cancel/ContinueCancelModel";
|
||||
import useFormatter from "../components/format/format-date";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { keycloak } from "../keycloak";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
const SigningIn = () => <PageSection>This is the signing in page.</PageSection>;
|
||||
type MobileLinkProps = {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const MobileLink = ({ title, onClick }: MobileLinkProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
isPlain
|
||||
position="right"
|
||||
toggle={<KebabToggle onToggle={setOpen} />}
|
||||
className="pf-u-display-none-on-lg"
|
||||
isOpen={open}
|
||||
dropdownItems={[
|
||||
<DropdownItem key="1" onClick={onClick}>
|
||||
{title}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={onClick}
|
||||
className="pf-u-display-none pf-u-display-inline-flex-on-lg"
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SigningIn = () => {
|
||||
const { t } = useTranslation();
|
||||
const { formatDate } = useFormatter();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { login } = keycloak;
|
||||
|
||||
const [credentials, setCredentials] = useState<CredentialContainer[]>();
|
||||
const [key, setKey] = useState(1);
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
||||
usePromise((signal) => getCredentials({ signal }), setCredentials, [key]);
|
||||
|
||||
const credentialRowCells = (
|
||||
credMetadata: CredentialMetadataRepresentation
|
||||
) => {
|
||||
const credential = credMetadata.credential;
|
||||
const maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as CSSProperties;
|
||||
const items = [
|
||||
<DataListCell
|
||||
id={`cred-${credMetadata.credential.id}`}
|
||||
key="title"
|
||||
className="pf-u-max-width"
|
||||
style={maxWidth}
|
||||
>
|
||||
{credential.userLabel || t(credential.type as TFuncKey)}
|
||||
</DataListCell>,
|
||||
];
|
||||
|
||||
if (credential.createdDate) {
|
||||
items.push(
|
||||
<DataListCell key={"created" + credential.id}>
|
||||
<Trans i18nKey="credentialCreatedAt">
|
||||
<strong className="pf-u-mr-md"></strong>
|
||||
{{ date: formatDate(new Date(credential.createdDate)) }}
|
||||
</Trans>
|
||||
</DataListCell>
|
||||
);
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
const label = (credential: CredentialRepresentation) =>
|
||||
credential.userLabel || t(credential.type as TFuncKey);
|
||||
|
||||
if (!credentials) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page title={t("signingIn")} description={t("signingInDescription")}>
|
||||
<DataList aria-label="user credential" className="pf-u-mb-xl">
|
||||
{credentials.map((container) => (
|
||||
<PageSection
|
||||
key={container.category}
|
||||
variant="light"
|
||||
className="pf-u-px-0"
|
||||
>
|
||||
<Title headingLevel="h2" size="xl">
|
||||
{t(container.category as TFuncKey)}
|
||||
</Title>
|
||||
<Split className="pf-u-mt-lg pf-u-mb-lg">
|
||||
<SplitItem>
|
||||
<Title headingLevel="h3" size="md" className="pf-u-mb-md">
|
||||
<span className="cred-title pf-u-display-block">
|
||||
{t(container.displayName as TFuncKey)}
|
||||
</span>
|
||||
</Title>
|
||||
{t(container.helptext as TFuncKey)}
|
||||
</SplitItem>
|
||||
{container.createAction && (
|
||||
<SplitItem isFilled>
|
||||
<div className="pf-u-float-right">
|
||||
<MobileLink
|
||||
onClick={() =>
|
||||
login({
|
||||
action: container.createAction,
|
||||
})
|
||||
}
|
||||
title={t("setUpNew", [
|
||||
t(container.displayName as TFuncKey),
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
</SplitItem>
|
||||
)}
|
||||
</Split>
|
||||
|
||||
<DataList aria-label="credential list" className="pf-u-mb-xl">
|
||||
{container.userCredentialMetadatas.length === 0 && (
|
||||
<DataListItem>
|
||||
<DataListItemRow className="pf-u-align-items-center pf-p-b-0">
|
||||
<DataListItemCells
|
||||
className="pf-u-py-0"
|
||||
dataListCells={[
|
||||
<DataListCell key="0" />,
|
||||
<EmptyState key="1" variant="xs">
|
||||
<EmptyStateBody>
|
||||
{t("notSetUp", [
|
||||
t(container.displayName as TFuncKey),
|
||||
])}
|
||||
</EmptyStateBody>
|
||||
,
|
||||
</EmptyState>,
|
||||
<DataListCell key="2" />,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
)}
|
||||
|
||||
{container.userCredentialMetadatas.map((meta) => (
|
||||
<DataListItem key={meta.credential.id}>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
className="pf-u-py-0"
|
||||
dataListCells={[
|
||||
...credentialRowCells(meta),
|
||||
<DataListAction
|
||||
key="action"
|
||||
id={`action-${meta.credential.id}`}
|
||||
aria-label={t("updateCredAriaLabel")}
|
||||
aria-labelledby={`cred-${meta.credential.id}`}
|
||||
>
|
||||
{container.removeable ? (
|
||||
<ContinueCancelModal
|
||||
buttonTitle="remove"
|
||||
buttonVariant="danger"
|
||||
modalTitle={t("removeCred", [
|
||||
label(meta.credential),
|
||||
])}
|
||||
modalMessage={t("stopUsingCred", [
|
||||
label(meta.credential),
|
||||
])}
|
||||
onContinue={async () => {
|
||||
try {
|
||||
await deleteCredentials(meta.credential);
|
||||
addAlert("successRemovedMessage");
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("errorRemovedMessage", error);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
if (container.updateAction)
|
||||
login({ action: container.updateAction });
|
||||
}}
|
||||
>
|
||||
{t("update")}
|
||||
</Button>
|
||||
)}
|
||||
</DataListAction>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
</PageSection>
|
||||
))}
|
||||
</DataList>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SigningIn;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { parseResponse } from "./parse-response";
|
||||
import {
|
||||
ClientRepresentation,
|
||||
CredentialContainer,
|
||||
CredentialRepresentation,
|
||||
DeviceRepresentation,
|
||||
Permission,
|
||||
UserRepresentation,
|
||||
|
@ -58,3 +60,16 @@ export async function deleteSession(id?: string) {
|
|||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCredentials({ signal }: CallOptions) {
|
||||
const response = await request("/credentials", {
|
||||
signal,
|
||||
});
|
||||
return parseResponse<CredentialContainer[]>(response);
|
||||
}
|
||||
|
||||
export async function deleteCredentials(credential: CredentialRepresentation) {
|
||||
return request("/credentials/" + credential.id, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
|
30
apps/account-ui/src/components/format/format-date.ts
Normal file
30
apps/account-ui/src/components/format/format-date.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
const DATE_AND_TIME_FORMAT: Intl.DateTimeFormatOptions = {
|
||||
dateStyle: "long",
|
||||
timeStyle: "short",
|
||||
};
|
||||
|
||||
const TIME_FORMAT: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
};
|
||||
|
||||
//todo use user local
|
||||
export default function useFormatter() {
|
||||
return {
|
||||
formatDate: function (
|
||||
date: Date,
|
||||
options: Intl.DateTimeFormatOptions | undefined = DATE_AND_TIME_FORMAT
|
||||
) {
|
||||
return date.toLocaleString("en", options);
|
||||
},
|
||||
formatTime: function (
|
||||
time: number,
|
||||
options: Intl.DateTimeFormatOptions | undefined = TIME_FORMAT
|
||||
) {
|
||||
return new Intl.DateTimeFormat("en", options).format(time);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import "@patternfly/react-core/dist/styles/base.css";
|
||||
import "@patternfly/patternfly/patternfly-addons.css";
|
||||
|
||||
import { StrictMode } from "react";
|
||||
import { render } from "react-dom";
|
||||
|
|
Loading…
Reference in a new issue