@keycloakify/keycloak-ui-shared 26.0.6001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +6 -0
  3. package/keycloak-theme/shared/keycloak-ui-shared/alerts/AlertPanel.tsx +43 -0
  4. package/keycloak-theme/shared/keycloak-ui-shared/alerts/Alerts.tsx +82 -0
  5. package/keycloak-theme/shared/keycloak-ui-shared/buttons/FormSubmitButton.tsx +47 -0
  6. package/keycloak-theme/shared/keycloak-ui-shared/context/ErrorPage.tsx +60 -0
  7. package/keycloak-theme/shared/keycloak-ui-shared/context/HelpContext.tsx +30 -0
  8. package/keycloak-theme/shared/keycloak-ui-shared/context/KeycloakContext.tsx +97 -0
  9. package/keycloak-theme/shared/keycloak-ui-shared/context/environment.ts +50 -0
  10. package/keycloak-theme/shared/keycloak-ui-shared/continue-cancel/ContinueCancelModal.tsx +75 -0
  11. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormErrorText.tsx +23 -0
  12. package/keycloak-theme/shared/keycloak-ui-shared/controls/FormLabel.tsx +40 -0
  13. package/keycloak-theme/shared/keycloak-ui-shared/controls/HelpItem.tsx +43 -0
  14. package/keycloak-theme/shared/keycloak-ui-shared/controls/KeycloakSpinner.tsx +12 -0
  15. package/keycloak-theme/shared/keycloak-ui-shared/controls/NumberControl.tsx +93 -0
  16. package/keycloak-theme/shared/keycloak-ui-shared/controls/OrganizationTable.tsx +122 -0
  17. package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordControl.tsx +71 -0
  18. package/keycloak-theme/shared/keycloak-ui-shared/controls/PasswordInput.tsx +50 -0
  19. package/keycloak-theme/shared/keycloak-ui-shared/controls/SwitchControl.tsx +67 -0
  20. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextAreaControl.tsx +60 -0
  21. package/keycloak-theme/shared/keycloak-ui-shared/controls/TextControl.tsx +75 -0
  22. package/keycloak-theme/shared/keycloak-ui-shared/controls/keycloak-text-area/KeycloakTextArea.tsx +23 -0
  23. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SelectControl.tsx +75 -0
  24. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/SingleSelectControl.tsx +109 -0
  25. package/keycloak-theme/shared/keycloak-ui-shared/controls/select-control/TypeaheadSelectControl.tsx +285 -0
  26. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/KeycloakDataTable.tsx +597 -0
  27. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/ListEmptyState.tsx +86 -0
  28. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/PaginatingTableToolbar.tsx +106 -0
  29. package/keycloak-theme/shared/keycloak-ui-shared/controls/table/TableToolbar.tsx +92 -0
  30. package/keycloak-theme/shared/keycloak-ui-shared/icons/IconMapper.tsx +63 -0
  31. package/keycloak-theme/shared/keycloak-ui-shared/index.ts +1 -0
  32. package/keycloak-theme/shared/keycloak-ui-shared/main.ts +96 -0
  33. package/keycloak-theme/shared/keycloak-ui-shared/masthead/DefaultAvatar.tsx +109 -0
  34. package/keycloak-theme/shared/keycloak-ui-shared/masthead/KeycloakDropdown.tsx +48 -0
  35. package/keycloak-theme/shared/keycloak-ui-shared/masthead/Masthead.tsx +161 -0
  36. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormPanel.tsx +29 -0
  37. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/FormTitle.tsx +28 -0
  38. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollForm.tsx +98 -0
  39. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/ScrollPanel.tsx +21 -0
  40. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/form-title.module.css +4 -0
  41. package/keycloak-theme/shared/keycloak-ui-shared/scroll-form/scroll-form.module.css +8 -0
  42. package/keycloak-theme/shared/keycloak-ui-shared/select/KeycloakSelect.tsx +49 -0
  43. package/keycloak-theme/shared/keycloak-ui-shared/select/SingleSelect.tsx +89 -0
  44. package/keycloak-theme/shared/keycloak-ui-shared/select/TypeaheadSelect.tsx +198 -0
  45. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/LocaleSelector.tsx +51 -0
  46. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/MultiInputComponent.tsx +146 -0
  47. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/OptionsComponent.tsx +63 -0
  48. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/SelectComponent.tsx +109 -0
  49. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextAreaComponent.tsx +23 -0
  50. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/TextComponent.tsx +32 -0
  51. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileFields.tsx +243 -0
  52. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/UserProfileGroup.tsx +71 -0
  53. package/keycloak-theme/shared/keycloak-ui-shared/user-profile/utils.ts +170 -0
  54. package/keycloak-theme/shared/keycloak-ui-shared/utils/ErrorBoundary.tsx +77 -0
  55. package/keycloak-theme/shared/keycloak-ui-shared/utils/createNamedContext.ts +11 -0
  56. package/keycloak-theme/shared/keycloak-ui-shared/utils/darkMode.ts +19 -0
  57. package/keycloak-theme/shared/keycloak-ui-shared/utils/errors.ts +55 -0
  58. package/keycloak-theme/shared/keycloak-ui-shared/utils/generateId.ts +1 -0
  59. package/keycloak-theme/shared/keycloak-ui-shared/utils/getRuleValue.ts +17 -0
  60. package/keycloak-theme/shared/keycloak-ui-shared/utils/isDefined.ts +3 -0
  61. package/keycloak-theme/shared/keycloak-ui-shared/utils/useFetch.ts +44 -0
  62. package/keycloak-theme/shared/keycloak-ui-shared/utils/useRequiredContext.ts +24 -0
  63. package/keycloak-theme/shared/keycloak-ui-shared/utils/useSetTimeout.ts +40 -0
  64. package/keycloak-theme/shared/keycloak-ui-shared/utils/useStorageItem.ts +51 -0
  65. package/keycloak-theme/shared/keycloak-ui-shared/utils/useStoredState.ts +38 -0
  66. package/package.json +31 -0
