Fixed the types of Andreas original (#33019)

* Fixed the types of Andreas original

in favour of: #32913
fixes: #21261

Co-authored-by: Andreas Kozadinos <koza-sparrow@hotmail.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* better types

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Co-authored-by: Andreas Kozadinos <koza-sparrow@hotmail.com>
This commit is contained in:
Erik Jan de Wit 2024-09-23 07:43:24 +02:00 committed by GitHub
parent d01f531b82
commit 75c548f862
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 211 additions and 32 deletions

View file

@ -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<LinkedAccountRepresentation[]>([]);
const [linkedAccounts, setLinkedAccounts] = useState<
LinkedAccountRepresentation[]
>([]);
const [unlinkedAccounts, setUninkedAccounts] = useState<
LinkedAccountRepresentation[]
>([]);
const [paramsUnlinked, setParamsUnlinked] =
useState<LinkedAccountQueryParams>({
first: 0,
max: 6,
linked: false,
});
const [paramsLinked, setParamsLinked] = useState<LinkedAccountQueryParams>({
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 = () => {
<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={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}
/>
<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")} />
)}
@ -64,15 +110,46 @@ export const LinkedAccounts = () => {
>
{t("unlinkedLoginProviders")}
</Title>
<LinkedAccountsToolbar
onFilter={(search) =>
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}
/>
<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")} />
)}

View file

@ -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>
);
};

View file

@ -109,8 +109,22 @@ 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 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<LinkedAccountRepresentation[]>(response);
}