Localization tab (#685)

* localization wip

wip localization

return key value data as array

localization table

css

lint

lint

clean up log stmts

clean up log stmts

* PR feedback from Erik

* fix logic for supported locales

* update empty state text

* set default value

* fix cypress test

* Update src/realm-settings/RealmSettingsSection.tsx

Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fix rsa-generated delete bug; PR feedback frog Erik

* revert locale abbreviation

* remove log stmts

* PR feedback from Erik, fix undefined

* fix loader w Erik

Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Jenny 2021-06-16 07:38:36 -04:00 committed by GitHub
parent 2c9b77c425
commit d2bf69fac7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 333 additions and 34 deletions

View file

@ -54,3 +54,7 @@ td.pf-c-table__check > input {
margin-top: var(--pf-c-table__check--input--MarginTop); margin-top: var(--pf-c-table__check--input--MarginTop);
vertical-align: baseline; vertical-align: baseline;
} }
.pf-c-pagination.pf-m-bottom.pf-m-compact {
padding: 0px;
}

View file

@ -0,0 +1,277 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import {
ActionGroup,
Button,
FormGroup,
PageSection,
Select,
SelectOption,
SelectVariant,
Switch,
TextContent,
} from "@patternfly/react-core";
import type RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { FormAccess } from "../components/form-access/FormAccess";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { FormPanel } from "../components/scroll-form/FormPanel";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useAdminClient } from "../context/auth/AdminClient";
import { getBaseUrl } from "../util";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
type LocalizationTabProps = {
save: (realm: RealmRepresentation) => void;
reset: () => void;
refresh: () => void;
realm: RealmRepresentation;
};
export const LocalizationTab = ({
save,
reset,
realm,
}: LocalizationTabProps) => {
const { t } = useTranslation("realm-settings");
const adminClient = useAdminClient();
const [key, setKey] = useState(0);
const [supportedLocalesOpen, setSupportedLocalesOpen] = useState(false);
const [defaultLocaleOpen, setDefaultLocaleOpen] = useState(false);
const { getValues, control, handleSubmit } = useFormContext();
// const [selectedLocale, setSelectedLocale] = useState("en");
const [valueSelected, setValueSelected] = useState(false);
const themeTypes = useServerInfo().themes!;
const watchSupportedLocales = useWatch({
control,
name: "supportedLocales",
defaultValue: themeTypes?.account![0].locales,
});
const internationalizationEnabled = useWatch({
control,
name: "internationalizationEnabled",
defaultValue: realm?.internationalizationEnabled,
});
const loader = async () => {
if (realm) {
const response = await fetch(
`${getBaseUrl(adminClient)}admin/realms/${realm.realm}/localization/${
getValues("defaultLocale") || "en"
}`,
{
method: "GET",
headers: {
Authorization: `bearer ${await adminClient.getAccessToken()}`,
},
}
);
const result = await response.json();
const resultTest = Object.keys(result).map((key) => [key, result[key]]);
return resultTest;
}
return [[]];
};
return (
<>
<PageSection variant="light">
<FormPanel
className="kc-login-screen"
title="Login screen customization"
>
<FormAccess
isHorizontal
role="manage-realm"
className="pf-u-mt-lg"
onSubmit={handleSubmit(save)}
>
<FormGroup
label={t("internationalization")}
fieldId="kc-internationalization"
>
<Controller
name="internationalizationEnabled"
control={control}
defaultValue={realm?.internationalizationEnabled}
render={({ onChange, value }) => (
<Switch
id="kc-l-internationalization"
label={t("common:enabled")}
labelOff={t("common:disabled")}
isChecked={internationalizationEnabled}
data-testid={
value
? "internationalization-enabled"
: "internationalization-disabled"
}
onChange={onChange}
/>
)}
/>
</FormGroup>
{internationalizationEnabled && (
<>
<FormGroup
label={t("supportedLocales")}
fieldId="kc-l-supported-locales"
>
<Controller
name="supportedLocales"
control={control}
defaultValue={themeTypes?.account![0].locales}
render={({ onChange }) => (
<Select
toggleId="kc-l-supported-locales"
onToggle={() => {
setSupportedLocalesOpen(!supportedLocalesOpen);
}}
onSelect={(_, v) => {
const option = v as string;
if (!watchSupportedLocales) {
onChange([option]);
} else if (watchSupportedLocales!.includes(option)) {
onChange(
watchSupportedLocales.filter(
(item: string) => item !== option
)
);
} else {
onChange([...watchSupportedLocales, option]);
}
}}
onClear={() => {
onChange([]);
}}
selections={watchSupportedLocales}
variant={SelectVariant.typeaheadMulti}
aria-label={t("supportedLocales")}
isOpen={supportedLocalesOpen}
placeholderText={"Select locales"}
>
{themeTypes?.login![0].locales.map(
(locale: string, idx: number) => (
<SelectOption
selected={true}
key={`locale-${idx}`}
value={locale}
>
{t(`allSupportedLocales.${locale}`)}
</SelectOption>
)
)}
</Select>
)}
/>
</FormGroup>
<FormGroup
label={t("defaultLocale")}
fieldId="kc-l-default-locale"
>
<Controller
name="defaultLocale"
control={control}
defaultValue={realm?.defaultLocale}
render={({ onChange, value }) => (
<Select
toggleId="kc-default-locale"
onToggle={() =>
setDefaultLocaleOpen(!defaultLocaleOpen)
}
onSelect={(_, value) => {
onChange(value as string);
setValueSelected(true);
// setSelectedLocale(value as string);
setKey(new Date().getTime());
setDefaultLocaleOpen(false);
}}
selections={
valueSelected
? t(`allSupportedLocales.${value}`)
: realm.defaultLocale !== ""
? t(
`allSupportedLocales.${
realm.defaultLocale || "en"
}`
)
: t("placeholderText")
}
variant={SelectVariant.single}
aria-label={t("defaultLocale")}
isOpen={defaultLocaleOpen}
placeholderText={t("placeholderText")}
data-testid="select-default-locale"
>
{watchSupportedLocales.map(
(locale: string, idx: number) => (
<SelectOption
key={`default-locale-${idx}`}
value={locale}
>
{t(`allSupportedLocales.${locale}`)}
</SelectOption>
)
)}
</Select>
)}
/>
</FormGroup>
</>
)}
<ActionGroup>
<Button
variant="primary"
type="submit"
data-testid="localization-tab-save"
>
{t("common:save")}
</Button>
<Button variant="link" onClick={reset}>
{t("common:revert")}
</Button>
</ActionGroup>
</FormAccess>
</FormPanel>
<FormPanel className="kc-login-screen" title="Edit message bundles">
<TextContent className="messageBundleDescription">
{t("messageBundleDescription")}
</TextContent>
<div className="tableBorder">
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="client-scopes:clientScopeList"
searchPlaceholderKey=" "
emptyState={
<ListEmptyState
hasIcon={true}
message={t("noMessageBundles")}
instructions={t("noMessageBundlesInstructions")}
onPrimaryAction={() => {}}
/>
}
canSelectAll
columns={[
{
name: "Key",
cellRenderer: (row) => row[0],
},
{
name: "Value",
cellRenderer: (row) => row[1],
},
]}
/>
</div>
</FormPanel>
</PageSection>
</>
);
};