@@ -0,0 +1,106 @@
1
+ import {
2
+ Pagination,
3
+ PaginationToggleTemplateProps,
4
+ ToolbarItem,
5
+ } from "@patternfly/react-core";
6
+ import { PropsWithChildren, ReactNode } from "react";
7
+ import { useTranslation } from "react-i18next";
8
+
9
+ import { TableToolbar } from "./TableToolbar";
10
+
11
+ type KeycloakPaginationProps = {
12
+ id?: string;
13
+ count: number;
14
+ first: number;
15
+ max: number;
16
+ onNextClick: (page: number) => void;
17
+ onPreviousClick: (page: number) => void;
18
+ onPerPageSelect: (max: number, first: number) => void;
19
+ variant?: "top" | "bottom";
20
+ };
21
+
22
+ type TableToolbarProps = KeycloakPaginationProps & {
23
+ searchTypeComponent?: ReactNode;
24
+ toolbarItem?: ReactNode;
25
+ subToolbar?: ReactNode;
26
+ inputGroupName?: string;
27
+ inputGroupPlaceholder?: string;
28
+ inputGroupOnEnter?: (value: string) => void;
29
+ };
30
+
31
+ const KeycloakPagination = ({
32
+ id,
33
+ variant = "top",
34
+ count,
35
+ first,
36
+ max,
37
+ onNextClick,
38
+ onPreviousClick,
39
+ onPerPageSelect,
40
+ }: KeycloakPaginationProps) => {
41
+ const { t } = useTranslation();
42
+ const page = Math.round(first / max);
43
+ return (
44
+ <Pagination
45
+ widgetId={id}
46
+ titles={{
47
+ paginationAriaLabel: `${t("pagination")} ${variant} `,
48
+ }}
49
+ isCompact
50
+ toggleTemplate={({
51
+ firstIndex,
52
+ lastIndex,
53
+ }: PaginationToggleTemplateProps) => (
54
+ <b>
55
+ {firstIndex} - {lastIndex}
56
+ </b>
57
+ )}
58
+ itemCount={count + page * max}
59
+ page={page + 1}
60
+ perPage={max}
61
+ onNextClick={(_, p) => onNextClick((p - 1) * max)}
62
+ onPreviousClick={(_, p) => onPreviousClick((p - 1) * max)}
63
+ onPerPageSelect={(_, m, f) => onPerPageSelect(f - 1, m)}
64
+ variant={variant}
65
+ />
66
+ );
67
+ };
68
+
69
+ export const PaginatingTableToolbar = ({
70
+ count,
71
+ searchTypeComponent,
72
+ toolbarItem,
73
+ subToolbar,
74
+ children,
75
+ inputGroupName,
76
+ inputGroupPlaceholder,
77
+ inputGroupOnEnter,
78
+ ...rest
79
+ }: PropsWithChildren<TableToolbarProps>) => {
80
+ return (
81
+ <TableToolbar
82
+ searchTypeComponent={searchTypeComponent}
83
+ toolbarItem={
84
+ <>
85
+ {toolbarItem}
86
+ <ToolbarItem variant="pagination">
87
+ <KeycloakPagination count={count} {...rest} />
88
+ </ToolbarItem>
89
+ </>
90
+ }
91
+ subToolbar={subToolbar}
92
+ toolbarItemFooter={
93
+ count !== 0 ? (
94
+ <ToolbarItem variant="pagination">
95
+ <KeycloakPagination count={count} variant="bottom" {...rest} />
96
+ </ToolbarItem>
97
+ ) : null
98
+ }
99
+ inputGroupName={inputGroupName}
100
+ inputGroupPlaceholder={inputGroupPlaceholder}
101
+ inputGroupOnEnter={inputGroupOnEnter}
102
+ >
103
+ {children}
104
+ </TableToolbar>
105
+ );
106
+ };
@@ -0,0 +1,92 @@
1
+ import {
2
+ Divider,
3
+ InputGroup,
4
+ SearchInput,
5
+ Toolbar,
6
+ ToolbarContent,
7
+ ToolbarItem,
8
+ } from "@patternfly/react-core";
9
+ import { KeyboardEvent, PropsWithChildren, ReactNode, useState } from "react";
10
+ import { useTranslation } from "react-i18next";
11
+
12
+ type TableToolbarProps = {
13
+ toolbarItem?: ReactNode;
14
+ subToolbar?: ReactNode;
15
+ toolbarItemFooter?: ReactNode;
16
+ searchTypeComponent?: ReactNode;
17
+ inputGroupName?: string;
18
+ inputGroupPlaceholder?: string;
19
+ inputGroupOnEnter?: (value: string) => void;
20
+ };
21
+
22
+ export const TableToolbar = ({
23
+ toolbarItem,
24
+ subToolbar,
25
+ toolbarItemFooter,
26
+ children,
27
+ searchTypeComponent,
28
+ inputGroupName,
29
+ inputGroupPlaceholder,
30
+ inputGroupOnEnter,
31
+ }: PropsWithChildren<TableToolbarProps>) => {
32
+ const { t } = useTranslation();
33
+ const [searchValue, setSearchValue] = useState<string>("");
34
+
35
+ const onSearch = () => {
36
+ if (searchValue !== "") {
37
+ setSearchValue(searchValue);
38
+ inputGroupOnEnter?.(searchValue);
39
+ } else {
40
+ setSearchValue("");
41
+ inputGroupOnEnter?.("");
42
+ }
43
+ };
44
+
45
+ const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
46
+ if (e.key === "Enter") {
47
+ onSearch();
48
+ }
49
+ };
50
+
51
+ return (
52
+ <>
53
+ <Toolbar>
54
+ <ToolbarContent>
55
+ {inputGroupName && (
56
+ <ToolbarItem>
57
+ <InputGroup data-testid={inputGroupName}>
58
+ {searchTypeComponent}
59
+ {inputGroupPlaceholder && (
60
+ <SearchInput
61
+ data-testid="table-search-input"
62
+ placeholder={inputGroupPlaceholder}
63
+ aria-label={t("search")}
64
+ value={searchValue}
65
+ onChange={(_, value) => {
66
+ setSearchValue(value);
67
+ }}
68
+ onSearch={onSearch}
69
+ onKeyDown={handleKeyDown}
70
+ onClear={() => {
71
+ setSearchValue("");
72
+ inputGroupOnEnter?.("");
73
+ }}
74
+ />
75
+ )}
76
+ </InputGroup>
77
+ </ToolbarItem>
78
+ )}
79
+ {toolbarItem}
80
+ </ToolbarContent>
81
+ </Toolbar>
82
+ {subToolbar && (
83
+ <Toolbar>
84
+ <ToolbarContent>{subToolbar}</ToolbarContent>
85
+ </Toolbar>
86
+ )}
87
+ <Divider />
88
+ {children}
89
+ <Toolbar>{toolbarItemFooter}</Toolbar>
90
+ </>
91
+ );
92
+ };
@@ -0,0 +1,63 @@
1
+ import { Icon } from "@patternfly/react-core";
2
+ import {
3
+ BitbucketIcon,
4
+ CubeIcon,
5
+ FacebookSquareIcon,
6
+ GithubIcon,
7
+ GitlabIcon,
8
+ GoogleIcon,
9
+ InstagramIcon,
10
+ LinkedinIcon,
11
+ MicrosoftIcon,
12
+ OpenshiftIcon,
13
+ PaypalIcon,
14
+ StackOverflowIcon,
15
+ TwitterIcon,
16
+ } from "@patternfly/react-icons";
17
+
18
+ type IconMapperProps = {
19
+ icon: string;
20
+ };
21
+
22
+ export const IconMapper = ({ icon }: IconMapperProps) => {
23
+ const SpecificIcon = getIcon(icon);
24
+ return (
25
+ <Icon size="lg">
26
+ <SpecificIcon alt={icon} />
27
+ </Icon>
28
+ );
29
+ };
30
+
31
+ function getIcon(icon: string) {
32
+ switch (icon) {
33
+ case "github":
34
+ return GithubIcon;
35
+ case "facebook":
36
+ return FacebookSquareIcon;
37
+ case "gitlab":
38
+ return GitlabIcon;
39
+ case "google":
40
+ return GoogleIcon;
41
+ case "linkedin":
42
+ case "linkedin-openid-connect":
43
+ return LinkedinIcon;
44
+
45
+ case "openshift-v3":
46
+ case "openshift-v4":
47
+ return OpenshiftIcon;
48
+ case "stackoverflow":
49
+ return StackOverflowIcon;
50
+ case "twitter":
51
+ return TwitterIcon;
52
+ case "microsoft":
53
+ return MicrosoftIcon;
54
+ case "bitbucket":
55
+ return BitbucketIcon;
56
+ case "instagram":
57
+ return InstagramIcon;
58
+ case "paypal":
59
+ return PaypalIcon;
60
+ default:
61
+ return CubeIcon;
62
+ }
63
+ }
@@ -0,0 +1 @@
1
+ export * from './main';
@@ -0,0 +1,96 @@
1
+ export {
2
+ AlertProvider,
3
+ useAlerts,
4
+ type AddAlertFunction,
5
+ type AddErrorFunction,
6
+ type AlertProps,
7
+ } from "./alerts/Alerts";
8
+ export { ErrorPage } from "./context/ErrorPage";
9
+ export { Help, useHelp } from "./context/HelpContext";
10
+ export {
11
+ KeycloakProvider,
12
+ useEnvironment,
13
+ type KeycloakContext,
14
+ } from "./context/KeycloakContext";
15
+ export {
16
+ getInjectedEnvironment,
17
+ type BaseEnvironment,
18
+ } from "./context/environment";
19
+ export { ContinueCancelModal } from "./continue-cancel/ContinueCancelModal";
20
+ export {
21
+ FormErrorText,
22
+ type FormErrorTextProps,
23
+ } from "./controls/FormErrorText";
24
+ export { HelpItem } from "./controls/HelpItem";
25
+ export { NumberControl } from "./controls/NumberControl";
26
+ export { PasswordControl } from "./controls/PasswordControl";
27
+ export { PasswordInput } from "./controls/PasswordInput";
28
+ export {
29
+ SelectControl,
30
+ SelectVariant,
31
+ } from "./controls/select-control/SelectControl";
32
+ export type {
33
+ SelectControlOption,
34
+ SelectControlProps,
35
+ } from "./controls/select-control/SelectControl";
36
+ export {
37
+ SwitchControl,
38
+ type SwitchControlProps,
39
+ } from "./controls/SwitchControl";
40
+ export { TextAreaControl } from "./controls/TextAreaControl";
41
+ export { TextControl } from "./controls/TextControl";
42
+ export {
43
+ KeycloakTextArea,
44
+ type KeycloakTextAreaProps,
45
+ } from "./controls/keycloak-text-area/KeycloakTextArea";
46
+ export { IconMapper } from "./icons/IconMapper";
47
+ export { FormPanel } from "./scroll-form/FormPanel";
48
+ export { ScrollForm, mainPageContentId } from "./scroll-form/ScrollForm";
49
+ export {
50
+ FormSubmitButton,
51
+ type FormSubmitButtonProps,
52
+ } from "./buttons/FormSubmitButton";
53
+ export { UserProfileFields } from "./user-profile/UserProfileFields";
54
+ export {
55
+ beerify,
56
+ debeerify,
57
+ isUserProfileError,
58
+ label,
59
+ setUserProfileServerError,
60
+ } from "./user-profile/utils";
61
+ export type { UserFormFields } from "./user-profile/utils";
62
+ export { createNamedContext } from "./utils/createNamedContext";
63
+ export {
64
+ getErrorDescription,
65
+ getErrorMessage,
66
+ getNetworkErrorMessage,
67
+ getNetworkErrorDescription,
68
+ } from "./utils/errors";
69
+ export { isDefined } from "./utils/isDefined";
70
+ export { useRequiredContext } from "./utils/useRequiredContext";
71
+ export { useStoredState } from "./utils/useStoredState";
72
+ export { useSetTimeout } from "./utils/useSetTimeout";
73
+ export { generateId } from "./utils/generateId";
74
+ export { default as KeycloakMasthead } from "./masthead/Masthead";
75
+ export { KeycloakSelect } from "./select/KeycloakSelect";
76
+ export type { Variant, KeycloakSelectProps } from "./select/KeycloakSelect";
77
+ export { KeycloakDataTable } from "./controls/table/KeycloakDataTable";
78
+ export type {
79
+ Action,
80
+ Field,
81
+ DetailField,
82
+ LoaderFunction,
83
+ } from "./controls/table/KeycloakDataTable";
84
+ export { PaginatingTableToolbar } from "./controls/table/PaginatingTableToolbar";
85
+ export { TableToolbar } from "./controls/table/TableToolbar";
86
+ export { ListEmptyState } from "./controls/table/ListEmptyState";
87
+ export { KeycloakSpinner } from "./controls/KeycloakSpinner";
88
+ export { useFetch } from "./utils/useFetch";
89
+ export {
90
+ useErrorBoundary,
91
+ ErrorBoundaryFallback,
92
+ ErrorBoundaryProvider,
93
+ } from "./utils/ErrorBoundary";
94
+ export type { FallbackProps } from "./utils/ErrorBoundary";
95
+ export { OrganizationTable } from "./controls/OrganizationTable";
96
+ export { initializeDarkMode } from "./utils/darkMode";
@@ -0,0 +1,109 @@
1
+ import styles from "@patternfly/react-styles/css/components/Avatar/avatar";
2
+ import { css } from "@patternfly/react-styles";
3
+
4
+ type DefaultAvatarProps = {
5
+ className?: string;
6
+ border?: "light" | "dark";
7
+ size?: "sm" | "md" | "lg" | "xl";
8
+ };
9
+
10
+ export const DefaultAvatar = ({
11
+ className = "",
12
+ border,
13
+ size = "md",
14
+ }: DefaultAvatarProps) => (
15
+ <svg
16
+ className={css(
17
+ styles.avatar,
18
+ styles.modifiers[size],
19
+ border === "light" && styles.modifiers.light,
20
+ border === "dark" && styles.modifiers.dark,
21
+ className,
22
+ )}
23
+ enableBackground="new 0 0 36 36"
24
+ version="1.1"
25
+ viewBox="0 0 36 36"
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ >
28
+ <circle
29
+ style={{ fillRule: "evenodd", clipRule: "evenodd", fill: "#FFFFFF" }}
30
+ cx="18"
31
+ cy="18.5"
32
+ r="18"
33
+ />
34
+ <defs>
35
+ <filter
36
+ id="b"
37
+ x="5.2"
38
+ y="7.2"
39
+ width="25.6"
40
+ height="53.6"
41
+ filterUnits="userSpaceOnUse"
42
+ >
43
+ <feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" />
44
+ </filter>
45
+ </defs>
46
+ <mask
47
+ id="a"
48
+ x="5.2"
49
+ y="7.2"
50
+ width="25.6"
51
+ height="53.6"
52
+ maskUnits="userSpaceOnUse"
53
+ >
54
+ <g style={{ filter: 'url("#b")' }}>
55
+ <circle
56
+ style={{ fillRule: "evenodd", clipRule: "evenodd", fill: "#FFFFFF" }}
57
+ cx="18"
58
+ cy="18.5"
59
+ r="18"
60
+ />
61
+ </g>
62
+ </mask>
63
+ <g style={{ filter: 'url("#a")' }}>
64
+ <g transform="translate(5.04 6.88)">
65
+ <path
66
+ style={{
67
+ fillRule: "evenodd",
68
+ clipRule: "evenodd",
69
+ fill: "#BBBBBB",
70
+ }}
71
+ d="m22.6 18.1c-1.1-1.4-2.3-2.2-3.5-2.6s-1.8-0.6-6.3-0.6-6.1 0.7-6.1 0.7 0 0 0 0c-1.2 0.4-2.4 1.2-3.4 2.6-2.3 2.8-3.2 12.3-3.2 14.8 0 3.2 0.4 12.3 0.6 15.4 0 0-0.4 5.5 4 5.5l-0.3-6.3-0.4-3.5 0.2-0.9c0.9 0.4 3.6 1.2 8.6 1.2 5.3 0 8-0.9 8.8-1.3l0.2 1-0.2 3.6-0.3 6.3c3 0.1 3.7-3 3.8-4.4s0.6-12.6 0.6-16.5c0.1-2.6-0.8-12.1-3.1-15z"
72
+ />
73
+ <path
74
+ style={{
75
+ opacity: 0.1,
76
+ fillRule: "evenodd",
77
+ clipRule: "evenodd",
78
+ }}
79
+ d="m22.5 26c-0.1-2.1-1.5-2.8-4.8-2.8l2.2 9.6s1.8-1.7 3-1.8c0 0-0.4-4.6-0.4-5z"
80
+ />
81
+ <path
82
+ style={{
83
+ fillRule: "evenodd",
84
+ clipRule: "evenodd",
85
+ fill: "#BBBBBB",
86
+ }}
87
+ d="m12.7 13.2c-3.5 0-6.4-2.9-6.4-6.4s2.9-6.4 6.4-6.4 6.4 2.9 6.4 6.4-2.8 6.4-6.4 6.4z"
88
+ />
89
+ <path
90
+ style={{
91
+ opacity: 8.0e-2,
92
+ fillRule: "evenodd",
93
+ clipRule: "evenodd",
94
+ fill: "#231F20",
95
+ }}
96
+ d="m9.4 6.8c0-3 2.1-5.5 4.9-6.3-0.5-0.1-1-0.2-1.6-0.2-3.5 0-6.4 2.9-6.4 6.4s2.9 6.4 6.4 6.4c0.6 0 1.1-0.1 1.6-0.2-2.8-0.6-4.9-3.1-4.9-6.1z"
97
+ />
98
+ <path
99
+ style={{
100
+ opacity: 0.1,
101
+ fillRule: "evenodd",
102
+ clipRule: "evenodd",
103
+ }}
104
+ d="m8.3 22.4c-2 0.4-2.9 1.4-3.1 3.5l-0.6 18.6s1.7 0.7 3.6 0.9l0.1-23z"
105
+ />
106
+ </g>
107
+ </g>
108
+ </svg>
109
+ );
@@ -0,0 +1,48 @@
1
+ import {
2
+ Dropdown,
3
+ DropdownList,
4
+ DropdownProps,
5
+ MenuToggle,
6
+ } from "@patternfly/react-core";
7
+ import { EllipsisVIcon } from "@patternfly/react-icons";
8
+ import { ReactNode, useState } from "react";
9
+
10
+ type KeycloakDropdownProps = Omit<DropdownProps, "toggle"> & {
11
+ "data-testid"?: string;
12
+ isKebab?: boolean;
13
+ title?: ReactNode;
14
+ dropDownItems: ReactNode[];
15
+ };
16
+
17
+ export const KeycloakDropdown = ({
18
+ isKebab = false,
19
+ title,
20
+ dropDownItems,
21
+ ...rest
22
+ }: KeycloakDropdownProps) => {
23
+ const [open, setOpen] = useState(false);
24
+
25
+ return (
26
+ <Dropdown
27
+ {...rest}
28
+ popperProps={{
29
+ position: "right",
30
+ }}
31
+ onOpenChange={(isOpen) => setOpen(isOpen)}
32
+ toggle={(ref) => (
33
+ <MenuToggle
34
+ data-testid={`${rest["data-testid"]}-toggle`}
35
+ ref={ref}
36
+ onClick={() => setOpen(!open)}
37
+ isExpanded={open}
38
+ variant={isKebab ? "plain" : "default"}
39
+ >
40
+ {isKebab ? <EllipsisVIcon /> : title}
41
+ </MenuToggle>
42
+ )}
43
+ isOpen={open}
44
+ >
45
+ <DropdownList>{dropDownItems}</DropdownList>
46
+ </Dropdown>
47
+ );
48
+ };
@@ -0,0 +1,161 @@
1
+ import {
2
+ Avatar,
3
+ AvatarProps,
4
+ DropdownItem,
5
+ Masthead,
6
+ MastheadBrand,
7
+ MastheadBrandProps,
8
+ MastheadContent,
9
+ MastheadMainProps,
10
+ MastheadToggle,
11
+ PageToggleButton,
12
+ Toolbar,
13
+ ToolbarContent,
14
+ ToolbarItem,
15
+ } from "@patternfly/react-core";
16
+ import { BarsIcon } from "@patternfly/react-icons";
17
+ import { TFunction } from "i18next";
18
+ import Keycloak, { type KeycloakTokenParsed } from "keycloak-js";
19
+ import { ReactNode } from "react";
20
+ import { useTranslation } from "react-i18next";
21
+ import { DefaultAvatar } from "./DefaultAvatar";
22
+ import { KeycloakDropdown } from "./KeycloakDropdown";
23
+
24
+ function loggedInUserName(
25
+ token: KeycloakTokenParsed | undefined,
26
+ t: TFunction,
27
+ ) {
28
+ if (!token) {
29
+ return t("unknownUser");
30
+ }
31
+
32
+ const givenName = token.given_name;
33
+ const familyName = token.family_name;
34
+ const preferredUsername = token.preferred_username;
35
+
36
+ if (givenName && familyName) {
37
+ return t("fullName", { givenName, familyName });
38
+ }
39
+
40
+ return givenName || familyName || preferredUsername || t("unknownUser");
41
+ }
42
+
43
+ type BrandLogo = MastheadBrandProps;
44
+
45
+ type KeycloakMastheadProps = MastheadMainProps & {
46
+ keycloak: Keycloak;
47
+ brand: BrandLogo;
48
+ avatar?: AvatarProps;
49
+ features?: {
50
+ hasLogout?: boolean;
51
+ hasManageAccount?: boolean;
52
+ hasUsername?: boolean;
53
+ };
54
+ kebabDropdownItems?: ReactNode[];
55
+ dropdownItems?: ReactNode[];
56
+ toolbarItems?: ReactNode[];
57
+ };
58
+
59
+ const KeycloakMasthead = ({
60
+ keycloak,
61
+ brand: { src, alt, className, ...brandProps },
62
+ avatar,
63
+ features: {
64
+ hasLogout = true,
65
+ hasManageAccount = true,
66
+ hasUsername = true,
67
+ } = {},
68
+ kebabDropdownItems,
69
+ dropdownItems = [],
70
+ toolbarItems,
71
+ ...rest
72
+ }: KeycloakMastheadProps) => {
73
+ const { t } = useTranslation();
74
+ const extraItems = [];
75
+ if (hasManageAccount) {
76
+ extraItems.push(
77
+ <DropdownItem
78
+ key="manageAccount"
79
+ onClick={() => keycloak.accountManagement()}
80
+ >
81
+ {t("manageAccount")}
82
+ </DropdownItem>,
83
+ );
84
+ }
85
+ if (hasLogout) {
86
+ extraItems.push(
87
+ <DropdownItem key="signOut" onClick={() => keycloak.logout()}>
88
+ {t("signOut")}
89
+ </DropdownItem>,
90
+ );
91
+ }
92
+
93
+ const picture = keycloak.idTokenParsed?.picture;
94
+ return (
95
+ <Masthead {...rest}>
96
+ <MastheadToggle>
97
+ <PageToggleButton variant="plain" aria-label={t("navigation")}>
98
+ <BarsIcon />
99
+ </PageToggleButton>
100
+ </MastheadToggle>
101
+ <MastheadBrand {...brandProps}>
102
+ <img src={src} alt={alt} className={className} />
103
+ </MastheadBrand>
104
+ <MastheadContent>
105
+ <Toolbar>
106
+ <ToolbarContent>
107
+ {toolbarItems?.map((item, index) => (
108
+ <ToolbarItem key={index} align={{ default: "alignRight" }}>
109
+ {item}
110
+ </ToolbarItem>
111
+ ))}
112
+ <ToolbarItem
113
+ visibility={{
114
+ default: "hidden",
115
+ md: "visible",
116
+ }} /** this user dropdown is hidden on mobile sizes */
117
+ >
118
+ <KeycloakDropdown
119
+ data-testid="options"
120
+ dropDownItems={[...dropdownItems, extraItems]}
121
+ title={
122
+ hasUsername
123
+ ? loggedInUserName(keycloak.idTokenParsed, t)
124
+ : undefined
125
+ }
126
+ />
127
+ </ToolbarItem>
128
+ <ToolbarItem
129
+ align={{ default: "alignLeft" }}
130
+ visibility={{
131
+ md: "hidden",
132
+ }}
133
+ >
134
+ <KeycloakDropdown
135
+ data-testid="options-kebab"
136
+ isKebab
137
+ dropDownItems={[
138
+ ...(kebabDropdownItems || dropdownItems),
139
+ extraItems,
140
+ ]}
141
+ />
142
+ </ToolbarItem>
143
+ <ToolbarItem
144
+ variant="overflow-menu"
145
+ align={{ default: "alignRight" }}
146
+ className="pf-v5-u-m-0-on-lg"
147
+ >
148
+ {picture || avatar?.src ? (
149
+ <Avatar {...{ src: picture, alt: t("avatar"), ...avatar }} />
150
+ ) : (
151
+ <DefaultAvatar {...avatar} />
152
+ )}
153
+ </ToolbarItem>
154
+ </ToolbarContent>
155
+ </Toolbar>
156
+ </MastheadContent>
157
+ </Masthead>
158
+ );
159
+ };
160
+
161
+ export default KeycloakMasthead;