├── .nvmrc ├── audit-ci.json ├── src ├── vite-env.d.ts ├── setupTests.ts ├── components │ ├── Form │ │ ├── IpaPACType │ │ │ └── index.ts │ │ ├── IpaSelect │ │ │ └── index.ts │ │ ├── IpaCalendar │ │ │ ├── index.ts │ │ │ └── IpaCalendar.tsx │ │ ├── IpaCheckbox │ │ │ ├── index.ts │ │ │ └── IpaCheckbox.tsx │ │ ├── IpaTextArea │ │ │ ├── index.ts │ │ │ └── IpaTextArea.tsx │ │ ├── IpaTextInput │ │ │ └── index.ts │ │ ├── IpaCheckboxes │ │ │ └── index.ts │ │ ├── IpaCertificates │ │ │ └── index.ts │ │ ├── IpaNumberInput │ │ │ └── index.ts │ │ ├── IpaTextContent │ │ │ ├── index.ts │ │ │ └── IpaTextContent.tsx │ │ ├── IpaTextboxList │ │ │ └── index.ts │ │ ├── IpaToggleGroup │ │ │ └── index.ts │ │ ├── DateTimeSelector │ │ │ └── index.ts │ │ ├── IpaDropdownSearch │ │ │ └── index.ts │ │ ├── IpaPasswordInput │ │ │ ├── index.ts │ │ │ └── IpaPasswordInput.tsx │ │ ├── IpaSshPublicKeys │ │ │ └── index.ts │ │ ├── IpaTextInputFromList │ │ │ └── index.ts │ │ ├── IpaCertificateMappingData │ │ │ └── index.ts │ │ └── PrincipalAliasMultiTextBox │ │ │ └── index.ts │ ├── layouts │ │ ├── TabLayout │ │ │ ├── index.ts │ │ │ ├── TabLayout.test.tsx │ │ │ └── TabLayout.tsx │ │ ├── BreadCrumb │ │ │ ├── index.ts │ │ │ └── BreadCrumb.tsx │ │ ├── DataSpinner │ │ │ ├── index.ts │ │ │ ├── DataSpinner.tsx │ │ │ └── DataSpinner.test.tsx │ │ ├── KebabLayout │ │ │ ├── index.ts │ │ │ └── KebabLayout.tsx │ │ ├── TableLayout │ │ │ ├── index.ts │ │ │ ├── TableLayout.tsx │ │ │ └── TableLayout.test.tsx │ │ ├── TitleLayout │ │ │ ├── index.ts │ │ │ ├── TitleLayout.test.tsx │ │ │ └── TitleLayout.tsx │ │ ├── DropdownSearch │ │ │ └── index.ts │ │ ├── DualListLayout │ │ │ └── index.ts │ │ ├── PasswordInput │ │ │ └── index.ts │ │ ├── SidebarLayout │ │ │ ├── index.ts │ │ │ ├── SidebarLayout.test.tsx │ │ │ └── SidebarLayout.tsx │ │ ├── ToolbarLayout │ │ │ ├── index.ts │ │ │ ├── ToolbarLayout.test.tsx │ │ │ └── ToolbarLayout.tsx │ │ ├── SearchInputLayout │ │ │ ├── index.ts │ │ │ └── SearchInputLayout.tsx │ │ ├── SettingsTableLayout │ │ │ └── index.ts │ │ ├── PopoverWithIconLayout │ │ │ ├── index.ts │ │ │ ├── PopoverWithIconLayout.tsx │ │ │ └── PopoverWithIconLayout.test.tsx │ │ ├── Skeleton │ │ │ └── SkeletonOnTableLayout │ │ │ │ ├── index.ts │ │ │ │ ├── SkeletonOnTableLayout.tsx │ │ │ │ └── SkeletonOnTableLayout.test.tsx │ │ ├── HelpTextWithIconLayout.tsx │ │ ├── InformationModalLayout.tsx │ │ ├── InputRequiredText.tsx │ │ ├── CustomTooltip.tsx │ │ ├── HelperTextWithIcon.tsx │ │ ├── SecondaryButton.tsx │ │ ├── PageLayout.tsx │ │ └── PageWithGrayBorderLayout.tsx │ ├── BulkSelectorPrep │ │ └── index.ts │ ├── modals │ │ ├── DnsZones │ │ │ └── dnsLabels.ts │ │ ├── ErrorModal.tsx │ │ └── ConfirmationModal.tsx │ ├── errors │ │ ├── GlobalErrors.tsx │ │ ├── PageErrors.tsx │ │ └── ModalErrors.tsx │ ├── tables │ │ ├── EmptyBodyTable.tsx │ │ └── UsersDisplayTable.tsx │ ├── ManagedAlerts.tsx │ ├── ServicesSections │ │ ├── ServiceCertificate.tsx │ │ └── Provisioning.tsx │ ├── HostsSections │ │ └── HostCertificate.tsx │ └── UsersSections │ │ └── UsersKerberosTicket.tsx ├── utils │ ├── constUtils.ts │ ├── paramsUtils.ts │ ├── testAlertsUtils.tsx │ ├── rolesUtils.tsx │ ├── configurationSettings.tsx │ ├── sudoCmdsUtils.tsx │ ├── hbacServicesUtils.tsx │ ├── sudoCmdGroupsUtils.tsx │ ├── idViewUtils.tsx │ ├── hbacServiceGrpUtils.tsx │ ├── ipdServerUtils.tsx │ ├── netgroupsUtils.tsx │ ├── hostGroupUtils.tsx │ ├── hbacRulesUtils.tsx │ ├── krbPolicyUtils.tsx │ ├── invariants.ts │ ├── dnsServersUtils.tsx │ ├── trustsUtils.tsx │ ├── pwPolicyUtils.tsx │ ├── subIdUtils.tsx │ ├── dnsConfigUtils.tsx │ ├── dnsForwardZonesUtils.tsx │ ├── groupUtils.tsx │ └── automemberUtils.tsx ├── pages │ ├── HBACTest │ │ └── HBACTest.tsx │ ├── SELinuxUserMaps │ │ └── SELinuxUserMaps.tsx │ └── Configuration │ │ ├── ConfigServiceOptions.tsx │ │ ├── ConfigSELinuxOptions.tsx │ │ ├── ConfigSearchOptions.tsx │ │ └── ConfigGroupOptions.tsx ├── store │ ├── hooks.ts │ ├── Global │ │ ├── auth-slice.ts │ │ ├── alerts-slice.ts │ │ └── routes-slice.ts │ └── store.ts ├── main.tsx ├── patternfly.d.ts ├── hooks │ ├── usePagination.tsx │ └── useApiError.tsx ├── main.css └── services │ └── rpcConfig.ts ├── public ├── favicon.ico └── assets │ └── images │ ├── header-logo.png │ ├── product-name.png │ ├── login-screen-background.jpg │ └── avatarImg.svg ├── .prettierignore ├── .prettierrc ├── doc ├── mockup-delete.png ├── mockup-settings.png ├── mockup-navigation.png ├── mockup-scrolling.png ├── images │ └── comm-layer-design.png ├── designs │ └── index.rst ├── requirements.txt ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── developer ├── scenarios │ ├── no-dns │ │ ├── hosts │ │ ├── README.md │ │ ├── compose.yml │ │ └── inventory.yml │ ├── single-server │ │ ├── hosts │ │ ├── README.md │ │ ├── compose.yml │ │ └── inventory.yml │ └── server-addc │ │ ├── hosts │ │ ├── README.md │ │ ├── inventory.yml │ │ ├── playbooks │ │ └── ipaserver_resolv_conf.yml │ │ └── compose.yml ├── requirements.yml ├── containerfiles │ ├── external-nodes │ ├── centos │ └── fedora ├── chrome_config.sh └── deploy-ipaserver.yml ├── .vscode └── settings.json ├── cypress ├── support │ ├── index.d.ts │ ├── e2e.ts │ └── commands.ts └── e2e │ ├── common │ ├── alert.ts │ ├── modal.ts │ ├── api │ │ └── intercept.ts │ ├── ui │ │ ├── button.ts │ │ ├── checkbox.ts │ │ ├── kebab.ts │ │ ├── tab.ts │ │ ├── toggle_button.ts │ │ ├── radiobutton.ts │ │ ├── select.ts │ │ ├── textbox.ts │ │ └── dual_list.ts │ ├── navigation.ts │ ├── settings_table.ts │ ├── members_table.ts │ ├── hbac_rule_management.ts │ ├── host_management.ts │ ├── sudo_rules_management.ts │ ├── authentication.ts │ └── member_of.ts │ ├── automember │ ├── parameter_types.ts │ ├── automember_user_group.feature │ └── automember_hostgroup.feature │ ├── login │ └── otp.ts │ ├── dns │ ├── parameter_types.ts │ └── dns_servers_settings.ts │ ├── sudo_rules │ └── sudo_rules.ts │ ├── subids │ ├── subids.feature │ ├── subids_settings.feature │ └── subids.ts │ ├── hostgroups │ └── hostgroup.feature │ ├── hbac │ ├── hbac_service │ │ └── hbac_service_settings.feature │ └── hbac_service_group │ │ └── hbac_service_group_settings.feature │ ├── id_views │ ├── id_views_settings.feature │ └── id_views.ts │ └── users │ ├── users.ts │ └── preserved_users.ts ├── index.html ├── .gitignore ├── .readthedocs.yaml ├── .sourcery.yaml ├── .pre-commit-config.yaml ├── knip.json ├── .github └── workflows │ ├── stale.yml │ ├── integration_tests.yaml │ ├── dev_environment.yml │ └── gating.yml ├── Makefile.am ├── tsconfig.json ├── index.dev.html ├── vite.config.ts └── cypress.config.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/jod -------------------------------------------------------------------------------- /audit-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "moderate": true 3 | } 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/vitest"; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | dist/** 3 | coverage/** 4 | doc/** 5 | tests/ipalab/_venv 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /doc/mockup-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/doc/mockup-delete.png -------------------------------------------------------------------------------- /doc/mockup-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/doc/mockup-settings.png -------------------------------------------------------------------------------- /doc/mockup-navigation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/doc/mockup-navigation.png -------------------------------------------------------------------------------- /doc/mockup-scrolling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/doc/mockup-scrolling.png -------------------------------------------------------------------------------- /developer/scenarios/no-dns/hosts: -------------------------------------------------------------------------------- 1 | 2 | # FreeIPA webui hosts for 'no-dns' 3 | 192.168.56.10 webui.ipa.test 4 | -------------------------------------------------------------------------------- /src/components/Form/IpaPACType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaPACType"; 2 | export * from "./IpaPACType"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaSelect/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaSelect"; 2 | export * from "./IpaSelect"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/TabLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TabLayout"; 2 | export * from "./TabLayout"; 3 | -------------------------------------------------------------------------------- /developer/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: containers.podman 4 | - name: freeipa.ansible_freeipa 5 | -------------------------------------------------------------------------------- /doc/images/comm-layer-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/doc/images/comm-layer-design.png -------------------------------------------------------------------------------- /src/components/Form/IpaCalendar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaCalendar"; 2 | export * from "./IpaCalendar"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaCheckbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaCheckbox"; 2 | export * from "./IpaCheckbox"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaTextArea/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaTextArea"; 2 | export * from "./IpaTextArea"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaTextInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaTextInput"; 2 | export * from "./IpaTextInput"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/BreadCrumb/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./BreadCrumb"; 2 | export * from "./BreadCrumb"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/DataSpinner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DataSpinner"; 2 | export * from "./DataSpinner"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/KebabLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./KebabLayout"; 2 | export * from "./KebabLayout"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/TableLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TableLayout"; 2 | export * from "./TableLayout"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/TitleLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TitleLayout"; 2 | export * from "./TitleLayout"; 3 | -------------------------------------------------------------------------------- /developer/scenarios/single-server/hosts: -------------------------------------------------------------------------------- 1 | 2 | # FreeIPA webui hosts for 'single-server' 3 | 192.168.56.10 webui.ipa.test 4 | -------------------------------------------------------------------------------- /src/components/Form/IpaCheckboxes/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaCheckboxes"; 2 | export * from "./IpaCheckboxes"; 3 | -------------------------------------------------------------------------------- /public/assets/images/header-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/public/assets/images/header-logo.png -------------------------------------------------------------------------------- /public/assets/images/product-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/public/assets/images/product-name.png -------------------------------------------------------------------------------- /src/components/BulkSelectorPrep/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./BulkSelectorPrep"; 2 | export * from "./BulkSelectorPrep"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaCertificates/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaCertificates"; 2 | export * from "./IpaCertificates"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaNumberInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaNumberInput"; 2 | export * from "./IpaNumberInput"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaTextContent/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaTextContent"; 2 | export * from "./IpaTextContent"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaTextboxList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaTextboxList"; 2 | export * from "./IpaTextboxList"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaToggleGroup/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaToggleGroup"; 2 | export * from "./IpaToggleGroup"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/DropdownSearch/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DropdownSearch"; 2 | export * from "./DropdownSearch"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/DualListLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DualListLayout"; 2 | export * from "./DualListLayout"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/PasswordInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PasswordInput"; 2 | export * from "./PasswordInput"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/SidebarLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SidebarLayout"; 2 | export * from "./SidebarLayout"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/ToolbarLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ToolbarLayout"; 2 | export * from "./ToolbarLayout"; 3 | -------------------------------------------------------------------------------- /src/components/Form/DateTimeSelector/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DateTimeSelector"; 2 | export * from "./DateTimeSelector"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaDropdownSearch/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaDropdownSearch"; 2 | export * from "./IpaDropdownSearch"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaPasswordInput/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaPasswordInput"; 2 | export * from "./IpaPasswordInput"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaSshPublicKeys/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaSshPublicKeys"; 2 | export * from "./IpaSshPublicKeys"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/SearchInputLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchInputLayout"; 2 | export * from "./SearchInputLayout"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaTextInputFromList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaTextInputFromList"; 2 | export * from "./IpaTextInputFromList"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/SettingsTableLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SettingsTableLayout"; 2 | export * from "./SettingsTableLayout"; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cucumberautocomplete.steps": ["cypress/e2e/**/*.ts"], 3 | "cucumberautocomplete.strictGherkinCompletion": true 4 | } 5 | -------------------------------------------------------------------------------- /public/assets/images/login-screen-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeipa/freeipa-webui/HEAD/public/assets/images/login-screen-background.jpg -------------------------------------------------------------------------------- /src/components/layouts/PopoverWithIconLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PopoverWithIconLayout"; 2 | export * from "./PopoverWithIconLayout"; 3 | -------------------------------------------------------------------------------- /src/components/Form/IpaCertificateMappingData/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./IpaCertificateMappingData"; 2 | export * from "./IpaCertificateMappingData"; 3 | -------------------------------------------------------------------------------- /src/components/layouts/Skeleton/SkeletonOnTableLayout/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./SkeletonOnTableLayout"; 2 | export * from "./SkeletonOnTableLayout"; 3 | -------------------------------------------------------------------------------- /developer/scenarios/server-addc/hosts: -------------------------------------------------------------------------------- 1 | 2 | # FreeIPA webui hosts for 'server-addc' 3 | 192.168.57.250 dc.ad.ipa.test 4 | 192.168.57.100 webui.linux.ipa.test 5 | -------------------------------------------------------------------------------- /src/components/Form/PrincipalAliasMultiTextBox/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./PrincipalAliasMultiTextBox"; 2 | export * from "./PrincipalAliasMultiTextBox"; 3 | -------------------------------------------------------------------------------- /src/utils/constUtils.ts: -------------------------------------------------------------------------------- 1 | /* This file contains constants ready to use in any component */ 2 | 3 | // Selectors 4 | export const NO_SELECTION_OPTION = "No selection"; 5 | -------------------------------------------------------------------------------- /cypress/support/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | dataCy(value: string): Chainable; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cypress/e2e/common/alert.ts: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I should see {string} alert", (name: string) => { 4 | cy.dataCy(name).should("be.visible"); 5 | }); 6 | -------------------------------------------------------------------------------- /src/pages/HBACTest/HBACTest.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { EmptyPage } from "src/components/errors/PageErrors"; 3 | 4 | const HBACTest = () => { 5 | return ; 6 | }; 7 | 8 | export default HBACTest; 9 | -------------------------------------------------------------------------------- /doc/designs/index.rst: -------------------------------------------------------------------------------- 1 | FreeIPA WebUI documentation 2 | ============================ 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | webui-communication-layer.md 8 | login-via-certificates.md 9 | login-via-kerberos.md 10 | -------------------------------------------------------------------------------- /src/pages/SELinuxUserMaps/SELinuxUserMaps.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { EmptyPage } from "src/components/errors/PageErrors"; 3 | 4 | const SELinuxUserMaps = () => { 5 | return ; 6 | }; 7 | 8 | export default SELinuxUserMaps; 9 | -------------------------------------------------------------------------------- /developer/scenarios/no-dns/README.md: -------------------------------------------------------------------------------- 1 | # Scenario: no-DNS 2 | 3 | This scenario provides a single IPA server, without embedded DNS or KRA. 4 | It uses the same IP address as the default scenario. 5 | 6 | ## Hosts 7 | 8 | - webui: 9 | - Hostname: webui.ipa.test 10 | - IP address: 192.168.56.10 11 | - AD Trust 12 | -------------------------------------------------------------------------------- /cypress/e2e/automember/parameter_types.ts: -------------------------------------------------------------------------------- 1 | import { defineParameterType } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | export type EntryType = "inclusive" | "exclusive"; 4 | 5 | defineParameterType({ 6 | name: "EntryType", 7 | regexp: /inclusive|exclusive/, 8 | transformer: (s: string) => s as EntryType, 9 | }); 10 | -------------------------------------------------------------------------------- /developer/containerfiles/external-nodes: -------------------------------------------------------------------------------- 1 | ARG distro_image="fedora-minimal" 2 | ARG distro_tag="latest" 3 | 4 | FROM ${distro_image}:${distro_tag} 5 | 6 | ARG packages 7 | 8 | RUN dnf update -y 9 | RUN dnf -y install python3 python3-libdnf5 iproute hostname sudo vim ${packages} 10 | RUN dnf clean all 11 | 12 | STOPSIGNAL RTMIN+3 13 | -------------------------------------------------------------------------------- /cypress/e2e/common/modal.ts: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I should see {string} modal", (modalName: string) => { 4 | cy.dataCy(modalName).should("exist"); 5 | }); 6 | 7 | Then("I should not see {string} modal", (modalName: string) => { 8 | cy.dataCy(modalName).should("not.exist"); 9 | }); 10 | -------------------------------------------------------------------------------- /src/store/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; 2 | import type { RootState, AppDispatch } from "./store"; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = () => useDispatch(); 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/components/layouts/DataSpinner/DataSpinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Spinner } from "@patternfly/react-core"; 4 | 5 | function DataSpinner() { 6 | return ( 7 | 11 | ); 12 | } 13 | 14 | export default DataSpinner; 15 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (C) 2023 FreeIPA Contributors see COPYING for license 3 | # 4 | 5 | wheel 6 | 7 | sphinx > 3.0 8 | 9 | # convert markdown to rest 10 | # m2r2 fork is active in development and works with 11 | # newer sphinx versions 12 | m2r2 13 | 14 | ## dependencies 15 | lxml 16 | myst_parser 17 | 18 | # Sequence diagrams generator 19 | sphinxcontrib-plantuml 20 | pillow 21 | -------------------------------------------------------------------------------- /developer/scenarios/single-server/README.md: -------------------------------------------------------------------------------- 1 | # Scenario: single-server 2 | 3 | This is the default webui testing scenario, with a single IPA server. 4 | 5 | ## Hosts 6 | 7 | - webui: 8 | - Hostname: webui.ipa.test 9 | - IP address: 192.168.56.10 10 | - DNS, AD Trust and KRA. 11 | 12 | ## Notes 13 | 14 | This scenario may be started without `podman-compose` with: 15 | 16 | ``` 17 | dev-env.sh -s 18 | ``` 19 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. freeipa-webui documentation master file, created by 2 | sphinx-quickstart on Tue Mar 21 11:14:57 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to FreeIPA WebUI documentation! 7 | ========================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | designs/index.rst 14 | 15 | -------------------------------------------------------------------------------- /cypress/e2e/common/api/intercept.ts: -------------------------------------------------------------------------------- 1 | import { Given, Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Given("I intercept the {string} API call", (apiMethodName: string) => { 4 | cy.intercept({ method: "POST", url: "**/ipa/session/json" }, (req) => { 5 | if (req.body.method === apiMethodName) { 6 | req.alias = "apiCall"; 7 | } 8 | }); 9 | }); 10 | 11 | Then("I should wait for the API call to finish", () => { 12 | cy.wait("@apiCall"); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/button.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click on the {string} button", (button: string) => { 4 | cy.dataCy(button).click(); 5 | }); 6 | 7 | Then("I should see the {string} button is disabled", (button: string) => { 8 | cy.dataCy(button).should("be.disabled"); 9 | }); 10 | 11 | Then("I should see the {string} button is enabled", (button: string) => { 12 | cy.dataCy(button).should("be.enabled"); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/checkbox.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click on the {string} checkbox", (checkbox: string) => { 4 | cy.dataCy(checkbox).click(); 5 | }); 6 | 7 | Then("I should see the {string} checkbox is checked", (checkbox: string) => { 8 | cy.dataCy(checkbox).should("be.checked"); 9 | }); 10 | 11 | Then("I should see the {string} checkbox is unchecked", (checkbox: string) => { 12 | cy.dataCy(checkbox).should("not.be.checked"); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/kebab.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click on the {string} kebab menu", (kebab: string) => { 4 | cy.dataCy(kebab).click(); 5 | }); 6 | 7 | Then("I should see {string} kebab menu expanded", (kebab: string) => { 8 | cy.dataCy(kebab).should("have.attr", "aria-expanded", "true"); 9 | }); 10 | 11 | Then("I should see {string} kebab menu collapsed", (kebab: string) => { 12 | cy.dataCy(kebab).should("have.attr", "aria-expanded", "false"); 13 | }); 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Identity Management 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/utils/paramsUtils.ts: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router"; 2 | import { invariantNonNullable, TupleUnion } from "./invariants"; 3 | 4 | export type UidParams = { 5 | uid: string; 6 | }; 7 | 8 | export type CnParams = { 9 | cn: string; 10 | }; 11 | 12 | type Params = Record; 13 | 14 | export const useSafeParams = ( 15 | validation: TupleUnion 16 | ) => { 17 | const params = useParams(); 18 | invariantNonNullable(params, validation); 19 | return params; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/layouts/TitleLayout/TitleLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { expect, test } from "vitest"; 4 | // Component 5 | import TitleLayout from "./TitleLayout"; 6 | 7 | test("should display the TitleLayout with required props", () => { 8 | render(); 9 | expect(screen.getByText("Test Title")).toBeInTheDocument(); 10 | expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument(); 11 | }); 12 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/tab.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click on the {string} tab", (tab: string) => { 4 | cy.dataCy(tab).click(); 5 | }); 6 | 7 | Then("I should not see {string} tab", (tab: string) => { 8 | cy.dataCy(tab).should("not.exist"); 9 | }); 10 | 11 | Then("I should see {string} tab", (tab: string) => { 12 | cy.dataCy(tab).should("exist"); 13 | }); 14 | 15 | Then("I should see {string} tab selected", (tab: string) => { 16 | cy.dataCy(tab).should("have.attr", "aria-selected", "true"); 17 | }); 18 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/toggle_button.ts: -------------------------------------------------------------------------------- 1 | import { Then } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | Then("I should see the {string} toggle button is pressed", (button: string) => { 4 | cy.get("[data-cy=" + button + "] button").should( 5 | "have.attr", 6 | "aria-pressed", 7 | "true" 8 | ); 9 | }); 10 | 11 | Then( 12 | "I should see the {string} toggle button is not pressed", 13 | (button: string) => { 14 | cy.get("[data-cy=" + button + "] button").should( 15 | "have.attr", 16 | "aria-pressed", 17 | "false" 18 | ); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /src/components/layouts/TabLayout/TabLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from "@testing-library/react"; 3 | import { expect, test } from "vitest"; 4 | // Component 5 | import TabLayout from "./TabLayout"; 6 | 7 | const testTabs = [ 8 |
Test Tab
, 9 |
Test Tab
, 10 |
Test Tab
, 11 | ]; 12 | 13 | test("should display the TabLayout with required props", () => { 14 | render({testTabs}); 15 | expect(screen.getAllByText("Test Tab")).toHaveLength(3); 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/radiobutton.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When("I click on the {string} radio button", (radioButton: string) => { 4 | cy.dataCy(radioButton).click(); 5 | }); 6 | 7 | Then( 8 | "I should see the {string} radio button is selected", 9 | (radioButton: string) => { 10 | cy.dataCy(radioButton).should("be.checked"); 11 | } 12 | ); 13 | 14 | Then( 15 | "I should see the {string} radio button is not selected", 16 | (radioButton: string) => { 17 | cy.dataCy(radioButton).should("not.be.checked"); 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /src/components/layouts/DataSpinner/DataSpinner.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen, cleanup } from "@testing-library/react"; 3 | import { describe, expect, it, afterEach } from "vitest"; 4 | // component 5 | import DataSpinner from "./DataSpinner"; 6 | 7 | describe("DataSpinner Component", () => { 8 | afterEach(() => { 9 | cleanup(); 10 | }); 11 | 12 | it("renders the spinner with default props", () => { 13 | render(); 14 | const spinner = screen.getByLabelText("Loading Data..."); 15 | expect(spinner).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | /cypress/screenshots 11 | /cypress/videos 12 | /cypress/downloads 13 | 14 | # production 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .vagrant/ 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # doc 30 | /doc/_build 31 | /doc/.venv 32 | !/doc/Makefile 33 | 34 | # Make 35 | Makefile.in 36 | Makefile -------------------------------------------------------------------------------- /cypress/e2e/login/otp.ts: -------------------------------------------------------------------------------- 1 | import { TOTP } from "otpauth"; 2 | 3 | let totp: TOTP; 4 | 5 | export const extractOTPSecret = (qrCodeLink: string) => { 6 | return qrCodeLink.split("secret=")[1]?.split("&")[0]; 7 | }; 8 | 9 | export const generateOTP = (otp?: string) => { 10 | if (otp) { 11 | totp = new TOTP({ 12 | secret: otp, 13 | // Increase in case of flakiness 14 | period: 5, 15 | }); 16 | } 17 | 18 | if (!totp) { 19 | throw new Error("No token provider, secret has not been provided."); 20 | } 21 | 22 | return totp.generate(); 23 | }; 24 | 25 | export const getTokenPeriodInMs = () => { 26 | return totp.period * 1000; 27 | }; 28 | -------------------------------------------------------------------------------- /cypress/e2e/common/navigation.ts: -------------------------------------------------------------------------------- 1 | import { When, Then, Given } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | export const navigateTo = (handle: string) => { 4 | cy.visit(Cypress.env("BASE_URL") + "/" + handle); 5 | cy.location("pathname").should("match", new RegExp(`.*${handle}$`)); 6 | }; 7 | 8 | Given("I am on {string} page", (handle: string) => { 9 | navigateTo(handle); 10 | }); 11 | 12 | When("I navigate to {string} page", (handle: string) => { 13 | cy.visit(Cypress.env("BASE_URL") + "/" + handle); 14 | }); 15 | 16 | Then("I should be on {string} page", (handle: string) => { 17 | cy.location("pathname").should("match", new RegExp(`.*${handle}$`)); 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/modals/DnsZones/dnsLabels.ts: -------------------------------------------------------------------------------- 1 | // Error message 2 | export const SKIP_OVERLAP_CHECK_MESSAGE = 3 | "Force DNS forward zone creation even if it will overlap with an existing forward zone."; 4 | 5 | export const REVERSE_ZONE_IP_ERROR_MESSAGE = 6 | "Not a valid network address (examples: 2001:db8::/64, 192.0.2.0/24)"; 7 | 8 | const REVERSE_ZONE_IP_REGEX = 9 | /^(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9]{1,3}\.){3}[0-9]{1,3}\/\d{1,2})$/; 10 | 11 | export const isValidReverseZoneIp = (value: string): boolean => { 12 | // Regular expression to validate format. Examples: 2001:db8::/64, 192.0.2.0/24, etc. 13 | return REVERSE_ZONE_IP_REGEX.test(value); 14 | }; 15 | -------------------------------------------------------------------------------- /cypress/e2e/dns/parameter_types.ts: -------------------------------------------------------------------------------- 1 | import { defineParameterType } from "@badeball/cypress-cucumber-preprocessor"; 2 | import { DnsRecordType } from "src/utils/datatypes/globalDataTypes"; 3 | 4 | const dnsRecordTypes: DnsRecordType[] = [ 5 | "A", 6 | "AAAA", 7 | "A6", 8 | "AFSDB", 9 | "CERT", 10 | "CNAME", 11 | "DNAME", 12 | "DS", 13 | "DLV", 14 | "KX", 15 | "LOC", 16 | "MX", 17 | "NAPTR", 18 | "NS", 19 | "PTR", 20 | "SRV", 21 | "SSHFP", 22 | "TLSA", 23 | "TXT", 24 | "URI", 25 | ]; 26 | 27 | defineParameterType({ 28 | name: "DnsRecordType", 29 | regexp: new RegExp(dnsRecordTypes.join("|")), 30 | transformer: (s: string) => s as DnsRecordType, 31 | }); 32 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: doc/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | # formats: 20 | # - pdf 21 | 22 | # Optionally declare the Python requirements required to build your docs 23 | python: 24 | install: 25 | - requirements: doc/requirements.txt 26 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /cypress/e2e/common/settings_table.ts: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | import { checkEntry } from "./data_tables"; 3 | 4 | When( 5 | "I select entry {string} in the settings data table {string}", 6 | (name: string, table: string) => { 7 | findEntryInTable(name, table); 8 | checkEntry(name); 9 | } 10 | ); 11 | 12 | export const findEntryInTable = (name: string, table: string) => { 13 | cy.dataCy("search-" + table) 14 | .find("input") 15 | .clear(); 16 | cy.dataCy("search-" + table) 17 | .find("input") 18 | .should("have.value", ""); 19 | cy.dataCy("search-" + table) 20 | .find("input") 21 | .type(name); 22 | cy.dataCy("search-" + table) 23 | .find("input") 24 | .should("have.value", name); 25 | }; 26 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | 18 | import { addCustomCommand } from "cy-verify-downloads"; 19 | 20 | addCustomCommand(); 21 | 22 | import "./commands"; 23 | -------------------------------------------------------------------------------- /developer/containerfiles/centos: -------------------------------------------------------------------------------- 1 | ARG distro_tag=stream10 2 | FROM quay.io/centos/centos:${distro_tag} 3 | 4 | ENV container=podman 5 | 6 | RUN dnf update -y 7 | 8 | RUN dnf --assumeyes install \ 9 | sudo \ 10 | bash \ 11 | systemd \ 12 | procps-ng \ 13 | hostname \ 14 | iputils \ 15 | bind-utils \ 16 | iproute \ 17 | `# Useful tools` \ 18 | vim \ 19 | `# Packages required by dev-evn.sh script` \ 20 | nc \ 21 | nodejs \ 22 | `# FreeIPA packages` \ 23 | ipa-server \ 24 | python3-libselinux \ 25 | ipa-server-dns \ 26 | ipa-server-trust-ad \ 27 | firewalld 28 | 29 | RUN dnf clean all && rm -rf "/var/cache/dnf/" 30 | 31 | STOPSIGNAL RTMIN+3 32 | 33 | VOLUME [/webui] 34 | 35 | WORKDIR /webui 36 | 37 | CMD ["/usr/sbin/init"] 38 | -------------------------------------------------------------------------------- /src/components/layouts/HelpTextWithIconLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { Button } from "@patternfly/react-core"; 4 | // Icons 5 | import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons"; 6 | 7 | interface PropsToHelpTextLayout { 8 | textContent: string; 9 | icon?: JSX.Element; 10 | onClick?: React.MouseEventHandler; 11 | } 12 | 13 | const HelpTextWithIconLayout = (props: PropsToHelpTextLayout) => { 14 | return ( 15 | 23 | ); 24 | }; 25 | 26 | export default HelpTextWithIconLayout; 27 | -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | rule_settings: 3 | disable: [use-object-destructuring, use-braces] 4 | 5 | rules: 6 | - id: remove-debug-logs 7 | pattern: console. ...(...) 8 | description: Remove debug logs 9 | replacement: "" 10 | tests: 11 | - match: console.log("test", object); 12 | - match: console.log("Test"); 13 | - match: console.error("Error"); 14 | - match: console.warn("Warn"); 15 | - match: console.debug("Debug"); 16 | - no-match: send(console); 17 | 18 | - id: prefer-redux 19 | pattern: fetch(...) 20 | replacement: build.query or build.mutation 21 | description: Avoid using fetch, instead use reduxjs 22 | tests: 23 | - match: fetch(test); 24 | - match: fetch(test).then(console.log); 25 | - no-match: send(fetch) 26 | -------------------------------------------------------------------------------- /src/components/errors/GlobalErrors.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { Content } from "@patternfly/react-core"; 4 | // Hooks 5 | import { ApiError } from "src/hooks/useApiError"; 6 | // Utils 7 | import { apiErrorToJsXError } from "src/utils/utils"; 8 | 9 | interface GlobalErrorProps { 10 | errors: ApiError[]; 11 | } 12 | 13 | const GlobalErrors = ({ errors }: GlobalErrorProps) => { 14 | return ( 15 |
16 | An error has occurred 17 | <> 18 | {errors.map((error: ApiError, idx: number) => { 19 | return apiErrorToJsXError(error.error, error.context, idx.toString()); 20 | })} 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default GlobalErrors; 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: prettier 6 | name: Check code formatting 7 | language: node 8 | entry: npx 9 | args: ["prettier", "--check"] 10 | exclude: | 11 | \.feature$ 12 | files: \.(css|tsx?|html|js|json|yaml|md)$ 13 | - repo: https://github.com/koalaman/shellcheck-precommit 14 | rev: v0.11.0 15 | hooks: 16 | - id: shellcheck 17 | args: 18 | [ 19 | "-x", 20 | "-a", 21 | "-o", 22 | "all", 23 | "-e", 24 | "SC2292", 25 | "-e", 26 | "SC2310", 27 | "-e", 28 | "SC2311", 29 | "-P", 30 | "./developer", 31 | ] 32 | files: \.sh$ 33 | -------------------------------------------------------------------------------- /developer/chrome_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file should be sourced from open-browser.sh 4 | 5 | # shellcheck disable=SC2154 6 | CONTAINER_PROFILE_DIR="${config_dir}/chrome/${profile_name}" 7 | 8 | declare -a browser_cmd 9 | if quiet command -v google-chrome && [ "${FORCE_FLATPAK:-"0"}" -ne "1" ] 10 | then 11 | browser_cmd=("google-chrome" "--user-data-dir=${CONTAINER_PROFILE_DIR}" "--no-sandbox" "--new-window") 12 | else 13 | [ "${FORCE_FLATPAK:-"0"}" -ne "1" ] && die "Chrome flatpak is not supported." 14 | die "Cannot find google-chorome executable." 15 | fi 16 | export browser_cmd 17 | 18 | remove_profile() { 19 | [ -d "${CONTAINER_PROFILE_DIR}" ] && rm -rf "${CONTAINER_PROFILE_DIR}" 20 | } 21 | 22 | create_profile() { 23 | [ -d "${CONTAINER_PROFILE_DIR}" ] || mkdir -p "${CONTAINER_PROFILE_DIR}" 24 | } 25 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/select.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | When( 4 | "I select {string} option in the {string} selector", 5 | (option: string, selector: string) => { 6 | selectOption(option, selector); 7 | } 8 | ); 9 | 10 | Then( 11 | "I should see {string} option in the {string} selector", 12 | (option: string, selector: string) => { 13 | isOptionSelected(option, selector); 14 | } 15 | ); 16 | 17 | export const selectOption = (option: string, selector: string) => { 18 | cy.dataCy(selector + "-toggle").click(); 19 | cy.dataCy(selector + "-toggle").should("have.attr", "aria-expanded", "true"); 20 | cy.dataCy(selector + "-" + option).click(); 21 | }; 22 | 23 | export const isOptionSelected = (option: string, selector: string) => { 24 | cy.dataCy(selector + "-toggle").contains(option); 25 | }; 26 | -------------------------------------------------------------------------------- /cypress/e2e/sudo_rules/sudo_rules.ts: -------------------------------------------------------------------------------- 1 | import { Given } from "@badeball/cypress-cucumber-preprocessor"; 2 | import { loginAsAdmin, logout } from "../common/authentication"; 3 | import { selectEntry } from "../common/data_tables"; 4 | import { navigateTo } from "../common/navigation"; 5 | 6 | const isDisabled = (name: string) => { 7 | cy.get("tr[id='" + name + "'] td[data-label=Status]").contains("Disabled"); 8 | }; 9 | 10 | Given("I disable sudo rule {string}", (ruleName: string) => { 11 | loginAsAdmin(); 12 | navigateTo("sudo-rules"); 13 | selectEntry(ruleName); 14 | cy.dataCy("sudo-rules-button-disable").click(); 15 | cy.dataCy("disable-enable-sudo-rules-modal").should("be.visible"); 16 | cy.dataCy("modal-button-disable").click(); 17 | cy.dataCy("disable-enable-sudo-rules-modal").should("not.exist"); 18 | isDisabled(ruleName); 19 | logout(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/errors/PageErrors.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Icon, Title } from "@patternfly/react-core"; 3 | import { ExclamationTriangleIcon } from "@patternfly/react-icons"; 4 | 5 | const ErrorPage = (text: string) => { 6 | return ( 7 | <> 8 |
9 | 10 | 11 | 12 |
13 |
14 | 15 | {text} 16 | 17 |
18 | 19 | ); 20 | }; 21 | 22 | export const EmptyPage = () => { 23 | return ErrorPage("This page is under construction"); 24 | }; 25 | 26 | export const NotFound = () => { 27 | return ErrorPage("404: Page not found"); 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/testAlertsUtils.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from "react"; 2 | import { render } from "@testing-library/react"; 3 | import type { RenderOptions } from "@testing-library/react"; 4 | import { Provider } from "react-redux"; 5 | 6 | import ManagedAlerts from "src/components/ManagedAlerts"; 7 | import { setupStore } from "src/store/store"; 8 | 9 | export function renderWithAlerts( 10 | ui: React.ReactElement, 11 | renderOptions: RenderOptions = {} 12 | ) { 13 | const store = setupStore(); 14 | 15 | const Wrapper = ({ children }: PropsWithChildren) => ( 16 | 17 | 18 | {children} 19 | 20 | ); 21 | 22 | // Return an object with the store and all of RTL's query functions 23 | return { 24 | store, 25 | ...render(ui, { wrapper: Wrapper, ...renderOptions }), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /cypress/e2e/common/members_table.ts: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | import { checkEntry } from "./data_tables"; 3 | 4 | export const searchForMembersEntry = (name: string) => { 5 | cy.dataCy("search").find("input").clear(); 6 | cy.dataCy("search").find("input").should("have.value", ""); 7 | cy.dataCy("search").find("input").type(name); 8 | cy.dataCy("search").find("input").should("have.value", name); 9 | 10 | cy.dataCy("search").find("button[type='submit']").click(); 11 | }; 12 | 13 | export const selectMembersEntry = (name: string) => { 14 | searchForMembersEntry(name); 15 | checkEntry(name); 16 | }; 17 | 18 | When("I search for {string} in the members table", (name: string) => { 19 | searchForMembersEntry(name); 20 | }); 21 | 22 | When("I select entry {string} in the members table", (name: string) => { 23 | selectMembersEntry(name); 24 | }); 25 | -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/knip@5/schema.json", 3 | "entry": [ 4 | "src/main.tsx!", 5 | "cypress/e2e/**/*.ts", 6 | "!cypress/e2e/common/**/*.ts" 7 | ], 8 | "project": [ 9 | "src/**/*.ts!", 10 | "src/**/*.tsx!", 11 | "cypress/e2e/**/*.ts", 12 | "!cypress/e2e/common/**/*.ts" 13 | ], 14 | "includeEntryExports": true, 15 | "rules": { 16 | "files": "error", 17 | "dependencies": "off", 18 | "unlisted": "off", 19 | "binaries": "error", 20 | "unresolved": "off", 21 | "exports": "error", 22 | "types": "error", 23 | "nsExports": "error", 24 | "nsTypes": "error", 25 | "enumMembers": "error", 26 | "classMembers": "error", 27 | "duplicates": "error" 28 | }, 29 | "ignore": [ 30 | "src/utils/*.ts", 31 | "src/utils/*.tsx", 32 | "src/utils/**/*.ts", 33 | "src/utils/**/*.tsx" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /developer/containerfiles/fedora: -------------------------------------------------------------------------------- 1 | ARG distro_image=fedora 2 | ARG distro_tag=latest 3 | FROM ${distro_image}:${distro_tag} 4 | 5 | ENV container=podman 6 | 7 | RUN dnf update -y 8 | 9 | RUN dnf install -y \ 10 | sudo \ 11 | bash \ 12 | systemd \ 13 | procps-ng \ 14 | hostname \ 15 | iputils \ 16 | bind-utils \ 17 | iproute \ 18 | python3-libdnf5 \ 19 | `# Useful tools` \ 20 | vim \ 21 | `# Packages required by dev-evn.sh script` \ 22 | nc \ 23 | nodejs \ 24 | `# FreeIPA packages` \ 25 | freeipa-server \ 26 | python3-libselinux \ 27 | freeipa-server-dns \ 28 | freeipa-server-trust-ad \ 29 | freeipa-server-encrypted-dns \ 30 | firewalld \ 31 | ; 32 | 33 | RUN dnf clean all && rm -rf "/var/cache/dnf/" 34 | 35 | STOPSIGNAL RTMIN+3 36 | 37 | VOLUME [/webui] 38 | 39 | WORKDIR /webui 40 | 41 | CMD ["/usr/sbin/init"] 42 | -------------------------------------------------------------------------------- /cypress/e2e/dns/dns_servers_settings.ts: -------------------------------------------------------------------------------- 1 | import { When } from "@badeball/cypress-cucumber-preprocessor"; 2 | import { typeInTextbox } from "../common/ui/textbox"; 3 | import { 4 | addElementToTextboxList, 5 | removeElementFromTextboxList, 6 | } from "../common/ui/textbox_list"; 7 | 8 | When("I change SOA name to {string}", (soaName: string) => { 9 | typeInTextbox("dns-servers-tab-settings-textbox-idnssoamname", soaName); 10 | }); 11 | 12 | When("I add new forwarder to {string}", (forwarder: string) => { 13 | addElementToTextboxList( 14 | "dns-servers-tab-settings-textbox-idnsforwarders-button-add", 15 | "dns-servers-tab-settings-textbox-idnsforwarders", 16 | forwarder 17 | ); 18 | }); 19 | 20 | When("I remove forwarder {string}", (forwarder: string) => { 21 | removeElementFromTextboxList( 22 | forwarder, 23 | "dns-servers-tab-settings-textbox-idnsforwarders" 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /cypress/e2e/common/ui/textbox.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@badeball/cypress-cucumber-preprocessor"; 2 | 3 | export const typeInTextbox = (textbox: string, text: string) => { 4 | cy.dataCy(textbox).clear(); 5 | cy.dataCy(textbox).should("have.value", ""); 6 | cy.dataCy(textbox).type(text); 7 | }; 8 | 9 | When( 10 | "I type in the {string} textbox text {string}", 11 | (textbox: string, text: string) => { 12 | typeInTextbox(textbox, text); 13 | } 14 | ); 15 | 16 | Then( 17 | "I should see {string} in the {string} textbox", 18 | (text: string, textbox: string) => { 19 | cy.dataCy(textbox).should("have.value", text); 20 | } 21 | ); 22 | 23 | When("I clear the {string} textbox", (textbox: string) => { 24 | cy.dataCy(textbox).clear(); 25 | }); 26 | 27 | Then("I should see the {string} textbox is empty", (textbox: string) => { 28 | cy.dataCy(textbox).should("have.value", ""); 29 | }); 30 | -------------------------------------------------------------------------------- /cypress/e2e/subids/subids.feature: -------------------------------------------------------------------------------- 1 | Feature: Subordinate IDs manipulation 2 | Create subordinate IDs 3 | 4 | @seed 5 | Scenario: Prep: Create new user 6 | Given User "testuser" "Test" "User" exists and is using password "Secret123" 7 | 8 | @test 9 | Scenario: Add a new subordinate ID 10 | Given I am logged in as admin 11 | And I am on "subordinate-ids" page 12 | 13 | When I click on the "subids-button-add" button 14 | Then I should see "add-subid-modal" modal 15 | 16 | When I select "testuser" option in the "modal-simple-owner-select" selector 17 | Then I should see "testuser" option in the "modal-simple-owner-select" selector 18 | 19 | When I click on the "modal-button-add" button 20 | Then I should not see "add-subid-modal" modal 21 | And I should see "add-subid-success" alert 22 | 23 | @cleanup 24 | Scenario: Cleanup: Delete a user 25 | Given I delete user "testuser" 26 | -------------------------------------------------------------------------------- /src/utils/rolesUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { Role } from "src/utils/datatypes/globalDataTypes"; 3 | // Utils 4 | import { convertApiObj } from "./ipaObjectUtils"; 5 | 6 | const simpleValues = new Set(["cn", "description", "dn"]); 7 | const dateValues = new Set([]); 8 | 9 | export function apiToRole(apiRecord: Record): Role { 10 | const converted = convertApiObj( 11 | apiRecord, 12 | simpleValues, 13 | dateValues 14 | ) as Partial; 15 | return partialRoleToRole(converted) as Role; 16 | } 17 | 18 | export function partialRoleToRole(partialGroup: Partial): Role { 19 | return { 20 | ...createEmptyRole(), 21 | ...partialGroup, 22 | }; 23 | } 24 | 25 | // Get empty User object initialized with default values 26 | export function createEmptyRole(): Role { 27 | const group: Role = { 28 | cn: "", 29 | description: "", 30 | dn: "", 31 | }; 32 | 33 | return group; 34 | } 35 | -------------------------------------------------------------------------------- /cypress/e2e/hostgroups/hostgroup.feature: -------------------------------------------------------------------------------- 1 | Feature: Hostgroup management 2 | 3 | @test 4 | Scenario: Create new host group 5 | Given I am logged in as admin 6 | And I am on "host-groups" page 7 | 8 | When I create hostgroup "my_automember_hostgroup" with description "test" 9 | Then I should see hostgroup "my_automember_hostgroup" in the data table 10 | 11 | @cleanup 12 | Scenario: Delete host group 13 | Given I delete hostgroup "my_automember_hostgroup" 14 | 15 | @seed 16 | Scenario: Create new host group 17 | Given Hostgroup "my_automember_hostgroup" with description "test" exists 18 | 19 | @test 20 | Scenario: Delete host group 21 | Given I am logged in as admin 22 | And I am on "host-groups" page 23 | 24 | When I try to delete hostgroup "my_automember_hostgroup" 25 | Then I should not see hostgroup "my_automember_hostgroup" in the data table 26 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /src/components/modals/ErrorModal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { 4 | Content, 5 | Modal, 6 | ModalBody, 7 | ModalFooter, 8 | ModalHeader, 9 | } from "@patternfly/react-core"; 10 | 11 | interface PropsToErrorModal { 12 | dataCy: string; 13 | title: string; 14 | isOpen: boolean; 15 | onClose: () => void; 16 | actions: JSX.Element[]; 17 | errorMessage: string; 18 | } 19 | 20 | const ErrorModal = (props: PropsToErrorModal) => { 21 | return ( 22 | 28 | 29 | 30 | {props.errorMessage} 31 | 32 | {props.actions} 33 | 34 | ); 35 | }; 36 | 37 | export default ErrorModal; 38 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: "25 12 * * *" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | stale: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: "This issue has not received any attention in 365 days." 25 | stale-pr-message: "This PR has not received any attention in 60 days." 26 | days-before-issue-stale: 365 27 | stale-issue-label: "stale" 28 | stale-pr-label: "stale" 29 | -------------------------------------------------------------------------------- /developer/scenarios/single-server/compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: single-server 3 | services: 4 | webui: 5 | container_name: webui 6 | restart: "no" 7 | cap_add: 8 | - SYS_ADMIN 9 | - DAC_READ_SEARCH 10 | security_opt: 11 | - label=disable 12 | hostname: webui.ipa.test 13 | extra_hosts: 14 | - webui.ipa.test:192.168.56.10 15 | ports: 16 | - "5173:5173/tcp" 17 | networks: 18 | ipanet: 19 | ipv4_address: 192.168.56.10 20 | image: quay.io/ansible-freeipa/webui-dev:latest 21 | build: 22 | # You may set the desired distro/version setting: 23 | # args: {distro_image: fedora, distro_tag: latest} 24 | context: "${SCRIPT_DIR}/containerfiles" 25 | dockerfile: fedora 26 | volumes: 27 | - "${REPO_DIR}:/webui:Z" 28 | networks: 29 | ipanet: 30 | name: webui-ipa-single-server 31 | driver: bridge 32 | ipam: 33 | config: 34 | - subnet: 192.168.56.0/24 35 | -------------------------------------------------------------------------------- /developer/scenarios/no-dns/compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: no-dns 3 | services: 4 | webui: 5 | container_name: webui 6 | restart: "no" 7 | cap_add: 8 | - SYS_ADMIN 9 | - DAC_READ_SEARCH 10 | security_opt: 11 | - label=disable 12 | hostname: webui.ipa.test 13 | extra_hosts: 14 | - webui.ipa.test:192.168.56.10 15 | ports: 16 | - "5173:5173/tcp" 17 | networks: 18 | ipanet: 19 | ipv4_address: 192.168.56.10 20 | image: localhost/webui-no-dns:latest 21 | build: 22 | # You may set the desired distro/version setting: 23 | # args: {distro_image: fedora, distro_tag: latest} 24 | context: "${SCRIPT_DIR}/containerfiles" 25 | dockerfile: fedora 26 | volumes: 27 | - "${REPO_DIR}:/webui:Z" 28 | networks: 29 | ipanet: 30 | x-podman.disable_dns: true 31 | name: webui-ipa-single-server 32 | driver: bridge 33 | ipam: 34 | config: 35 | - subnet: 192.168.56.0/24 36 | -------------------------------------------------------------------------------- /src/components/layouts/InformationModalLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { 4 | Modal, 5 | ModalHeader, 6 | ModalBody, 7 | ModalFooter, 8 | } from "@patternfly/react-core"; 9 | 10 | interface PropsToModalLayout { 11 | dataCy: string; 12 | isOpen: boolean; 13 | onClose: () => void; 14 | actions: JSX.Element[]; 15 | title: string; 16 | variant?: "default" | "small" | "medium" | "large"; 17 | content: React.ReactNode; 18 | } 19 | 20 | const InformationModalLayout = (props: PropsToModalLayout) => { 21 | return ( 22 | 28 | 29 | {props.content} 30 | {props.actions} 31 | 32 | ); 33 | }; 34 | 35 | export default InformationModalLayout; 36 | -------------------------------------------------------------------------------- /src/utils/configurationSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppSelector } from "src/store/hooks"; 3 | 4 | /** 5 | * Custom hook containing modern WebUI configuration settings 6 | * This can be based on: 7 | * - metadata 8 | * - Redux data 9 | * - pre-defined parameters 10 | * The hook is flexible and more parameters can be added to be 11 | * consumed in other components 12 | */ 13 | 14 | export interface ConfigurationSettings { 15 | dnsIsEnabled: boolean; 16 | // ... other configuration settings here 17 | } 18 | 19 | const useConfigurationSettings = () => { 20 | const dnsIsEnabled = useAppSelector((state) => state.global.dnsIsEnabled); 21 | 22 | const configurationSettings: ConfigurationSettings = React.useMemo( 23 | () => ({ 24 | dnsIsEnabled: dnsIsEnabled, 25 | // ... other configuration settings here 26 | }), 27 | [dnsIsEnabled] 28 | ); 29 | 30 | return configurationSettings; 31 | }; 32 | 33 | export { useConfigurationSettings }; 34 | -------------------------------------------------------------------------------- /src/store/Global/auth-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | 3 | interface onLoginPayload { 4 | loggedInUser: string; 5 | error: string | null; 6 | } 7 | 8 | interface AuthState { 9 | isUserLoggedIn: boolean; 10 | user: string | null; 11 | error: string | null; 12 | } 13 | 14 | const initialState: AuthState = { 15 | isUserLoggedIn: false, 16 | user: null, 17 | error: null, 18 | }; 19 | 20 | const authSlice = createSlice({ 21 | name: "auth", 22 | initialState, 23 | reducers: { 24 | setIsLogin: (state, action: PayloadAction) => { 25 | state.isUserLoggedIn = true; 26 | state.user = action.payload.loggedInUser; 27 | state.error = action.payload.error; 28 | }, 29 | setIsLogout: (state) => { 30 | state.isUserLoggedIn = false; 31 | state.user = null; 32 | state.error = null; 33 | }, 34 | }, 35 | }); 36 | 37 | export const { setIsLogin, setIsLogout } = authSlice.actions; 38 | export default authSlice.reducer; 39 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | NULL = 2 | 3 | appdir = $(IPA_DATA_DIR)/modern-ui 4 | licensedir = $(DESTDIR)$(datarootdir)/licenses/$(PACKAGE_NAME)-server-common/modern-ui 5 | 6 | EXTRA_DIST = \ 7 | $(NULL) 8 | 9 | install-exec-local: 10 | ## We don't want to run this code in prci, prci runs those commands before make install 11 | test -d "dist" || npm clean-install 12 | test -d "dist" || ( npm run build && rm -rf node_modules && npm clean-install --omit=dev ) 13 | mkdir -p "$(DESTDIR)$(appdir)" 14 | cp -p "dist/index.html" "$(DESTDIR)$(appdir)/index.html" 15 | cp -p "dist/favicon.ico" "$(DESTDIR)$(appdir)/favicon.ico" 16 | mkdir -p "$(licensedir)" 17 | cp -p "dist/COPYING" "$(licensedir)/COPYING" 18 | cp -rp "dist/assets" "$(DESTDIR)$(appdir)/assets" 19 | 20 | dist-hook: 21 | npm clean-install 22 | ## We need to build, but we only want the distributable dependencies in the .src.rpm 23 | npm run build && rm -rf node_modules && npm clean-install --omit=dev 24 | cp -r "." "$(distdir)/" 25 | 26 | clean-local: 27 | rm -rf node_modules dist 28 | -------------------------------------------------------------------------------- /src/store/Global/alerts-slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { AlertProps } from "@patternfly/react-core"; 3 | 4 | export interface AlertInfo { 5 | name: string; 6 | title: React.ReactNode; 7 | variant: AlertProps["variant"]; 8 | } 9 | 10 | const alertsSlice = createSlice({ 11 | name: "alerts", 12 | initialState: [] as AlertInfo[], 13 | reducers: { 14 | addAlert: (state, action: PayloadAction) => { 15 | state.push(action.payload); 16 | }, 17 | removeAlert: (state, action: PayloadAction<{ name: string }>) => { 18 | const index = state.findIndex( 19 | (alert) => alert.name === action.payload.name 20 | ); 21 | 22 | if (index !== -1) { 23 | state.splice(index, 1); 24 | } 25 | }, 26 | removeAllAlerts: (state) => { 27 | state.length = 0; 28 | }, 29 | }, 30 | }); 31 | 32 | export const { addAlert, removeAlert, removeAllAlerts } = alertsSlice.actions; 33 | 34 | export default alertsSlice.reducer; 35 | -------------------------------------------------------------------------------- /src/utils/sudoCmdsUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { SudoCmd } from "src/utils/datatypes/globalDataTypes"; 3 | // Utils 4 | import { convertApiObj } from "./ipaObjectUtils"; 5 | 6 | const simpleValues = new Set(["sudocmd", "dn", "description"]); 7 | const dateValues = new Set([]); 8 | 9 | export function apiToSudoCmd(apiRecord: Record): SudoCmd { 10 | const converted = convertApiObj( 11 | apiRecord, 12 | simpleValues, 13 | dateValues 14 | ) as Partial; 15 | return partialSudoCmdToSudoCmd(converted) as SudoCmd; 16 | } 17 | 18 | export function partialSudoCmdToSudoCmd( 19 | partialSudoCmd: Partial 20 | ): SudoCmd { 21 | return { 22 | ...createEmptySudoCmd(), 23 | ...partialSudoCmd, 24 | }; 25 | } 26 | 27 | // Get empty User object initialized with default values 28 | export function createEmptySudoCmd(): SudoCmd { 29 | const sudoCmd: SudoCmd = { 30 | sudocmd: "", 31 | dn: "", 32 | description: "", 33 | memberof_sudocmdgroup: [], 34 | }; 35 | 36 | return sudoCmd; 37 | } 38 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.ts shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | // 27 | 28 | Cypress.Commands.add("dataCy", (value: string) => { 29 | return cy.get(`[data-cy='${value}']`); 30 | }); 31 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./main.css"; 5 | // react router dom 6 | import { BrowserRouter } from "react-router"; 7 | // Redux 8 | import store from "./store/store"; 9 | import { Provider } from "react-redux"; 10 | // PatternFly utilities 11 | import "@patternfly/patternfly/utilities/Spacing/spacing.css"; 12 | import "@patternfly/patternfly/utilities/Text/text.css"; 13 | import "@patternfly/patternfly/utilities/Sizing/sizing.css"; 14 | import "@patternfly/patternfly/utilities/Display/display.css"; 15 | import "@patternfly/patternfly/utilities/Accessibility/accessibility.css"; 16 | // Navigation 17 | import { URL_PREFIX } from "./navigation/NavRoutes"; 18 | 19 | const root = ReactDOM.createRoot( 20 | document.getElementById("root") as HTMLElement 21 | ); 22 | 23 | root.render( 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /cypress/e2e/hbac/hbac_service/hbac_service_settings.feature: -------------------------------------------------------------------------------- 1 | Feature: HBAC service settings manipulation 2 | Modify a HBAC service 3 | 4 | @seed 5 | Scenario: Seed: Ensure service exists 6 | Given HBAC service "a_service_settings" exists 7 | 8 | @test 9 | Scenario: Set Description in settings 10 | Given I am logged in as admin 11 | And I am on "hbac-services/a_service_settings" page 12 | 13 | When I type in the "hbac-services-tab-settings-textbox-description" textbox text "test" 14 | Then I should see "test" in the "hbac-services-tab-settings-textbox-description" textbox 15 | And I should see the "hbac-services-tab-settings-button-save" button is enabled 16 | 17 | When I click on the "hbac-services-tab-settings-button-save" button 18 | Then I should see "save-success" alert 19 | 20 | When I am on "hbac-services" page 21 | Then I should see "a_service_settings" entry in the data table with attribute "Description" set to "test" 22 | 23 | @cleanup 24 | Scenario: Cleanup: Delete a service 25 | Given I delete service "a_service_settings" 26 | -------------------------------------------------------------------------------- /cypress/e2e/subids/subids_settings.feature: -------------------------------------------------------------------------------- 1 | Feature: Subordinate IDs - Settings page 2 | Modify Subodinate IDs settings 3 | 4 | @seed 5 | Scenario: Prep: Create new user 6 | Given User "testuser" "Test" "User" exists and is using password "Secret123" 7 | And subid for owner "testuser" exists 8 | 9 | # This test assumes that there is only one single subordinate ID in the table 10 | # because we don't know the ID of the subordinate ID 11 | @test 12 | Scenario: Set 'Description' field 13 | Given I am logged in as admin 14 | Given I am on "subordinate-ids" page 15 | 16 | When I click on the first subid 17 | Then I should be on the subid settings page 18 | 19 | When I type in the "subids-tab-settings-textbox-description" textbox text "test_user" 20 | Then I should see "test_user" in the "subids-tab-settings-textbox-description" textbox 21 | 22 | When I click on the "subids-tab-settings-button-save" button 23 | Then I should see "success" alert 24 | 25 | @cleanup 26 | Scenario: Cleanup: Delete user 27 | Given I delete user "testuser" 28 | -------------------------------------------------------------------------------- /developer/scenarios/no-dns/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | container: 3 | vars: 4 | ansible_connection: podman 5 | hosts: 6 | webui: 7 | ipaserver_domain: ipa.test 8 | ipaserver_realm: IPA.TEST 9 | ipaserver_hostname: webui.ipa.test 10 | ipaadmin_password: Secret123 11 | ipadm_password: Secret123 12 | ipaserver_setup_firewalld: false 13 | ipaserver_no_host_dns: true # Ease installation due to podman DNS 14 | ipaclient_no_ntp: false 15 | # Optional tools 16 | # DNS - to enable DNS set both to true 17 | ipaserver_setup_dns: false 18 | # ipaserver_auto_forwarders: true 19 | # KRA 20 | ipaserver_setup_kra: false 21 | # These ranges are required to run on a rootless container 22 | ipaserver_idstart: 60001 23 | ipaserver_idmax: 62000 24 | ipaserver_rid_base: 63000 25 | ipaserver_secondary_rid_base: 65000 26 | children: 27 | ipaserver: 28 | hosts: 29 | webui: 30 | ipa: 31 | hosts: 32 | webui: 33 | ipa_deployments: 34 | children: 35 | ipa: 36 | -------------------------------------------------------------------------------- /developer/scenarios/single-server/inventory.yml: -------------------------------------------------------------------------------- 1 | --- 2 | container: 3 | vars: 4 | ansible_connection: podman 5 | hosts: 6 | webui: 7 | ipaserver_domain: ipa.test 8 | ipaserver_realm: IPA.TEST 9 | ipaserver_hostname: webui.ipa.test 10 | ipaadmin_password: Secret123 11 | ipadm_password: Secret123 12 | ipaserver_setup_firewalld: false 13 | ipaserver_no_host_dns: true # Ease installation due to podman DNS 14 | ipaclient_no_ntp: false 15 | # Optional tools 16 | # DNS - to disable DNS set both to false 17 | ipaserver_setup_dns: true 18 | ipaserver_auto_forwarders: true 19 | # KRA 20 | ipaserver_setup_kra: true 21 | # These ranges are required to run on a rootless container 22 | ipaserver_idstart: 60001 23 | ipaserver_idmax: 62000 24 | ipaserver_rid_base: 63000 25 | ipaserver_secondary_rid_base: 65000 26 | children: 27 | ipaserver: 28 | hosts: 29 | webui: 30 | ipa: 31 | hosts: 32 | webui: 33 | ipa_deployments: 34 | children: 35 | ipa: 36 | -------------------------------------------------------------------------------- /src/utils/hbacServicesUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { HBACService } from "src/utils/datatypes/globalDataTypes"; 3 | // Utils 4 | import { convertApiObj } from "./ipaObjectUtils"; 5 | 6 | const simpleValues = new Set(["cn", "dn"]); 7 | const dateValues = new Set([]); 8 | 9 | export function apiToHBACService( 10 | apiRecord: Record 11 | ): HBACService { 12 | const converted = convertApiObj( 13 | apiRecord, 14 | simpleValues, 15 | dateValues 16 | ) as Partial; 17 | return partialHBACServiceToHBACService(converted) as HBACService; 18 | } 19 | 20 | export function partialHBACServiceToHBACService( 21 | partialHbacRule: Partial 22 | ): HBACService { 23 | return { 24 | ...createEmptyHBACService(), 25 | ...partialHbacRule, 26 | }; 27 | } 28 | 29 | // Get empty User object initialized with default values 30 | export function createEmptyHBACService(): HBACService { 31 | const hbacService: HBACService = { 32 | description: "", 33 | cn: "", 34 | dn: "", 35 | memberof_hbacsvcgroup: [], 36 | }; 37 | 38 | return hbacService; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/sudoCmdGroupsUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { SudoCmdGroup } from "src/utils/datatypes/globalDataTypes"; 3 | // Utils 4 | import { convertApiObj } from "./ipaObjectUtils"; 5 | 6 | const simpleValues = new Set(["cn", "dn", "description"]); 7 | const dateValues = new Set([]); 8 | 9 | export function apiToSudoCmdGroup( 10 | apiRecord: Record 11 | ): SudoCmdGroup { 12 | const converted = convertApiObj( 13 | apiRecord, 14 | simpleValues, 15 | dateValues 16 | ) as Partial; 17 | return partialSudoCmdGroupToSudoCmdGroup(converted) as SudoCmdGroup; 18 | } 19 | 20 | export function partialSudoCmdGroupToSudoCmdGroup( 21 | partialSudoCmd: Partial 22 | ): SudoCmdGroup { 23 | return { 24 | ...createEmptySudoCmdGroup(), 25 | ...partialSudoCmd, 26 | }; 27 | } 28 | 29 | // Get empty object initialized with default values 30 | export function createEmptySudoCmdGroup(): SudoCmdGroup { 31 | const sudoCmdGroup: SudoCmdGroup = { 32 | cn: "", 33 | dn: "", 34 | description: "", 35 | member_sudocmd: [], 36 | }; 37 | 38 | return sudoCmdGroup; 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/idViewUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { IDView } from "src/utils/datatypes/globalDataTypes"; 3 | // Utils 4 | import { convertApiObj } from "./ipaObjectUtils"; 5 | 6 | const simpleValues = new Set([ 7 | "cn", 8 | "description", 9 | "dn", 10 | "ipadomainresolutionorder", 11 | ]); 12 | const dateValues = new Set([]); 13 | 14 | export function apiToIDView(apiRecord: Record): IDView { 15 | const converted = convertApiObj( 16 | apiRecord, 17 | simpleValues, 18 | dateValues 19 | ) as Partial; 20 | return partialViewToView(converted) as IDView; 21 | } 22 | 23 | export function partialViewToView(partialView: Partial): IDView { 24 | return { 25 | ...createEmptyView(), 26 | ...partialView, 27 | }; 28 | } 29 | 30 | // Get empty User object initialized with default values 31 | export function createEmptyView(): IDView { 32 | const view: IDView = { 33 | dn: "", 34 | cn: "", 35 | description: "", 36 | ipadomainresolutionorder: "", 37 | useroverrides: [], 38 | groupoverrides: [], 39 | appliedtohosts: [], 40 | }; 41 | 42 | return view; 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "target": "ES2015", 7 | "module": "ES2020", 8 | "lib": ["es6", "dom"], 9 | "sourceMap": true, 10 | "jsx": "react", 11 | "forceConsistentCasingInFileNames": true, 12 | "noImplicitReturns": false, 13 | "noImplicitThis": true, 14 | "noImplicitAny": false, 15 | "allowJs": false, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "strict": true, 19 | "importHelpers": true, 20 | "skipLibCheck": true, 21 | "noEmit": false, 22 | "resolveJsonModule": true, 23 | "moduleResolution": "bundler", 24 | "isolatedModules": true, 25 | "paths": { 26 | "@badeball/cypress-cucumber-preprocessor/*": [ 27 | "./node_modules/@badeball/cypress-cucumber-preprocessor/dist/subpath-entrypoints/*" 28 | ] 29 | } 30 | }, 31 | "exclude": [ 32 | "node_modules/**", 33 | "dist/**", 34 | "coverage/**", 35 | "doc/**", 36 | "tests/ipalab/_venv", 37 | "eslint.config.mjs" 38 | ], 39 | "types": ["@testing-library/react"] 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/hbacServiceGrpUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { HBACServiceGroup } from "src/utils/datatypes/globalDataTypes"; 3 | // Utils 4 | import { convertApiObj } from "./ipaObjectUtils"; 5 | 6 | const simpleValues = new Set(["cn", "dn", "description"]); 7 | const dateValues = new Set([]); 8 | 9 | export function apiToHBACServiceGroup( 10 | apiRecord: Record 11 | ): HBACServiceGroup { 12 | const converted = convertApiObj( 13 | apiRecord, 14 | simpleValues, 15 | dateValues 16 | ) as Partial; 17 | return partialHBACSvcGrpToHBACSvcGrp(converted) as HBACServiceGroup; 18 | } 19 | 20 | export function partialHBACSvcGrpToHBACSvcGrp( 21 | partialHbacRule: Partial 22 | ): HBACServiceGroup { 23 | return { 24 | ...createEmptyHBACGroupService(), 25 | ...partialHbacRule, 26 | }; 27 | } 28 | 29 | // Get empty object initialized with default values 30 | export function createEmptyHBACGroupService(): HBACServiceGroup { 31 | const hbacService: HBACServiceGroup = { 32 | description: "", 33 | cn: "", 34 | dn: "", 35 | member_hbacsvc: [], 36 | }; 37 | 38 | return hbacService; 39 | } 40 | -------------------------------------------------------------------------------- /developer/scenarios/server-addc/README.md: -------------------------------------------------------------------------------- 1 | # Scenario: server-addc 2 | 3 | This scenario is meant to test IPA integration with Active Directory, 4 | through trusts. 5 | 6 | This scenario provides two hosts: 7 | 8 | - webui: 9 | - An IPA server deployed 10 | - DNS and Trust enabled 11 | - There's no KRA for faster deployment 12 | - IP address is 192.168.57.10 13 | - Hostname is webui.linux.ipa.test 14 | - Samba AD DC: 15 | - A Samba AD DC deployed, with forwarders to 'webui' 16 | - IP address is 192.168.57.250 17 | - Hostname is dc.ad.ipa.test 18 | - Administrator user is 'Administrator' 19 | - Regular users 'jdoe' and 'anne' are configured. 20 | - All passwords are 'Secret123' 21 | 22 | To set up a trust between Samba AD DC and IPA run: 23 | 24 | ``` 25 | podman exec -it webui bash 26 | kinit admin <<< Secret123 27 | ipa dnsforwardzone-add ad.ipa.test --forwarder 192.168.57.250 28 | ipa trust-add ad.ipa.test --admin Administrator --two-way True <<< Secret123 29 | ``` 30 | 31 | > **NOTE**: To use the development server (Vite) edit the file 32 | > `vite.config.ts` in the repository root and change 'server: cors: origin' 33 | > attribute to `https://webui.linux.ipa.test`. 34 | -------------------------------------------------------------------------------- /src/components/layouts/TableLayout/TableLayout.tsx: -------------------------------------------------------------------------------- 1 | import { Table, TableVariant, Tbody, Thead } from "@patternfly/react-table"; 2 | import React from "react"; 3 | import EmptyBodyTable from "../../tables/EmptyBodyTable"; 4 | 5 | interface PropsToTable { 6 | ariaLabel: string; 7 | name?: string; 8 | variant: TableVariant | "compact"; 9 | hasBorders: boolean; 10 | classes?: string; 11 | tableId: string; 12 | isStickyHeader: boolean; 13 | tableHeader?: JSX.Element; 14 | tableBody: JSX.Element[] | JSX.Element; 15 | } 16 | 17 | const TableLayout = (props: PropsToTable) => { 18 | let body = props.tableBody; 19 | if (Array.isArray(props.tableBody) && props.tableBody.length === 0) { 20 | body = ; 21 | } 22 | 23 | return ( 24 | 33 | {props.tableHeader} 34 | {body} 35 |
36 | ); 37 | }; 38 | 39 | export default TableLayout; 40 | -------------------------------------------------------------------------------- /cypress/e2e/hbac/hbac_service_group/hbac_service_group_settings.feature: -------------------------------------------------------------------------------- 1 | Feature: HBAC service group settings manipulation 2 | Modify a HBAC service group 3 | 4 | @seed 5 | Scenario: Seed: Ensure service group exists 6 | Given HBAC service group "a_service_group_settings" exists 7 | 8 | @test 9 | Scenario: Set Description in settings 10 | Given I am logged in as admin 11 | And I am on "hbac-service-groups/a_service_group_settings" page 12 | 13 | When I type in the "hbac-service-groups-tab-settings-textbox-description" textbox text "test" 14 | Then I should see "test" in the "hbac-service-groups-tab-settings-textbox-description" textbox 15 | And I should see the "hbac-service-groups-tab-settings-button-save" button is enabled 16 | 17 | When I click on the "hbac-service-groups-tab-settings-button-save" button 18 | Then I should see "save-success" alert 19 | 20 | When I am on "hbac-service-groups" page 21 | Then I should see "a_service_group_settings" entry in the data table with attribute "Description" set to "test" 22 | 23 | @cleanup 24 | Scenario: Cleanup: Delete a service group 25 | Given I delete service group "a_service_group_settings" 26 | -------------------------------------------------------------------------------- /public/assets/images/avatarImg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/utils/ipdServerUtils.tsx: -------------------------------------------------------------------------------- 1 | // Data types 2 | import { IDPServer } from "./datatypes/globalDataTypes"; 3 | import { convertApiObj } from "src/utils/ipaObjectUtils"; 4 | 5 | const simpleValues = new Set([ 6 | "cn", 7 | "dn", 8 | "ipaidpauthendpoint", 9 | "ipaidpscope", 10 | "ipaidpsub", 11 | "ipaidptokenendpoint", 12 | ]); 13 | const dateValues = new Set([]); 14 | 15 | export function apiToIdpServer(apiRecord: Record): IDPServer { 16 | const converted = convertApiObj( 17 | apiRecord, 18 | simpleValues, 19 | dateValues 20 | ) as Partial; 21 | return partialIdpServerToIdpServer(converted); 22 | } 23 | 24 | export function partialIdpServerToIdpServer( 25 | partialIdpServer: Partial 26 | ) { 27 | return { 28 | ...createEmptyIdpServer(), 29 | ...partialIdpServer, 30 | }; 31 | } 32 | 33 | export function createEmptyIdpServer(): IDPServer { 34 | return { 35 | cn: "", 36 | dn: "", 37 | ipaidpauthendpoint: "", 38 | ipaidpclientid: [], 39 | ipaidpdevauthendpoint: [], 40 | ipaidpscope: "", 41 | ipaidpsub: "", 42 | ipaidptokenendpoint: "", 43 | ipaidpuserinfoendpoint: [], 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/layouts/TitleLayout/TitleLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { Content, Title } from "@patternfly/react-core"; 4 | 5 | interface PropsToTitleLayout { 6 | headingLevel: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 7 | id: string; 8 | text: string; 9 | className?: string; 10 | ouiaId?: number | string; 11 | ouiaSafe?: boolean; 12 | size?: "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl"; 13 | preText?: string; 14 | } 15 | 16 | const TitleLayout = (props: PropsToTitleLayout) => { 17 | const titleText = props.preText ? ( 18 |
19 |
{props.preText}
20 |
{props.text}
21 |
22 | ) : ( 23 | <>{props.text} 24 | ); 25 | return ( 26 | 27 | 35 | {titleText} 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default TitleLayout; 42 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'freeipa-webui' 10 | copyright = '2023, FreeIPA contributors' 11 | author = 'FreeIPA contributors' 12 | 13 | # -- General configuration --------------------------------------------------- 14 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 15 | 16 | extensions = ['myst_parser'] 17 | 18 | templates_path = ['_templates'] 19 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.venv/*'] 20 | 21 | 22 | 23 | # -- Options for HTML output ------------------------------------------------- 24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 25 | 26 | html_theme = 'alabaster' 27 | html_static_path = ['_static'] 28 | 29 | # -- Options for sources ----------------------------------------------------- 30 | 31 | source_suffix = ['.rst', '.md'] 32 | 33 | -------------------------------------------------------------------------------- /index.dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Identity Management 9 | 15 | 19 | 20 | 21 | 22 |
23 |

24 | Make sure a development server is active by running 25 | npm run dev, or run npm run build for a 26 | production build. 27 |

28 |
29 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/layouts/PopoverWithIconLayout/PopoverWithIconLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { Popover } from "@patternfly/react-core"; 4 | // Icons 5 | import { OutlinedQuestionCircleIcon } from "@patternfly/react-icons"; 6 | 7 | interface PropsToPopover { 8 | message: React.ReactNode | ((hide: () => void) => React.ReactNode); 9 | showClose?: boolean; 10 | ariaLabel?: string; 11 | hasNoPadding?: boolean; 12 | withFocusTrap?: boolean; 13 | hasAutoWidth?: boolean; 14 | triggerHover?: boolean; 15 | } 16 | 17 | const PopoverWithIconLayout = (props: PropsToPopover) => { 18 | return ( 19 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default PopoverWithIconLayout; 36 | -------------------------------------------------------------------------------- /src/components/Form/IpaTextArea/IpaTextArea.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | // PatternFly 3 | import { TextArea } from "@patternfly/react-core"; 4 | import { 5 | IPAParamDefinition, 6 | getParamProperties, 7 | convertToString, 8 | } from "src/utils/ipaObjectUtils"; 9 | 10 | export interface IpaTextAreaProps extends IPAParamDefinition { 11 | dataCy: string; 12 | } 13 | 14 | const IpaTextArea = (props: IpaTextAreaProps) => { 15 | const { required, readOnly, value, onChange } = getParamProperties(props); 16 | 17 | const [textareaValue, setTextareaValue] = React.useState( 18 | convertToString(value) 19 | ); 20 | 21 | React.useEffect(() => { 22 | setTextareaValue(convertToString(value)); 23 | }, [value]); 24 | 25 | return ( 26 |