View file

@ -214,8 +214,9 @@ export const RSAGeneratedModal = ({
} }
> >
<Controller <Controller
name="algorithm" name="config.algorithm"
defaultValue="" control={control}
defaultValue={["RS256"]}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Select <Select
toggleId="kc-rsa-algorithm" toggleId="kc-rsa-algorithm"
@ -223,7 +224,7 @@ export const RSAGeneratedModal = ({
setIsRSAalgDropdownOpen(!isRSAalgDropdownOpen) setIsRSAalgDropdownOpen(!isRSAalgDropdownOpen)
} }
onSelect={(_, value) => { onSelect={(_, value) => {
onChange(value as string); onChange([value + ""]);
setIsRSAalgDropdownOpen(false); setIsRSAalgDropdownOpen(false);
}} }}
selections={[value + ""]} selections={[value + ""]}

View file

@ -80,3 +80,15 @@ button.pf-c-button.pf-m-link.add-provider {
.add-provider-modal > div.pf-c-modal-box__body { .add-provider-modal > div.pf-c-modal-box__body {
overflow: visible; overflow: visible;
} }
.pf-c-content.messageBundleDescription {
max-width: 1024px;
padding-bottom: var(--pf-global--spacer--lg);
}
div.tableBorder {
border-style: solid;
border-width: 1px;
border-color: var(--pf-global--BorderColor--100);
max-width: 1024px;
}

View file

@ -31,6 +31,7 @@ import { EventsTab } from "./event-config/EventsTab";
import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation";
import { KeysProviderTab } from "./KeysProvidersTab"; import { KeysProviderTab } from "./KeysProvidersTab";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { LocalizationTab } from "./LocalizationTab";
type RealmSettingsHeaderProps = { type RealmSettingsHeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -129,9 +130,9 @@ export const RealmSettingsSection = () => {
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const form = useForm(); const form = useForm();
const { control, getValues, setValue, reset: resetForm } = form; const { control, getValues, setValue, reset: resetForm } = form;
const [key, setKey] = useState(0);
const [realm, setRealm] = useState<RealmRepresentation>(); const [realm, setRealm] = useState<RealmRepresentation>();
const [activeTab, setActiveTab] = useState(0); const [activeTab, setActiveTab] = useState(0);
const [key, setKey] = useState(0);
const [realmComponents, setRealmComponents] = useState< const [realmComponents, setRealmComponents] = useState<
ComponentRepresentation[] ComponentRepresentation[]
>(); >();
@ -149,17 +150,6 @@ export const RealmSettingsSection = () => {
[] []
); );
useEffect(() => {
const update = async () => {
const realmComponents = await adminClient.components.find({
type: "org.keycloak.keys.KeyProvider",
realm: realmName,
});
setRealmComponents(realmComponents);
};
setTimeout(update, 100);
}, [key]);
useFetch( useFetch(
async () => { async () => {
const realm = await adminClient.realms.findOne({ realm: realmName }); const realm = await adminClient.realms.findOne({ realm: realmName });
@ -174,24 +164,13 @@ export const RealmSettingsSection = () => {
setRealm(result.realm); setRealm(result.realm);
setRealmComponents(result.realmComponents); setRealmComponents(result.realmComponents);
}, },
[] [key]
); );
const refresh = () => { const refresh = () => {
setKey(new Date().getTime()); setKey(new Date().getTime());
}; };
useEffect(() => {
const update = async () => {
const realmComponents = await adminClient.components.find({
type: "org.keycloak.keys.KeyProvider",
realm: realmName,
});
setRealmComponents(realmComponents);
};
setTimeout(update, 100);
}, [key]);
useEffect(() => { useEffect(() => {
if (realm) setupForm(realm); if (realm) setupForm(realm);
}, [realm]); }, [realm]);
@ -264,6 +243,7 @@ export const RealmSettingsSection = () => {
<RealmSettingsThemesTab <RealmSettingsThemesTab
save={save} save={save}
reset={() => setupForm(realm!)} reset={() => setupForm(realm!)}
realm={realm!}
/> />
</Tab> </Tab>
<Tab <Tab
@ -306,6 +286,22 @@ export const RealmSettingsSection = () => {
> >
<EventsTab /> <EventsTab />
</Tab> </Tab>
{realm && (
<Tab
id="localization"
eventKey="localization"
title={<TabTitleText>{t("localization")}</TabTitleText>}
>
<LocalizationTab
key={key}
refresh={refresh}
save={save}
reset={() => setupForm(realm)}
realm={realm}
/>
</Tab>
)}
</KeycloakTabs> </KeycloakTabs>
</FormProvider> </FormProvider>
</PageSection> </PageSection>

View file

@ -20,11 +20,13 @@ import { useServerInfo } from "../context/server-info/ServerInfoProvider";
type RealmSettingsThemesTabProps = { type RealmSettingsThemesTabProps = {
save: (realm: RealmRepresentation) => void; save: (realm: RealmRepresentation) => void;
reset: () => void; reset: () => void;
realm: RealmRepresentation;
}; };
export const RealmSettingsThemesTab = ({ export const RealmSettingsThemesTab = ({
save, save,
reset, reset,
realm,
}: RealmSettingsThemesTabProps) => { }: RealmSettingsThemesTabProps) => {
const { t } = useTranslation("realm-settings"); const { t } = useTranslation("realm-settings");
@ -49,7 +51,7 @@ export const RealmSettingsThemesTab = ({
const internationalizationEnabled = useWatch({ const internationalizationEnabled = useWatch({
control, control,
name: "internationalizationEnabled", name: "internationalizationEnabled",
defaultValue: false, defaultValue: realm?.internationalizationEnabled,
}); });
return ( return (
@ -242,10 +244,10 @@ export const RealmSettingsThemesTab = ({
<Controller <Controller
name="internationalizationEnabled" name="internationalizationEnabled"
control={control} control={control}
defaultValue={false} defaultValue={internationalizationEnabled}
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Switch <Switch
id="kc-internationalization" id="kc-t-internationalization"
label={t("common:enabled")} label={t("common:enabled")}
labelOff={t("common:disabled")} labelOff={t("common:disabled")}
isChecked={value} isChecked={value}
@ -263,7 +265,7 @@ export const RealmSettingsThemesTab = ({
<> <>
<FormGroup <FormGroup
label={t("supportedLocales")} label={t("supportedLocales")}
fieldId="kc-supported-locales" fieldId="kc-t-supported-locales"
> >
<Controller <Controller
name="supportedLocales" name="supportedLocales"
@ -271,7 +273,7 @@ export const RealmSettingsThemesTab = ({
defaultValue={themeTypes?.account![0].locales} defaultValue={themeTypes?.account![0].locales}
render={({ value, onChange }) => ( render={({ value, onChange }) => (
<Select <Select
toggleId="kc-supported-locales" toggleId="kc-t-supported-locales"
onToggle={() => { onToggle={() => {
setSupportedLocalesOpen(!supportedLocalesOpen); setSupportedLocalesOpen(!supportedLocalesOpen);
}} }}
@ -318,7 +320,7 @@ export const RealmSettingsThemesTab = ({
defaultValue="" defaultValue=""
render={({ onChange, value }) => ( render={({ onChange, value }) => (
<Select <Select
toggleId="kc-default-locale" toggleId="kc-t-default-locale"
onToggle={() => setDefaultLocaleOpen(!defaultLocaleOpen)} onToggle={() => setDefaultLocaleOpen(!defaultLocaleOpen)}
onSelect={(_, value) => { onSelect={(_, value) => {
onChange(value as string); onChange(value as string);

View file

@ -127,6 +127,7 @@
"tr": "Türkçe", "tr": "Türkçe",
"zh-CN": "中文" "zh-CN": "中文"
}, },
"placeholderText": "Select one",
"userManagedAccess": "User-managed access", "userManagedAccess": "User-managed access",
"endpoints": "Endpoints", "endpoints": "Endpoints",
"openIDEndpointConfiguration": "OpenID Endpoint Configuration", "openIDEndpointConfiguration": "OpenID Endpoint Configuration",
@ -136,6 +137,9 @@
"adminTheme": "Admin console theme", "adminTheme": "Admin console theme",
"emailTheme": "Email theme", "emailTheme": "Email theme",
"internationalization": "Internationalization", "internationalization": "Internationalization",
"localization": "Localization",
"key": "Key",
"value": "Value",
"supportedLocales": "Supported locales", "supportedLocales": "Supported locales",
"defaultLocale": "Default locale", "defaultLocale": "Default locale",
@ -489,7 +493,10 @@
"user-events-cleared-error": "Could not clear the user events {{error}}", "user-events-cleared-error": "Could not clear the user events {{error}}",
"events-disable-title": "Unsave events?", "events-disable-title": "Unsave events?",
"events-disable-confirm": "If \"Save events\" is disabled, subsequent events will not be displayed in the \"Events\" menu", "events-disable-confirm": "If \"Save events\" is disabled, subsequent events will not be displayed in the \"Events\" menu",
"confirm": "Confirm" "confirm": "Confirm",
"noMessageBundles": "No message bundles",
"noMessageBundlesInstructions": "Add a message bundle to get started.",
"messageBundleDescription": "You can edit the supported locales. If you haven't selected supported locales yet, you can only edit the English locale."
}, },
"partial-import": { "partial-import": {
"partialImportHeaderText": "Partial import allows you to import users, clients, and other resources from a previously exported json file.", "partialImportHeaderText": "Partial import allows you to import users, clients, and other resources from a previously exported json file.",