diff --git a/js/apps/account-ui/src/account-security/LinkedAccounts.tsx b/js/apps/account-ui/src/account-security/LinkedAccounts.tsx index 277b457ba0..541c2b84b9 100644 --- a/js/apps/account-ui/src/account-security/LinkedAccounts.tsx +++ b/js/apps/account-ui/src/account-security/LinkedAccounts.tsx @@ -1,34 +1,49 @@ +import { useEnvironment } from "@keycloak/keycloak-ui-shared"; import { DataList, Stack, StackItem, Title } from "@patternfly/react-core"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { getLinkedAccounts } from "../api/methods"; +import { getLinkedAccounts, LinkedAccountQueryParams } from "../api/methods"; import { LinkedAccountRepresentation } from "../api/representations"; import { EmptyRow } from "../components/datalist/EmptyRow"; import { Page } from "../components/page/Page"; import { usePromise } from "../utils/usePromise"; import { AccountRow } from "./AccountRow"; -import { useEnvironment } from "@keycloak/keycloak-ui-shared"; +import { LinkedAccountsToolbar } from "./LinkedAccountsToolbar"; export const LinkedAccounts = () => { const { t } = useTranslation(); const context = useEnvironment(); - const [accounts, setAccounts] = useState([]); + const [linkedAccounts, setLinkedAccounts] = useState< + LinkedAccountRepresentation[] + >([]); + const [unlinkedAccounts, setUninkedAccounts] = useState< + LinkedAccountRepresentation[] + >([]); + const [paramsUnlinked, setParamsUnlinked] = + useState({ + first: 0, + max: 6, + linked: false, + }); + const [paramsLinked, setParamsLinked] = useState({ + first: 0, + max: 6, + linked: true, + }); const [key, setKey] = useState(1); const refresh = () => setKey(key + 1); - usePromise((signal) => getLinkedAccounts({ signal, context }), setAccounts, [ - key, - ]); - - const linkedAccounts = useMemo( - () => accounts.filter((account) => account.connected), - [accounts], + usePromise( + (signal) => getLinkedAccounts({ signal, context }, paramsUnlinked), + setUninkedAccounts, + [paramsUnlinked, key], ); - const unLinkedAccounts = useMemo( - () => accounts.filter((account) => !account.connected), - [accounts], + usePromise( + (signal) => getLinkedAccounts({ signal, context }, paramsLinked), + setLinkedAccounts, + [paramsLinked, key], ); return ( @@ -41,16 +56,47 @@ export const LinkedAccounts = () => { {t("linkedLoginProviders")} + + setParamsLinked({ ...paramsLinked, first: 0, search }) + } + count={linkedAccounts.length} + first={paramsLinked["first"]} + max={paramsLinked["max"]} + onNextClick={() => { + setParamsLinked({ + ...paramsLinked, + first: paramsLinked.first + paramsLinked.max - 1, + }); + }} + onPreviousClick={() => + setParamsLinked({ + ...paramsLinked, + first: paramsLinked.first - paramsLinked.max + 1, + }) + } + onPerPageSelect={(first, max) => + setParamsLinked({ + ...paramsLinked, + first, + max, + }) + } + hasNext={linkedAccounts.length > paramsLinked.max - 1} + /> {linkedAccounts.length > 0 ? ( - linkedAccounts.map((account) => ( - - )) + linkedAccounts.map( + (account, index) => + index !== paramsLinked.max - 1 && ( + + ), + ) ) : ( )} @@ -64,15 +110,46 @@ export const LinkedAccounts = () => { > {t("unlinkedLoginProviders")} + + setParamsUnlinked({ ...paramsUnlinked, first: 0, search }) + } + count={unlinkedAccounts.length} + first={paramsUnlinked["first"]} + max={paramsUnlinked["max"]} + onNextClick={() => { + setParamsUnlinked({ + ...paramsUnlinked, + first: paramsUnlinked.first + paramsUnlinked.max - 1, + }); + }} + onPreviousClick={() => + setParamsUnlinked({ + ...paramsUnlinked, + first: paramsUnlinked.first - paramsUnlinked.max + 1, + }) + } + onPerPageSelect={(first, max) => + setParamsUnlinked({ + ...paramsUnlinked, + first, + max, + }) + } + hasNext={unlinkedAccounts.length > paramsUnlinked.max - 1} + /> - {unLinkedAccounts.length > 0 ? ( - unLinkedAccounts.map((account) => ( - - )) + {unlinkedAccounts.length > 0 ? ( + unlinkedAccounts.map( + (account, index) => + index !== paramsUnlinked.max - 1 && ( + + ), + ) ) : ( )} diff --git a/js/apps/account-ui/src/account-security/LinkedAccountsToolbar.tsx b/js/apps/account-ui/src/account-security/LinkedAccountsToolbar.tsx new file mode 100644 index 0000000000..16c45196fe --- /dev/null +++ b/js/apps/account-ui/src/account-security/LinkedAccountsToolbar.tsx @@ -0,0 +1,88 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Pagination, + SearchInput, + PaginationToggleTemplateProps, + Toolbar, + ToolbarContent, + ToolbarItem, +} from "@patternfly/react-core"; + +type LinkedAccountsToolbarProps = { + onFilter: (nameFilter: string) => void; + count: number; + first: number; + max: number; + onNextClick: (page: number) => void; + onPreviousClick: (page: number) => void; + onPerPageSelect: (max: number, first: number) => void; + hasNext: boolean; +}; + +export const LinkedAccountsToolbar = ({ + count, + first, + max, + onNextClick, + onPreviousClick, + onPerPageSelect, + onFilter, + hasNext, +}: LinkedAccountsToolbarProps) => { + const { t } = useTranslation(); + const [nameFilter, setNameFilter] = useState(""); + + const page = Math.round(first / max) + 1; + return ( + + + + { + setNameFilter(value); + }} + onSearch={() => onFilter(nameFilter)} + onKeyDown={(e) => { + if (e.key === "Enter") { + onFilter(nameFilter); + } + }} + onClear={() => { + setNameFilter(""); + onFilter(""); + }} + /> + + + ( + + {firstIndex && firstIndex > 1 ? firstIndex - 1 : firstIndex} -{" "} + {lastIndex && lastIndex > 1 ? lastIndex - 1 : lastIndex} + + )} + itemCount={count + (page - 1) * max + (hasNext ? 1 : 0)} + page={page} + perPage={max} + onNextClick={(_, p) => onNextClick((p - 1) * max)} + onPreviousClick={(_, p) => onPreviousClick((p - 1) * max)} + onPerPageSelect={(_, m, f) => onPerPageSelect(f - 1, m)} + /> + + + + ); +}; diff --git a/js/apps/account-ui/src/api/methods.ts b/js/apps/account-ui/src/api/methods.ts index a40fe8a027..ef9f6da509 100644 --- a/js/apps/account-ui/src/api/methods.ts +++ b/js/apps/account-ui/src/api/methods.ts @@ -109,8 +109,22 @@ export async function getCredentials({ signal, context }: CallOptions) { return parseResponse(response); } -export async function getLinkedAccounts({ signal, context }: CallOptions) { - const response = await request("/linked-accounts", context, { signal }); +export type LinkedAccountQueryParams = PaginationParams & { + search?: string; + linked?: boolean; +}; + +export async function getLinkedAccounts( + { signal, context }: CallOptions, + query: LinkedAccountQueryParams, +) { + const response = await request("/linked-accounts", context, { + searchParams: Object.entries(query).reduce( + (acc, [key, value]) => ({ ...acc, [key]: value.toString() }), + {}, + ), + signal, + }); return parseResponse(response); }