Refactored the way we show time using Intl
(#2178)
This commit is contained in:
parent
1b6d679d89
commit
b7ea8629a2
12 changed files with 107 additions and 136 deletions
|
@ -255,7 +255,7 @@ export const OtpPolicy = ({ realm, realmUpdated }: OtpPolicyProps) => {
|
|||
aria-label={t("otpPolicyPeriod")}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["seconds", "minutes"]}
|
||||
units={["second", "minute"]}
|
||||
validated={errors.otpPolicyPeriod ? "error" : "default"}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -307,7 +307,7 @@ export const WebauthnPolicy = ({
|
|||
aria-label={t("webAuthnPolicyCreateTimeout")}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["seconds", "minutes", "hours"]}
|
||||
units={["second", "minute", "hour"]}
|
||||
validated={
|
||||
errors.webAuthnPolicyCreateTimeout ? "error" : "default"
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export const AdvancedSettings = ({
|
|||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
units={["minutes", "days", "hours"]}
|
||||
units={["minute", "day", "hour"]}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
@ -64,7 +64,7 @@ export const AdvancedSettings = ({
|
|||
id="accessTokenLifespan"
|
||||
name="attributes.access.token.lifespan"
|
||||
defaultValue=""
|
||||
units={["minutes", "days", "hours"]}
|
||||
units={["minute", "day", "hour"]}
|
||||
control={control}
|
||||
/>
|
||||
|
||||
|
|
|
@ -86,7 +86,6 @@ export default function CreateInitialAccessToken() {
|
|||
data-testid="expiration"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["days", "hours", "minutes", "seconds"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
|
@ -8,10 +10,17 @@ import {
|
|||
TextInputProps,
|
||||
ToggleMenuBaseProps,
|
||||
} 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 &
|
||||
ToggleMenuBaseProps & {
|
||||
|
@ -21,9 +30,28 @@ export type TimeSelectorProps = TextInputProps &
|
|||
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 = ({
|
||||
value,
|
||||
units = ["seconds", "minutes", "hours", "days"],
|
||||
units = ["second", "minute", "hour", "day"],
|
||||
onChange,
|
||||
className,
|
||||
min,
|
||||
|
@ -32,36 +60,26 @@ export const TimeSelector = ({
|
|||
}: TimeSelectorProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const allTimes: { unit: Unit; label: string; multiplier: number }[] = [
|
||||
{ unit: "seconds", label: t("times.seconds"), multiplier: 1 },
|
||||
{ unit: "minutes", label: t("times.minutes"), multiplier: 60 },
|
||||
{ unit: "hours", label: t("times.hours"), multiplier: 3600 },
|
||||
{ unit: "days", label: t("times.days"), multiplier: 86400 },
|
||||
];
|
||||
|
||||
const times = units.map(
|
||||
(unit) => allTimes.find((time) => time.unit === unit)!
|
||||
const times = useMemo(
|
||||
() => units.map((unit) => allTimes.find((time) => time.unit === unit)!),
|
||||
[units]
|
||||
);
|
||||
|
||||
const defaultMultiplier = useMemo(
|
||||
() => allTimes.find((time) => time.unit === units[0])?.multiplier,
|
||||
[units]
|
||||
);
|
||||
const defaultMultiplier = allTimes.find(
|
||||
(time) => time.unit === units[0]
|
||||
)?.multiplier;
|
||||
|
||||
const [timeValue, setTimeValue] = useState<"" | number>("");
|
||||
const [multiplier, setMultiplier] = useState(defaultMultiplier);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const x = times.reduce(
|
||||
(v, time) =>
|
||||
value % time.multiplier === 0 && v < time.multiplier
|
||||
? time.multiplier
|
||||
: v,
|
||||
1
|
||||
);
|
||||
const multiplier = getTimeUnit(value).multiplier;
|
||||
|
||||
if (value) {
|
||||
setMultiplier(x);
|
||||
setTimeValue(value / x);
|
||||
setMultiplier(multiplier);
|
||||
setTimeValue(value / multiplier);
|
||||
} else {
|
||||
setTimeValue(value);
|
||||
setMultiplier(defaultMultiplier);
|
||||
|
@ -118,7 +136,7 @@ export const TimeSelector = ({
|
|||
key={time.label}
|
||||
value={time.multiplier}
|
||||
>
|
||||
{time.label}
|
||||
{t(time.label)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
|
|
23
src/components/time-selector/time-selector.test.ts
Normal file
23
src/components/time-selector/time-selector.test.ts
Normal 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");
|
||||
});
|
||||
});
|
|
@ -78,7 +78,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="sso-session-idle-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -105,7 +105,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="sso-session-max-input"
|
||||
value={value}
|
||||
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"
|
||||
value={value}
|
||||
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"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -197,7 +197,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="client-session-idle-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -224,7 +224,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="client-session-max-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -262,7 +262,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="offline-session-idle-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -318,7 +318,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="offline-session-max-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -358,7 +358,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="login-timeout-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -385,7 +385,7 @@ export const RealmSettingsSessionsTab = ({
|
|||
aria-label="login-action-timeout-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -19,11 +19,14 @@ import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/r
|
|||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||
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 { forHumans, interpolateTimespan } from "../util";
|
||||
|
||||
import "./realm-settings-section.css";
|
||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
||||
|
||||
type RealmSettingsSessionsTabProps = {
|
||||
realm: RealmRepresentation;
|
||||
|
@ -38,6 +41,7 @@ export const RealmSettingsTokensTab = ({
|
|||
}: RealmSettingsSessionsTabProps) => {
|
||||
const { t } = useTranslation("realm-settings");
|
||||
const serverInfo = useServerInfo();
|
||||
const { whoAmI } = useWhoAmI();
|
||||
|
||||
const [defaultSigAlgDrpdwnIsOpen, setDefaultSigAlgDrpdwnOpen] =
|
||||
useState(false);
|
||||
|
@ -200,9 +204,12 @@ export const RealmSettingsTokensTab = ({
|
|||
<FormGroup
|
||||
label={t("accessTokenLifespan")}
|
||||
fieldId="accessTokenLifespan"
|
||||
helperText={`It is recommended for this value to be shorter than the SSO session idle timeout: ${interpolateTimespan(
|
||||
forHumans(realm.ssoSessionIdleTimeout!)
|
||||
)}`}
|
||||
helperText={t("recommendedSsoTimeout", {
|
||||
time: toHumanFormat(
|
||||
realm.ssoSessionIdleTimeout!,
|
||||
whoAmI.getLocale()
|
||||
),
|
||||
})}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="realm-settings-help:accessTokenLifespan"
|
||||
|
@ -225,7 +232,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="access-token-lifespan"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -252,7 +259,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="access-token-lifespan-implicit"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -278,7 +285,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="client-login-timeout"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -307,7 +314,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="offline-session-max-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -347,7 +354,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="user-initiated-action-lifespan"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -374,7 +381,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="default-admin-initated-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -401,7 +408,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="email-verification-input"
|
||||
value={value}
|
||||
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"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -443,7 +450,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="forgot-pw-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -464,7 +471,7 @@ export const RealmSettingsTokensTab = ({
|
|||
aria-label="execute-actions-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -132,7 +132,7 @@ export const EventConfigForm = ({
|
|||
<TimeSelector
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -358,11 +358,8 @@ export default {
|
|||
userProfileSuccess: "User profile settings successfully updated.",
|
||||
userProfileError: "Could not update user profile settings: {{error}}",
|
||||
status: "Status",
|
||||
convertedToYearsValue: "{{convertedToYears}}",
|
||||
convertedToDaysValue: "{{convertedToDays}}",
|
||||
convertedToHoursValue: "{{convertedToHours}}",
|
||||
convertedToMinutesValue: "{{convertedToMinutes}}",
|
||||
convertedToSecondsValue: "{{convertedToSeconds}}",
|
||||
recommendedSsoTimeout:
|
||||
"It is recommended for this value to be shorter than the SSO session idle timeout: {{time}}",
|
||||
supportedLocales: "Supported locales",
|
||||
defaultLocale: "Default locale",
|
||||
selectLocales: "Select locales",
|
||||
|
|
|
@ -27,7 +27,7 @@ export const LifespanField = () => {
|
|||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
value={value}
|
||||
units={["minutes", "hours", "days"]}
|
||||
units={["minute", "hour", "day"]}
|
||||
onChange={onChange}
|
||||
menuAppendTo="parent"
|
||||
/>
|
||||
|
|
73
src/util.ts
73
src/util.ts
|
@ -1,5 +1,4 @@
|
|||
import { cloneDeep } from "lodash-es";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import FileSaver from "file-saver";
|
||||
import type { IFormatter, IFormatterValueType } from "@patternfly/react-table";
|
||||
import { unflatten, flatten } from "flat";
|
||||
|
@ -150,78 +149,6 @@ export const alphaRegexPattern = /[^A-Za-z]/g;
|
|||
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,}))$/;
|
||||
|
||||
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 prettyPrintJSON = (value: any) => JSON.stringify(value, null, 2);
|
||||
|
|
Loading…
Reference in a new issue