Added the signing-in page (#4255)

This commit is contained in:
Erik Jan de Wit 2023-02-07 12:29:52 +01:00 committed by GitHub
parent a83300e514
commit 038122ca12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 295 additions and 2 deletions

View file

@ -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",

View file

@ -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;

View file

@ -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",
});
}

View 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);
},
};
}

View file

@ -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";