Skip to content

Commit

Permalink
Identity Providers Pagination in account console
Browse files Browse the repository at this point in the history
Closes keycloak#21261

Signed-off-by: Andreas Kozadinos <[email protected]>
  • Loading branch information
linathedog committed Sep 13, 2024
1 parent aec3eb9 commit f0cddfe
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 30 deletions.
140 changes: 112 additions & 28 deletions js/apps/account-ui/src/account-security/LinkedAccounts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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 { LinkedAccountRepresentation } from "../api/representations";
Expand All @@ -8,27 +8,41 @@ 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 "../resources/LinkedAccountsToolbar";

export const LinkedAccounts = () => {
const { t } = useTranslation();
const context = useEnvironment();
const [accounts, setAccounts] = useState<LinkedAccountRepresentation[]>([]);
const [linkedAccounts, setLinkedAccounts] = useState<
LinkedAccountRepresentation[]
>([]);
const [unlinkedAccounts, setUninkedAccounts] = useState<
LinkedAccountRepresentation[]
>([]);

const [paramsUnlinked, setParamsUnlinked] = useState<Record<string, any>>({
first: "0",
max: "6",
linked: false,
});
const [paramsLinked, setParamsLinked] = useState<Record<string, any>>({
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 (
Expand All @@ -41,16 +55,49 @@ export const LinkedAccounts = () => {
<Title headingLevel="h2" className="pf-v5-u-mb-lg" size="xl">
{t("linkedLoginProviders")}
</Title>
<LinkedAccountsToolbar
onFilter={(search) =>
setParamsLinked({ ...paramsLinked, first: 0, search })
}
count={linkedAccounts.length}
first={parseInt(paramsLinked["first"])}
max={parseInt(paramsLinked["max"])}
onNextClick={() => {
setParamsLinked({
...paramsLinked,
first:
parseInt(paramsLinked.first) + parseInt(paramsLinked.max) - 1,
});
}}
onPreviousClick={() =>
setParamsLinked({
...paramsLinked,
first:
parseInt(paramsLinked.first) - parseInt(paramsLinked.max) + 1,
})
}
onPerPageSelect={(first, max) =>
setParamsLinked({
...paramsLinked,
first: `${first}`,
max: `${max}`,
})
}
hasNext={linkedAccounts.length > paramsLinked.max - 1}
/>
<DataList id="linked-idps" aria-label={t("linkedLoginProviders")}>
{linkedAccounts.length > 0 ? (
linkedAccounts.map((account) => (
<AccountRow
key={account.providerName}
account={account}
isLinked
refresh={refresh}
/>
))
linkedAccounts.map(
(account, index) =>
index !== paramsLinked.max - 1 && (
<AccountRow
key={account.providerName}
account={account}
isLinked
refresh={refresh}
/>
),
)
) : (
<EmptyRow message={t("linkedEmpty")} />
)}
Expand All @@ -64,15 +111,52 @@ export const LinkedAccounts = () => {
>
{t("unlinkedLoginProviders")}
</Title>
<LinkedAccountsToolbar
onFilter={(search) =>
setParamsUnlinked({ ...paramsUnlinked, first: 0, search })
}
count={unlinkedAccounts.length}
first={parseInt(paramsUnlinked["first"])}
max={parseInt(paramsUnlinked["max"])}
onNextClick={() => {
setParamsUnlinked({
...paramsUnlinked,
first:
parseInt(paramsUnlinked.first) +
parseInt(paramsUnlinked.max) -
1,
});
}}
onPreviousClick={() =>
setParamsUnlinked({
...paramsUnlinked,
first:
parseInt(paramsUnlinked.first) -
parseInt(paramsUnlinked.max) +
1,
})
}
onPerPageSelect={(first, max) =>
setParamsUnlinked({
...paramsUnlinked,
first: `${first}`,
max: `${max}`,
})
}
hasNext={unlinkedAccounts.length > paramsUnlinked.max - 1}
/>
<DataList id="unlinked-idps" aria-label={t("unlinkedLoginProviders")}>
{unLinkedAccounts.length > 0 ? (
unLinkedAccounts.map((account) => (
<AccountRow
key={account.providerName}
account={account}
refresh={refresh}
/>
))
{unlinkedAccounts.length > 0 ? (
unlinkedAccounts.map(
(account, index) =>
index !== paramsUnlinked.max - 1 && (
<AccountRow
key={account.providerName}
account={account}
refresh={refresh}
/>
),
)
) : (
<EmptyRow message={t("unlinkedEmpty")} />
)}
Expand Down
10 changes: 8 additions & 2 deletions js/apps/account-ui/src/api/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,14 @@ export async function getCredentials({ signal, context }: CallOptions) {
return parseResponse<CredentialContainer[]>(response);
}

export async function getLinkedAccounts({ signal, context }: CallOptions) {
const response = await request("/linked-accounts", context, { signal });
export async function getLinkedAccounts(
{ signal, context }: CallOptions,
requestParams: Record<string, any>,
) {
const response = await request("/linked-accounts", context, {
searchParams: requestParams,
signal,
});
return parseResponse<LinkedAccountRepresentation[]>(response);
}

Expand Down
88 changes: 88 additions & 0 deletions js/apps/account-ui/src/resources/LinkedAccountsToolbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<SearchInput
placeholder={t("filterByName")}
aria-label={t("filterByName")}
value={nameFilter}
onChange={(_, value) => {
setNameFilter(value);
}}
onSearch={() => onFilter(nameFilter)}
onKeyDown={(e) => {
if (e.key === "Enter") {
onFilter(nameFilter);
}
}}
onClear={() => {
setNameFilter("");
onFilter("");
}}
/>
</ToolbarItem>
<ToolbarItem variant="pagination">
<Pagination
isCompact
perPageOptions={[
{ title: "5", value: 6 },
{ title: "10", value: 11 },
{ title: "20", value: 21 },
]}
toggleTemplate={({
firstIndex,
lastIndex,
}: PaginationToggleTemplateProps) => (
<b>
{firstIndex && firstIndex > 1 ? firstIndex - 1 : firstIndex} -{" "}
{lastIndex && lastIndex > 1 ? lastIndex - 1 : lastIndex}
</b>
)}
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)}
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
);
};

0 comments on commit f0cddfe

Please sign in to comment.