Skip to content

Commit

Permalink
Implement remote sorting
Browse files Browse the repository at this point in the history
  • Loading branch information
amonsosanz committed Jan 30, 2024
1 parent dac42ee commit 9fec695
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 25 deletions.
12 changes: 10 additions & 2 deletions ui/src/adapters/api/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import axios from "axios";
import { z } from "zod";

import { Response, buildErrorResponse, buildSuccessResponse } from "src/adapters";
import { Message, buildAuthorizationHeader, messageParser } from "src/adapters/api";
import {
Message,
Sorter,
buildAuthorizationHeader,
messageParser,
serializeSorters,
} from "src/adapters/api";
import { credentialParser } from "src/adapters/api/credentials";
import {
datetimeParser,
Expand Down Expand Up @@ -57,7 +63,7 @@ export async function getConnection({
export async function getConnections({
credentials,
env,
params: { maxResults, page, query },
params: { maxResults, page, query, sorters },
signal,
}: {
credentials: boolean;
Expand All @@ -66,6 +72,7 @@ export async function getConnections({
maxResults?: number;
page?: number;
query?: string;
sorters?: Sorter[];
};
signal?: AbortSignal;
}): Promise<Response<Resource<Connection>>> {
Expand All @@ -81,6 +88,7 @@ export async function getConnections({
...(credentials ? { credentials: "true" } : {}),
...(maxResults !== undefined ? { max_results: maxResults.toString() } : {}),
...(page !== undefined ? { page: page.toString() } : {}),
...(sorters !== undefined && sorters.length ? { sort: serializeSorters(sorters) } : {}),
}),
signal,
url: `${API_VERSION}/connections`,
Expand Down
14 changes: 12 additions & 2 deletions ui/src/adapters/api/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import axios from "axios";
import { z } from "zod";

import { Response, buildErrorResponse, buildSuccessResponse } from "src/adapters";
import { ID, IDParser, Message, buildAuthorizationHeader, messageParser } from "src/adapters/api";
import {
ID,
IDParser,
Message,
Sorter,
buildAuthorizationHeader,
messageParser,
serializeSorters,
} from "src/adapters/api";
import {
datetimeParser,
getListParser,
Expand Down Expand Up @@ -103,7 +111,7 @@ export async function getCredential({

export async function getCredentials({
env,
params: { did, maxResults, page, query, status },
params: { did, maxResults, page, query, sorters, status },
signal,
}: {
env: Env;
Expand All @@ -112,6 +120,7 @@ export async function getCredentials({
maxResults?: number;
page?: number;
query?: string;
sorters?: Sorter[];
status?: CredentialStatus;
};
signal?: AbortSignal;
Expand All @@ -129,6 +138,7 @@ export async function getCredentials({
...(status !== undefined && status !== "all" ? { [STATUS_SEARCH_PARAM]: status } : {}),
...(maxResults !== undefined ? { max_results: maxResults.toString() } : {}),
...(page !== undefined ? { page: page.toString() } : {}),
...(sorters !== undefined && sorters.length ? { sort: serializeSorters(sorters) } : {}),
}),
signal,
url: `${API_VERSION}/credentials`,
Expand Down
12 changes: 12 additions & 0 deletions ui/src/adapters/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { z } from "zod";
import { getStrictParser } from "src/adapters/parsers";
import { Env } from "src/domain";

export type Sorter = { field: string; order?: "ascend" | "descend" };

export const sorterParser = getStrictParser<Sorter>()(
z.object({
field: z.string(),
order: z.union([z.literal("ascend"), z.literal("descend")]).optional(),
})
);

export const serializeSorters = (sorters: Sorter[]) =>
sorters.map(({ field, order }) => `${order === "descend" ? "-" : ""}${field}`).join(",");

export type ID = {
id: string;
};
Expand Down
4 changes: 2 additions & 2 deletions ui/src/adapters/parsers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function getListParser<Input, Output = Input>(
);
}

const metaParser = getStrictParser<ResourceMeta>()(
const resourceMetaParser = getStrictParser<ResourceMeta>()(
z.object({
max_results: z.number().int().min(1),
page: z.number().int().min(1),
Expand All @@ -52,7 +52,7 @@ export function getResourceParser<Input, Output = Input>(
>()(
z.object({
items: getListParser(parser),
meta: metaParser,
meta: resourceMetaParser,
})
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dayjs, { isDayjs } from "dayjs";
import { z } from "zod";

import { Sorter, sorterParser } from "src/adapters/api";
import { CreateCredential, CreateLink } from "src/adapters/api/credentials";
import { jsonParser } from "src/adapters/json";
import { getStrictParser } from "src/adapters/parsers";
Expand Down Expand Up @@ -42,6 +43,24 @@ export type CredentialLinkIssuance = CredentialIssuance & {
};

// Parsers

export const tableSorterParser = getStrictParser<Sorter | unknown[], Sorter[]>()(
z.union([
z
.unknown()
.array()
.transform((unknowns) =>
unknowns.reduce((acc: Sorter[], curr): Sorter[] => {
const parsedSorter = sorterParser.safeParse(curr);
return parsedSorter.success && parsedSorter.data.order !== undefined
? [...acc, parsedSorter.data]
: acc;
}, [])
),
sorterParser.transform((sorter) => (sorter.order !== undefined ? [sorter] : [])),
])
);

export const dayjsInstanceParser = getStrictParser<dayjs.Dayjs>()(
z.custom<dayjs.Dayjs>(isDayjs, {
message: "The provided input is not a valid Dayjs instance",
Expand Down
23 changes: 19 additions & 4 deletions ui/src/components/connections/ConnectionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
import { useCallback, useEffect, useState } from "react";
import { generatePath, useNavigate, useSearchParams } from "react-router-dom";

import { Sorter } from "src/adapters/api";
import { getConnections } from "src/adapters/api/connections";
import { positiveIntegerFromStringParser } from "src/adapters/parsers";
import { tableSorterParser } from "src/adapters/parsers/view";
import IconCreditCardPlus from "src/assets/icons/credit-card-plus.svg?react";
import IconDots from "src/assets/icons/dots-vertical.svg?react";
import IconInfoCircle from "src/assets/icons/info-circle.svg?react";
Expand Down Expand Up @@ -67,6 +69,7 @@ export function ConnectionsTable() {
);

const [paginationTotal, setPaginationTotal] = useState<number>(DEFAULT_PAGINATION_TOTAL);
const [sorters, setSorters] = useState<Sorter[]>();

const paginationPage = paginationPageParsed.success
? paginationPageParsed.data
Expand All @@ -87,7 +90,9 @@ export function ConnectionsTable() {
<Typography.Text strong>{userID.split(":").pop()}</Typography.Text>
</Tooltip>
),
sorter: ({ id: a }, { id: b }) => a.localeCompare(b),
sorter: {
multiple: 1,
},
title: IDENTIFIER,
},
{
Expand Down Expand Up @@ -180,14 +185,20 @@ export function ConnectionsTable() {

const fetchConnections = useCallback(
async (signal?: AbortSignal) => {
setConnections({ status: "loading" });
setConnections((previousConnections) =>
isAsyncTaskDataAvailable(previousConnections)
? { data: previousConnections.data, status: "reloading" }
: { status: "loading" }
);

const response = await getConnections({
credentials: true,
env,
params: {
maxResults: paginationMaxResults,
page: paginationPage,
query: queryParam || undefined,
sorters,
},
signal,
});
Expand All @@ -208,7 +219,7 @@ export function ConnectionsTable() {
}
}
},
[env, paginationMaxResults, paginationPage, queryParam, updatePaginationParams]
[env, paginationMaxResults, paginationPage, queryParam, sorters, updatePaginationParams]
);

const onSearch = useCallback(
Expand Down Expand Up @@ -287,7 +298,11 @@ export function ConnectionsTable() {
<NoResults searchQuery={queryParam} />
),
}}
onChange={({ current, pageSize, total }) => {
onChange={({ current, pageSize, total }, _, sorters) => {
const parsedSorters = tableSorterParser.safeParse(sorters);
if (parsedSorters.success) {
setSorters(parsedSorters.data);
}
setPaginationTotal(total || DEFAULT_PAGINATION_TOTAL);
updatePaginationParams({ maxResults: pageSize, page: current });
}}
Expand Down
33 changes: 21 additions & 12 deletions ui/src/components/credentials/CredentialsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import dayjs from "dayjs";
import { useCallback, useEffect, useState } from "react";
import { Link, generatePath, useNavigate, useSearchParams } from "react-router-dom";

import { Sorter } from "src/adapters/api";
import { credentialStatusParser, getCredentials } from "src/adapters/api/credentials";
import { positiveIntegerFromStringParser } from "src/adapters/parsers";
import { tableSorterParser } from "src/adapters/parsers/view";
import IconCreditCardPlus from "src/assets/icons/credit-card-plus.svg?react";
import IconCreditCardRefresh from "src/assets/icons/credit-card-refresh.svg?react";
import IconDots from "src/assets/icons/dots-vertical.svg?react";
Expand Down Expand Up @@ -82,6 +84,7 @@ export function CredentialsTable() {
);

const [paginationTotal, setPaginationTotal] = useState<number>(DEFAULT_PAGINATION_TOTAL);
const [sorters, setSorters] = useState<Sorter[]>();

const paginationPage = paginationPageParsed.success
? paginationPageParsed.data
Expand All @@ -104,7 +107,9 @@ export function CredentialsTable() {
<Typography.Text strong>{schemaType}</Typography.Text>
</Tooltip>
),
sorter: ({ schemaType: a }, { schemaType: b }) => a.localeCompare(b),
sorter: {
multiple: 1,
},
title: "Credential",
},
{
Expand All @@ -113,7 +118,9 @@ export function CredentialsTable() {
render: (createdAt: Credential["createdAt"]) => (
<Typography.Text>{formatDate(createdAt)}</Typography.Text>
),
sorter: ({ createdAt: a }, { createdAt: b }) => a.getTime() - b.getTime(),
sorter: {
multiple: 2,
},
title: ISSUE_DATE,
},
{
Expand All @@ -130,14 +137,8 @@ export function CredentialsTable() {
"-"
),
responsive: ["md"],
sorter: ({ expiresAt: a }, { expiresAt: b }) => {
if (a && b) {
return a.getTime() - b.getTime();
} else if (a) {
return -1;
} else {
return 1;
}
sorter: {
multiple: 3,
},
title: EXPIRATION,
},
Expand All @@ -148,7 +149,9 @@ export function CredentialsTable() {
<Typography.Text>{revoked ? "Revoked" : "-"}</Typography.Text>
),
responsive: ["sm"],
sorter: ({ revoked: a }, { revoked: b }) => (a === b ? 0 : a ? 1 : -1),
sorter: {
multiple: 4,
},
title: REVOCATION,
},
{
Expand Down Expand Up @@ -236,6 +239,7 @@ export function CredentialsTable() {
maxResults: paginationMaxResults,
page: paginationPage,
query: queryParam || undefined,
sorters,
status: credentialStatus,
},
signal,
Expand All @@ -262,6 +266,7 @@ export function CredentialsTable() {
paginationMaxResults,
paginationPage,
queryParam,
sorters,
credentialStatus,
updatePaginationParams,
]
Expand Down Expand Up @@ -356,7 +361,11 @@ export function CredentialsTable() {
<NoResults searchQuery={queryParam} />
),
}}
onChange={({ current, pageSize, total }) => {
onChange={({ current, pageSize, total }, _, sorters) => {
const parsedSorters = tableSorterParser.safeParse(sorters);
if (parsedSorters.success) {
setSorters(parsedSorters.data);
}
setPaginationTotal(total || DEFAULT_PAGINATION_TOTAL);
updatePaginationParams({ maxResults: pageSize, page: current });
}}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/credentials/IssuanceMethodForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import dayjs from "dayjs";
import { useCallback, useEffect, useState } from "react";

import { getConnections } from "src/adapters/api/connections";
import { IssuanceMethodFormData, issuanceMethodFormDataParser } from "src/adapters/parsers/forms";
import { IssuanceMethodFormData, issuanceMethodFormDataParser } from "src/adapters/parsers/view";
import IconRight from "src/assets/icons/arrow-narrow-right.svg?react";
import { useEnvContext } from "src/contexts/Env";
import { AppError, Connection } from "src/domain";
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/credentials/IssueCredential.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
credentialFormParser,
serializeCredentialIssuance,
serializeCredentialLinkIssuance,
} from "src/adapters/parsers/forms";
} from "src/adapters/parsers/view";
import { IssuanceMethodForm } from "src/components/credentials/IssuanceMethodForm";
import { IssueCredentialForm } from "src/components/credentials/IssueCredentialForm";
import { Summary } from "src/components/credentials/Summary";
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/credentials/IssueCredentialForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
IssueCredentialFormData,
dayjsInstanceParser,
serializeSchemaForm,
} from "src/adapters/parsers/forms";
} from "src/adapters/parsers/view";
import IconBack from "src/assets/icons/arrow-narrow-left.svg?react";
import IconRight from "src/assets/icons/arrow-narrow-right.svg?react";
import IconCheckMark from "src/assets/icons/check.svg?react";
Expand Down

0 comments on commit 9fec695

Please sign in to comment.