From 33651c396cf2e6244a8dbdab1ab873deb228249e Mon Sep 17 00:00:00 2001 From: agagancarczyk <4890675+agagancarczyk@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:24:26 +0000 Subject: [PATCH] Localization: Realm Overrides Fixes (#26169) * resolved conflict Signed-off-by: Agnieszka Gancarczyk * improvements Signed-off-by: Agnieszka Gancarczyk * improved tests Signed-off-by: Agnieszka Gancarczyk * feedback Signed-off-by: Agnieszka Gancarczyk * test fix Signed-off-by: Agnieszka Gancarczyk * test fix Signed-off-by: Agnieszka Gancarczyk * resolved conflict Signed-off-by: Agnieszka Gancarczyk * fixed test Signed-off-by: Agnieszka Gancarczyk --------- Signed-off-by: Agnieszka Gancarczyk Co-authored-by: Agnieszka Gancarczyk --- .../e2e/realm_settings_tabs_test.spec.ts | 8 +- .../realm_settings/RealmSettingsPage.ts | 2 +- .../admin/messages/messages_en.properties | 8 +- .../admin/messages/messages_pl.properties | 5 +- .../src/realm-settings/LocalizationTab.tsx | 625 ------------------ .../localization/LocalizationTab.tsx | 24 +- .../localization/RealmOverrides.tsx | 46 +- 7 files changed, 43 insertions(+), 675 deletions(-) delete mode 100644 js/apps/admin-ui/src/realm-settings/LocalizationTab.tsx diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts index 4f12843fbacc..ad29f8313564 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts @@ -206,7 +206,8 @@ describe("Realm settings tabs tests", () => { .contains("td", "123") .should("be.visible"); - cy.findByTestId("realmOverrides-deleteKebabToggle").click(); + cy.get('td.pf-c-table__action button[aria-label="Actions"]').click(); + cy.contains("button", "Delete").click(); cy.findByTestId("confirm").click(); masthead.checkNotificationMessage( "Successfully removed message(s) from the bundle.", @@ -253,7 +254,8 @@ describe("Realm settings tabs tests", () => { .contains("td", "def") .should("be.visible"); - cy.findByTestId("realmOverrides-deleteKebabToggle").click(); + cy.get('td.pf-c-table__action button[aria-label="Actions"]').click(); + cy.contains("button", "Delete").click(); cy.findByTestId("confirm").click(); masthead.checkNotificationMessage( @@ -343,7 +345,7 @@ describe("Realm settings tabs tests", () => { it("Check a11y violations on localization realm overrides sub tab/ adding message bundle", () => { realmSettingsPage.goToLocalizationTab(); realmSettingsPage.goToLocalizationRealmOverridesSubTab(); - cy.findByTestId("add-bundle-button").click(); + cy.findByTestId("add-translationBtn").click(); cy.checkA11y(); modalUtils.cancelModal(); }); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts index e2e86305b589..ceadf00455df 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts @@ -95,7 +95,7 @@ export default class RealmSettingsPage extends CommonPage { testConnectionButton = "test-connection-button"; modalTestConnectionButton = "modal-test-connection-button"; emailAddressInput = "email-address-input"; - addBundleButton = "add-bundle-button"; + addBundleButton = "add-translationBtn"; confirmAddBundle = "add-bundle-confirm-button"; keyInput = "key-input"; valueInput = "value-input"; diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index fea97d8659c6..70966dcc2a7a 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -964,7 +964,7 @@ deleteMappingConfirm=Are you sure you want to delete this mapping? createClientProfileSuccess=New client profile created eventTypes.CLIENT_LOGIN_ERROR.description=Client login error explainBearerOnly=This is a special OIDC type. This client only allows bearer token requests and cannot participate in browser logins. -noMessageBundlesInstructions=Add a message bundle to get started. +noTranslationsInstructions=Add a translation to get started. clearFile=Clear this file allowCreate=Allow create providerUpdatedError=Could not update client policy due to {{error}} @@ -2632,7 +2632,7 @@ minus=Minus groupsHelp=Groups where the user has membership. To leave a group, select it and click Leave. includeGroupsAndRoles=Include groups and roles groupsPermissionsHint=Determines if fine grained permissions are enabled for managing this role. Disabling will delete all current permissions that have been set up. -searchForMessageBundle=Search for message bundle +searchForTranslation=Search for translation offlineSessionMaxHelp=Max time before an offline session is expired regardless of activity. resourceSaveError=Could not persist resource due to {{error}} clientsClientScopesHelp=The scopes associated with this resource. @@ -2673,7 +2673,7 @@ policyType.totp=Time based addAttribute=Add {{label}} clientScopeSearch.protocol=Protocol initialAccessTokenDetails=Initial access token details -noMessageBundles=No message bundles +noTranslations=No translations deleteProvider=Delete provider? inputTypeSize=Input size createAttributeSubTitle=Create a new attribute @@ -2984,3 +2984,5 @@ joinCommunity=Join community readBlog=Read blog customValue=Custom value termsAndConditionsUserAttribute=Terms and conditions accepted timestamp +realmOverridesDescription= Realm overrides allow you to specify translations that will take effect for the entire realm. These translations will override any translation specified by a theme. +addTranslation=Add translation \ No newline at end of file diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_pl.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_pl.properties index e8beeacc61b2..9302a24477e4 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_pl.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_pl.properties @@ -951,7 +951,7 @@ deleteMappingConfirm=Czy na pewno chcesz usunąć to odwzorowanie? createClientProfileSuccess=Utworzono nowy profil klienta eventTypes.CLIENT_LOGIN_ERROR.description=Błąd logowania klienta explainBearerOnly=To jest specjalny typ OIDC. Ten klient pozwala tylko na żądania tokenów dostępu i nie może uczestniczyć w logowaniach przeglądarki. -noMessageBundlesInstructions=Dodaj pakiet wiadomości, aby rozpocząć. +noTranslationsInstructions=Dodaj tłumaczenie, aby rozpocząć. clearFile=Wyczyść ten plik allowCreate=Zezwól na tworzenie providerUpdatedError=Nie można zaktualizować polityki klienta z powodu błędu: {{error}} @@ -2644,7 +2644,7 @@ policyType.totp=Oparte na czasie addAttribute=Dodaj atrybut clientScopeSearch.protocol=Protokół initialAccessTokenDetails=Informacje o tokenie początkowym dostępu -noMessageBundles=Brak pakietów wiadomości +noTranslations=Brak tłuamczeń deleteProvider=Usunąć dostawcę? inputTypeSize=Rozmiar pola wejściowego createAttributeSubTitle=Utwórz nowy atrybut @@ -2916,3 +2916,4 @@ invalidEmailMessage='{{0}}': Nieprawidłowy adres e-mail. missingLastNameMessage='{{0}}': Proszę podać nazwisko. missingEmailMessage='{{0}}': Proszę podać adres e-mail. missingPasswordMessage='{{0}}': Proszę podać hasło. +addTranslation=Dodaj tłumaczenie diff --git a/js/apps/admin-ui/src/realm-settings/LocalizationTab.tsx b/js/apps/admin-ui/src/realm-settings/LocalizationTab.tsx deleted file mode 100644 index 5229ce59257c..000000000000 --- a/js/apps/admin-ui/src/realm-settings/LocalizationTab.tsx +++ /dev/null @@ -1,625 +0,0 @@ -import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; -import { - ActionGroup, - AlertVariant, - Button, - Divider, - FormGroup, - PageSection, - Select, - SelectGroup, - SelectOption, - SelectVariant, - Switch, - TextContent, - ToolbarItem, -} from "@patternfly/react-core"; -import { SearchIcon } from "@patternfly/react-icons"; -import { - EditableTextCell, - IEditableTextCell, - IRow, - IRowCell, - RowEditType, - RowErrors, - Table, - TableBody, - TableHeader, - TableVariant, - applyCellEdits, - cancelCellEdits, - validateCellEdits, -} from "@patternfly/react-table"; -import { cloneDeep, isEqual, uniqWith } from "lodash-es"; -import { useEffect, useMemo, useState } from "react"; -import { Controller, useForm, useWatch } from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import { FormPanel, HelpItem } from "ui-shared"; -import { adminClient } from "../admin-client"; -import { useAlerts } from "../components/alert/Alerts"; -import { FormAccess } from "../components/form/FormAccess"; -import type { KeyValueType } from "../components/key-value-form/key-value-convert"; -import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; -import { PaginatingTableToolbar } from "../components/table-toolbar/PaginatingTableToolbar"; -import { useRealm } from "../context/realm-context/RealmContext"; -import { useServerInfo } from "../context/server-info/ServerInfoProvider"; -import { useWhoAmI } from "../context/whoami/WhoAmI"; -import { DEFAULT_LOCALE } from "../i18n/i18n"; -import { convertToFormValues, localeToDisplayName } from "../util"; -import { useFetch } from "../utils/useFetch"; -import useLocaleSort, { mapByKey } from "../utils/useLocaleSort"; -import { AddMessageBundleModal } from "./AddMessageBundleModal"; - -type LocalizationTabProps = { - save: (realm: RealmRepresentation) => void; - refresh: () => void; - realm: RealmRepresentation; -}; - -type LocaleSpecificEntry = { - key: string; - value: string; -}; - -export enum RowEditAction { - Save = "save", - Cancel = "cancel", - Edit = "edit", - Delete = "delete", -} - -export type BundleForm = { - key: string; - value: string; - messageBundle: KeyValueType; -}; - -export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => { - const { t } = useTranslation(); - const [addMessageBundleModalOpen, setAddMessageBundleModalOpen] = - useState(false); - - const [supportedLocalesOpen, setSupportedLocalesOpen] = useState(false); - const [defaultLocaleOpen, setDefaultLocaleOpen] = useState(false); - const [filterDropdownOpen, setFilterDropdownOpen] = useState(false); - const [selectMenuLocale, setSelectMenuLocale] = useState(DEFAULT_LOCALE); - - const { setValue, getValues, control, handleSubmit, formState } = useForm(); - const [selectMenuValueSelected, setSelectMenuValueSelected] = useState(false); - const [messageBundles, setMessageBundles] = useState( - [], - ); - const [tableRows, setTableRows] = useState([]); - - const themeTypes = useServerInfo().themes!; - const allLocales = useMemo(() => { - const locales = Object.values(themeTypes).flatMap((theme) => - theme.flatMap(({ locales }) => (locales ? locales : [])), - ); - return Array.from(new Set(locales)); - }, [themeTypes]); - const bundleForm = useForm({ mode: "onChange" }); - const { addAlert, addError } = useAlerts(); - const { realm: currentRealm } = useRealm(); - const { whoAmI } = useWhoAmI(); - const localeSort = useLocaleSort(); - - const defaultSupportedLocales = realm.supportedLocales?.length - ? realm.supportedLocales - : [DEFAULT_LOCALE]; - - const setupForm = () => { - convertToFormValues(realm, setValue); - setValue("supportedLocales", defaultSupportedLocales); - }; - - useEffect(setupForm, []); - - const watchSupportedLocales: string[] = useWatch({ - control, - name: "supportedLocales", - defaultValue: defaultSupportedLocales, - }); - const internationalizationEnabled = useWatch({ - control, - name: "internationalizationEnabled", - defaultValue: realm.internationalizationEnabled, - }); - - const [tableKey, setTableKey] = useState(0); - const [max, setMax] = useState(10); - const [first, setFirst] = useState(0); - const [filter, setFilter] = useState(""); - - const refreshTable = () => { - setTableKey(tableKey + 1); - }; - - useFetch( - async () => { - let result = await adminClient.realms - .getRealmLocalizationTexts({ - first, - max, - realm: realm.realm!, - selectedLocale: - selectMenuLocale || - getValues("defaultLocale") || - whoAmI.getLocale(), - }) - // prevents server error in dev mode due to snowpack - .catch(() => []); - - const searchInBundles = (idx: number) => { - return Object.entries(result).filter((i) => i[idx].includes(filter)); - }; - - if (filter) { - const filtered = uniqWith( - searchInBundles(0).concat(searchInBundles(1)), - isEqual, - ); - - result = Object.fromEntries(filtered); - } - - return { result }; - }, - ({ result }) => { - const bundles = localeSort( - Object.entries(result).map(([key, value]) => ({ - key, - value, - })), - mapByKey("key"), - ).slice(first, first + max + 1); - - setMessageBundles(bundles); - - const updatedRows = bundles.map((messageBundle) => ({ - rowEditBtnAriaLabel: () => - t("rowEditBtnAriaLabel", { - messageBundle: messageBundle.value, - }), - rowSaveBtnAriaLabel: () => - t("rowSaveBtnAriaLabel", { - messageBundle: messageBundle.value, - }), - rowCancelBtnAriaLabel: () => - t("rowCancelBtnAriaLabel", { - messageBundle: messageBundle.value, - }), - cells: [ - { - title: (value, rowIndex, cellIndex, props) => ( - - ), - props: { - value: messageBundle.key, - }, - }, - { - title: (value, rowIndex, cellIndex, props) => ( - - ), - props: { - value: messageBundle.value, - }, - }, - ], - })); - setTableRows(updatedRows); - - return bundles; - }, - [tableKey, filter, first, max], - ); - - const handleTextInputChange = ( - newValue: string, - evt: any, - rowIndex: number, - cellIndex: number, - ) => { - setTableRows((prev) => { - const newRows = cloneDeep(prev); - const textCell = newRows[rowIndex]?.cells?.[ - cellIndex - ] as IEditableTextCell; - textCell.props.editableValue = newValue; - return newRows; - }); - }; - - const updateEditableRows = async ( - type: RowEditType, - rowIndex?: number, - validationErrors?: RowErrors, - ) => { - if (rowIndex === undefined) { - return; - } - const newRows = cloneDeep(tableRows); - let newRow: IRow; - const invalid = - !!validationErrors && Object.keys(validationErrors).length > 0; - - if (invalid) { - newRow = validateCellEdits(newRows[rowIndex], type, validationErrors); - } else if (type === RowEditAction.Cancel) { - newRow = cancelCellEdits(newRows[rowIndex]); - } else { - newRow = applyCellEdits(newRows[rowIndex], type); - } - newRows[rowIndex] = newRow; - - // Update the copy of the retrieved data set so we can save it when the user saves changes - - if (!invalid && type === RowEditAction.Save) { - const key = (newRow.cells?.[0] as IRowCell).props.value; - const value = (newRow.cells?.[1] as IRowCell).props.value; - - // We only have one editable value, otherwise we'd need to save each - try { - await adminClient.realms.addLocalization( - { - realm: realm.realm!, - selectedLocale: - selectMenuLocale || getValues("defaultLocale") || DEFAULT_LOCALE, - key, - }, - value, - ); - addAlert(t("updateMessageBundleSuccess"), AlertVariant.success); - } catch (error) { - addAlert(t("updateMessageBundleError"), AlertVariant.danger); - } - } - setTableRows(newRows); - }; - - const handleModalToggle = () => { - setAddMessageBundleModalOpen(!addMessageBundleModalOpen); - }; - - const options = [ - - - {localeToDisplayName(DEFAULT_LOCALE, whoAmI.getLocale())} - - , - , - - {watchSupportedLocales.map((locale) => ( - - {localeToDisplayName(locale, whoAmI.getLocale())} - - ))} - , - ]; - - const addKeyValue = async (pair: KeyValueType): Promise => { - try { - await adminClient.realms.addLocalization( - { - realm: currentRealm!, - selectedLocale: - selectMenuLocale || getValues("defaultLocale") || DEFAULT_LOCALE, - key: pair.key, - }, - pair.value, - ); - - adminClient.setConfig({ - realmName: currentRealm!, - }); - refreshTable(); - addAlert(t("addMessageBundleSuccess"), AlertVariant.success); - } catch (error) { - addError(t("addMessageBundleError"), error); - } - }; - - const deleteKey = async (key: string) => { - try { - await adminClient.realms.deleteRealmLocalizationTexts({ - realm: currentRealm!, - selectedLocale: selectMenuLocale, - key, - }); - refreshTable(); - addAlert(t("deleteMessageBundleSuccess")); - } catch (error) { - addError("deleteMessageBundleError", error); - } - }; - - return ( - <> - {addMessageBundleModalOpen && ( - { - addKeyValue(pair); - handleModalToggle(); - }} - form={bundleForm} - /> - )} - - - - } - > - ( - - )} - /> - - {internationalizationEnabled && ( - <> - - ( - - )} - /> - - - ( - - )} - /> - - - )} - - - - - - - - - {t("messageBundleDescription")} - -
- { - setFirst(first); - setMax(max); - }} - inputGroupName={"search"} - inputGroupOnEnter={(search) => { - setFilter(search); - setFirst(0); - setMax(10); - }} - inputGroupPlaceholder={t("searchForMessageBundle")} - toolbarItem={ - - } - searchTypeComponent={ - - - - } - > - {messageBundles.length === 0 && !filter && ( - - )} - {messageBundles.length === 0 && filter && ( - - )} - {messageBundles.length !== 0 && ( - - updateEditableRows(type, rowIndex, validation) - } - actions={[ - { - title: t("delete"), - onClick: (_, row) => - deleteKey( - (tableRows[row].cells?.[0] as IRowCell).props.value, - ), - }, - ]} - > - - -
- )} -
-
-
-
- - ); -}; diff --git a/js/apps/admin-ui/src/realm-settings/localization/LocalizationTab.tsx b/js/apps/admin-ui/src/realm-settings/localization/LocalizationTab.tsx index 58a8c1b25814..2bbbb295470f 100644 --- a/js/apps/admin-ui/src/realm-settings/localization/LocalizationTab.tsx +++ b/js/apps/admin-ui/src/realm-settings/localization/LocalizationTab.tsx @@ -231,17 +231,7 @@ export const LocalizationTab = ({ save, realm }: LocalizationTabProps) => { - {t("realmOverrides")}{" "} - - - } + title={{t("realmOverrides")} } data-testid="rs-localization-realm-overrides-tab" > { - {t("effectiveMessageBundles")} - - - } + title={{t("effectiveMessageBundles")}} data-testid="rs-localization-effective-message-bundles-tab" > )} + + + {t("realmOverridesDescription")} + + )} @@ -544,20 +553,19 @@ export const RealmOverrides = ({ - - } - onClick={() => { - setSelectedRowKeys([ - (row.cells?.[0] as IRowCell).props.value, - ]); - toggleDeleteDialog(); - setKebabOpen(false); - }} + { + setSelectedRowKeys([ + (row.cells?.[0] as IRowCell).props.value, + ]); + toggleDeleteDialog(); + setKebabOpen(false); + }, + }, + ]} />