Refactored the way we show time using Intl (#2178)

This commit is contained in:
Erik Jan de Wit 2022-03-07 14:49:38 +01:00 committed by GitHub
parent 1b6d679d89
commit b7ea8629a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 107 additions and 136 deletions

View file

@ -255,7 +255,7 @@ export const OtpPolicy = ({ realm, realmUpdated }: OtpPolicyProps) => {
aria-label={t("otpPolicyPeriod")} aria-label={t("otpPolicyPeriod")}
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["seconds", "minutes"]} units={["second", "minute"]}
validated={errors.otpPolicyPeriod ? "error" : "default"} validated={errors.otpPolicyPeriod ? "error" : "default"}
/> />
)} )}

View file

@ -307,7 +307,7 @@ export const WebauthnPolicy = ({
aria-label={t("webAuthnPolicyCreateTimeout")} aria-label={t("webAuthnPolicyCreateTimeout")}
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["seconds", "minutes", "hours"]} units={["second", "minute", "hour"]}
validated={ validated={
errors.webAuthnPolicyCreateTimeout ? "error" : "default" errors.webAuthnPolicyCreateTimeout ? "error" : "default"
} }

View file

@ -50,7 +50,7 @@ export const AdvancedSettings = ({
control={control} control={control}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<TimeSelector <TimeSelector
units={["minutes", "days", "hours"]} units={["minute", "day", "hour"]}
value={value} value={value}
onChange={onChange} onChange={onChange}
/> />
@ -64,7 +64,7 @@ export const AdvancedSettings = ({
id="accessTokenLifespan" id="accessTokenLifespan"
name="attributes.access.token.lifespan" name="attributes.access.token.lifespan"
defaultValue="" defaultValue=""
units={["minutes", "days", "hours"]} units={["minute", "day", "hour"]}
control={control} control={control}
/> />

View file

@ -86,7 +86,6 @@ export default function CreateInitialAccessToken() {
data-testid="expiration" data-testid="expiration"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["days", "hours", "minutes", "seconds"]}
/> />
)} )}
/> />

View file

@ -1,3 +1,5 @@
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { import {
Select, Select,
SelectOption, SelectOption,
@ -8,10 +10,17 @@ import {
TextInputProps, TextInputProps,
ToggleMenuBaseProps, ToggleMenuBaseProps,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
export type Unit = "seconds" | "minutes" | "hours" | "days"; export type Unit = "second" | "minute" | "hour" | "day";
type TimeUnit = { unit: Unit; label: string; multiplier: number };
const allTimes: TimeUnit[] = [
{ unit: "second", label: "times.seconds", multiplier: 1 },
{ unit: "minute", label: "times.minutes", multiplier: 60 },
{ unit: "hour", label: "times.hours", multiplier: 3600 },
{ unit: "day", label: "times.days", multiplier: 86400 },
];
export type TimeSelectorProps = TextInputProps & export type TimeSelectorProps = TextInputProps &
ToggleMenuBaseProps & { ToggleMenuBaseProps & {
@ -21,9 +30,28 @@ export type TimeSelectorProps = TextInputProps &
className?: string; className?: string;
}; };
export const getTimeUnit = (value: number) =>
allTimes.reduce(
(v, time) =>
value % time.multiplier === 0 && v.multiplier < time.multiplier
? time
: v,
allTimes[0]
);
export const toHumanFormat = (value: number, locale: string) => {
const timeUnit = getTimeUnit(value);
const formatter = new Intl.NumberFormat(locale, {
style: "unit",
unit: timeUnit.unit,
unitDisplay: "long",
});
return formatter.format(value / timeUnit.multiplier);
};
export const TimeSelector = ({ export const TimeSelector = ({
value, value,
units = ["seconds", "minutes", "hours", "days"], units = ["second", "minute", "hour", "day"],
onChange, onChange,
className, className,
min, min,
@ -32,36 +60,26 @@ export const TimeSelector = ({
}: TimeSelectorProps) => { }: TimeSelectorProps) => {
const { t } = useTranslation("common"); const { t } = useTranslation("common");
const allTimes: { unit: Unit; label: string; multiplier: number }[] = [ const times = useMemo(
{ unit: "seconds", label: t("times.seconds"), multiplier: 1 }, () => units.map((unit) => allTimes.find((time) => time.unit === unit)!),
{ unit: "minutes", label: t("times.minutes"), multiplier: 60 }, [units]
{ unit: "hours", label: t("times.hours"), multiplier: 3600 }, );
{ unit: "days", label: t("times.days"), multiplier: 86400 },
]; const defaultMultiplier = useMemo(
() => allTimes.find((time) => time.unit === units[0])?.multiplier,
const times = units.map( [units]
(unit) => allTimes.find((time) => time.unit === unit)!
); );
const defaultMultiplier = allTimes.find(
(time) => time.unit === units[0]
)?.multiplier;
const [timeValue, setTimeValue] = useState<"" | number>(""); const [timeValue, setTimeValue] = useState<"" | number>("");
const [multiplier, setMultiplier] = useState(defaultMultiplier); const [multiplier, setMultiplier] = useState(defaultMultiplier);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
const x = times.reduce( const multiplier = getTimeUnit(value).multiplier;
(v, time) =>
value % time.multiplier === 0 && v < time.multiplier
? time.multiplier
: v,
1
);
if (value) { if (value) {
setMultiplier(x); setMultiplier(multiplier);
setTimeValue(value / x); setTimeValue(value / multiplier);
} else { } else {
setTimeValue(value); setTimeValue(value);
setMultiplier(defaultMultiplier); setMultiplier(defaultMultiplier);
@ -118,7 +136,7 @@ export const TimeSelector = ({
key={time.label} key={time.label}
value={time.multiplier} value={time.multiplier}
> >
{time.label} {t(time.label)}
</SelectOption> </SelectOption>
))} ))}
</Select> </Select>

View file

@ -0,0 +1,23 @@
import { getTimeUnit, toHumanFormat } from "./TimeSelector";
describe("Time conversion functions", () => {
it("should convert milliseconds to unit", () => {
const givenTime = 86400;
//when
const timeUnit = getTimeUnit(givenTime);
//then
expect(timeUnit.unit).toEqual("day");
});
it("should convert to human format", () => {
const givenTime = 86400 * 2;
//when
const timeString = toHumanFormat(givenTime, "en");
//then
expect(timeString).toEqual("2 days");
});
});

View file

@ -78,7 +78,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="sso-session-idle-input" aria-label="sso-session-idle-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -105,7 +105,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="sso-session-max-input" aria-label="sso-session-max-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -132,7 +132,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="sso-session-idle-remember-me-input" aria-label="sso-session-idle-remember-me-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -159,7 +159,7 @@ export const RealmSettingsSessionsTab = ({
data-testid="sso-session-max-remember-me-input" data-testid="sso-session-max-remember-me-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -197,7 +197,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="client-session-idle-input" aria-label="client-session-idle-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -224,7 +224,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="client-session-max-input" aria-label="client-session-max-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -262,7 +262,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="offline-session-idle-input" aria-label="offline-session-idle-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -318,7 +318,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="offline-session-max-input" aria-label="offline-session-max-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -358,7 +358,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="login-timeout-input" aria-label="login-timeout-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -385,7 +385,7 @@ export const RealmSettingsSessionsTab = ({
aria-label="login-action-timeout-input" aria-label="login-action-timeout-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />

View file

@ -19,11 +19,14 @@ import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/r
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import { FormPanel } from "../components/scroll-form/FormPanel"; import { FormPanel } from "../components/scroll-form/FormPanel";
import { TimeSelector } from "../components/time-selector/TimeSelector"; import {
TimeSelector,
toHumanFormat,
} from "../components/time-selector/TimeSelector";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { forHumans, interpolateTimespan } from "../util";
import "./realm-settings-section.css"; import "./realm-settings-section.css";
import { useWhoAmI } from "../context/whoami/WhoAmI";
type RealmSettingsSessionsTabProps = { type RealmSettingsSessionsTabProps = {
realm: RealmRepresentation; realm: RealmRepresentation;
@ -38,6 +41,7 @@ export const RealmSettingsTokensTab = ({
}: RealmSettingsSessionsTabProps) => { }: RealmSettingsSessionsTabProps) => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
const serverInfo = useServerInfo(); const serverInfo = useServerInfo();
const { whoAmI } = useWhoAmI();
const [defaultSigAlgDrpdwnIsOpen, setDefaultSigAlgDrpdwnOpen] = const [defaultSigAlgDrpdwnIsOpen, setDefaultSigAlgDrpdwnOpen] =
useState(false); useState(false);
@ -200,9 +204,12 @@ export const RealmSettingsTokensTab = ({
<FormGroup <FormGroup
label={t("accessTokenLifespan")} label={t("accessTokenLifespan")}
fieldId="accessTokenLifespan" fieldId="accessTokenLifespan"
helperText={`It is recommended for this value to be shorter than the SSO session idle timeout: ${interpolateTimespan( helperText={t("recommendedSsoTimeout", {
forHumans(realm.ssoSessionIdleTimeout!) time: toHumanFormat(
)}`} realm.ssoSessionIdleTimeout!,
whoAmI.getLocale()
),
})}
labelIcon={ labelIcon={
<HelpItem <HelpItem
helpText="realm-settings-help:accessTokenLifespan" helpText="realm-settings-help:accessTokenLifespan"
@ -225,7 +232,7 @@ export const RealmSettingsTokensTab = ({
aria-label="access-token-lifespan" aria-label="access-token-lifespan"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -252,7 +259,7 @@ export const RealmSettingsTokensTab = ({
aria-label="access-token-lifespan-implicit" aria-label="access-token-lifespan-implicit"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -278,7 +285,7 @@ export const RealmSettingsTokensTab = ({
aria-label="client-login-timeout" aria-label="client-login-timeout"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -307,7 +314,7 @@ export const RealmSettingsTokensTab = ({
aria-label="offline-session-max-input" aria-label="offline-session-max-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -347,7 +354,7 @@ export const RealmSettingsTokensTab = ({
aria-label="user-initiated-action-lifespan" aria-label="user-initiated-action-lifespan"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -374,7 +381,7 @@ export const RealmSettingsTokensTab = ({
aria-label="default-admin-initated-input" aria-label="default-admin-initated-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -401,7 +408,7 @@ export const RealmSettingsTokensTab = ({
aria-label="email-verification-input" aria-label="email-verification-input"
value={value} value={value}
onChange={(value: any) => onChange(value.toString())} onChange={(value: any) => onChange(value.toString())}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -422,7 +429,7 @@ export const RealmSettingsTokensTab = ({
aria-label="idp-email-verification" aria-label="idp-email-verification"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -443,7 +450,7 @@ export const RealmSettingsTokensTab = ({
aria-label="forgot-pw-input" aria-label="forgot-pw-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />
@ -464,7 +471,7 @@ export const RealmSettingsTokensTab = ({
aria-label="execute-actions-input" aria-label="execute-actions-input"
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />

View file

@ -132,7 +132,7 @@ export const EventConfigForm = ({
<TimeSelector <TimeSelector
value={value} value={value}
onChange={onChange} onChange={onChange}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
/> />
)} )}
/> />

View file

@ -358,11 +358,8 @@ export default {
userProfileSuccess: "User profile settings successfully updated.", userProfileSuccess: "User profile settings successfully updated.",
userProfileError: "Could not update user profile settings: {{error}}", userProfileError: "Could not update user profile settings: {{error}}",
status: "Status", status: "Status",
convertedToYearsValue: "{{convertedToYears}}", recommendedSsoTimeout:
convertedToDaysValue: "{{convertedToDays}}", "It is recommended for this value to be shorter than the SSO session idle timeout: {{time}}",
convertedToHoursValue: "{{convertedToHours}}",
convertedToMinutesValue: "{{convertedToMinutes}}",
convertedToSecondsValue: "{{convertedToSeconds}}",
supportedLocales: "Supported locales", supportedLocales: "Supported locales",
defaultLocale: "Default locale", defaultLocale: "Default locale",
selectLocales: "Select locales", selectLocales: "Select locales",

View file

@ -27,7 +27,7 @@ export const LifespanField = () => {
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<TimeSelector <TimeSelector
value={value} value={value}
units={["minutes", "hours", "days"]} units={["minute", "hour", "day"]}
onChange={onChange} onChange={onChange}
menuAppendTo="parent" menuAppendTo="parent"
/> />

View file

@ -1,5 +1,4 @@
import { cloneDeep } from "lodash-es"; import { cloneDeep } from "lodash-es";
import { useTranslation } from "react-i18next";
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import type { IFormatter, IFormatterValueType } from "@patternfly/react-table"; import type { IFormatter, IFormatterValueType } from "@patternfly/react-table";
import { unflatten, flatten } from "flat"; import { unflatten, flatten } from "flat";
@ -150,78 +149,6 @@ export const alphaRegexPattern = /[^A-Za-z]/g;
export const emailRegexPattern = export const emailRegexPattern =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const forHumans = (seconds: number) => {
const { t } = useTranslation();
const levels: [
[number, string],
[number, string],
[number, string],
[number, string],
[number, string]
] = [
[Math.floor(seconds / 31536000), t("common:times.years")],
[Math.floor((seconds % 31536000) / 86400), t("common:times.days")],
[
Math.floor(((seconds % 31536000) % 86400) / 3600),
t("common:times.hours"),
],
[
Math.floor((((seconds % 31536000) % 86400) % 3600) / 60),
t("common:times.minutes"),
],
[(((seconds % 31536000) % 86400) % 3600) % 60, t("common:times.seconds")],
];
let returntext = "";
for (let i = 0, max = levels.length; i < max; i++) {
if (levels[i][0] === 0) continue;
returntext +=
" " +
levels[i][0] +
" " +
(levels[i][0] === 1
? levels[i][1].substr(0, levels[i][1].length - 1)
: levels[i][1]);
}
return returntext.trim();
};
export const interpolateTimespan = (forHumans: string) => {
const { t } = useTranslation();
const timespan = forHumans.split(" ");
if (timespan[1] === "Years") {
return t(`realm-settings:convertedToYearsValue`, {
convertedToYears: forHumans,
});
}
if (timespan[1] === "Days") {
return t(`realm-settings:convertedToDaysValue`, {
convertedToYears: forHumans,
});
}
if (timespan[1] === "Hours") {
return t(`realm-settings:convertedToHoursValue`, {
convertedToHours: forHumans,
});
}
if (timespan[1] === "Minutes") {
return t(`realm-settings:convertedToMinutesValue`, {
convertedToMinutes: forHumans,
});
}
if (timespan[1] === "Seconds") {
return t(`realm-settings:convertedToSecondsValue`, {
convertedToSeconds: forHumans,
});
}
};
export const KEY_PROVIDER_TYPE = "org.keycloak.keys.KeyProvider"; export const KEY_PROVIDER_TYPE = "org.keycloak.keys.KeyProvider";
export const prettyPrintJSON = (value: any) => JSON.stringify(value, null, 2); export const prettyPrintJSON = (value: any) => JSON.stringify(value, null, 2);