Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix(dashboard-for-dapps): dapps dashboard fixes #411

Merged
merged 7 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/dashboard-for-dapps/src/components/secret-key-prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function SecretKeyPrompt({
open: boolean;
toggle: (value?: boolean) => void;
onSubmit: (key: string, validKey: boolean) => void;
credentialSample?: idOSCredential;
credentialSample?: idOSCredential | null;
}) {
const ref = useRef<HTMLInputElement>(null);
const [key, setKey] = useState("");
Expand All @@ -47,8 +47,10 @@ export function SecretKeyPrompt({
const isValid = checkForValidity();
setHasError(!isValid);
setLoading(false);
onSubmit(key, isValid);
if (isValid) toggle(false);
if (isValid) {
onSubmit(key, isValid);
toggle(false);
}
};

const resetState = () => {
Expand Down
1 change: 1 addition & 0 deletions apps/dashboard-for-dapps/src/idOS.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function Provider({ children }: PropsWithChildren) {
nodeUrl: import.meta.env.VITE_IDOS_NODE_URL,
enclaveOptions: {
container: "#idOS-enclave",
url: import.meta.env.VITE_IDOS_ENCLAVE_URL,
},
});

Expand Down
27 changes: 17 additions & 10 deletions apps/dashboard-for-dapps/src/routes/credentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@chakra-ui/react";
import { base64Encode } from "@idos-network/codecs";
import type { idOSCredential } from "@idos-network/idos-sdk";
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useToggle } from "@uidotdev/usehooks";
import { matchSorter } from "match-sorter";
Expand Down Expand Up @@ -55,7 +55,7 @@ export const Route = createFileRoute("/credentials")({
},
});

const safeParse = (json?: string) => {
export const safeParse = (json?: string) => {
try {
// biome-ignore lint/style/noNonNullAssertion: <explanation>
return JSON.parse(json!);
Expand Down Expand Up @@ -94,7 +94,7 @@ export const useDecryptAllCredentials = ({
const [secretKey] = useSecretKey();

return useQuery({
queryKey: ["credentials", secretKey],
queryKey: ["credentials", secretKey, credentials.length],
queryFn: async () => {
const promiseList = credentials?.map(async (credential) => {
if (!credential) return null;
Expand Down Expand Up @@ -419,13 +419,15 @@ function SearchResults({
}

function Credentials() {
const queryClient = useQueryClient();
const navigate = useNavigate({ from: Route.fullPath });
const { filter = "" } = Route.useSearch();
const deferredSearchItem = useDeferredValue(filter);
const [, setSecretKey] = useSecretKey();

const credentialsList = useListCredentials();
const credentials = useDecryptAllCredentials({

const decryptedCredentials = useDecryptAllCredentials({
enabled: !!credentialsList.data?.length,
credentials: credentialsList.data ?? [],
});
Expand All @@ -443,18 +445,23 @@ function Credentials() {
});
};

const handleRefresh = async () => {
await queryClient.invalidateQueries({ queryKey: ["credentials", "credentials-list"] });
credentialsList.refetch();
};

const onKeySubmit = (secretKey: string) => {
setSecretKey(secretKey);
};

const results = useMemo(() => {
if (!credentials.data) return [];
if (!deferredSearchItem) return credentials.data;
return matchSorter(credentials.data || [], deferredSearchItem, {
if (!decryptedCredentials.data) return [];
if (!deferredSearchItem) return decryptedCredentials.data;
return matchSorter(decryptedCredentials.data || [], deferredSearchItem, {
keys: ["public_notes", "content"],
threshold: matchSorter.rankings.CONTAINS,
});
}, [deferredSearchItem, credentials.data]);
}, [deferredSearchItem, decryptedCredentials.data]);

return (
<Container h="100%">
Expand All @@ -466,7 +473,7 @@ function Credentials() {
<Spinner />
<Text>Fetching credentials...</Text>
</Center>
) : credentials.isFetching ? (
) : decryptedCredentials.isFetching ? (
<Center h="100%" flexDirection="column" gap="2">
<Spinner />
<Text>Decrypting credentials...</Text>
Expand Down Expand Up @@ -500,7 +507,7 @@ function Credentials() {
title="Refresh credentials list"
variant="subtle"
colorPalette="gray"
onClick={() => credentials.refetch()}
onClick={handleRefresh}
/>
</HStack>
</HStack>
Expand Down
208 changes: 108 additions & 100 deletions apps/dashboard-for-dapps/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { changeCase, decrypt, openImageInNewTab } from "@/utils";

import { Pagination } from "@/components/pagination";
import { idOSContext, useIdOS } from "@/idOS.provider";
import { safeParse } from "./credentials";

export const Route = createFileRoute("/")({
component: Index,
Expand Down Expand Up @@ -78,6 +79,7 @@ const useFetchCredential = (id: string) => {
return useQuery({
queryKey: ["credential-details", id],
queryFn: id ? () => idOS.data.getShared<idOSCredential>("credentials", id, false) : skipToken,
enabled: !!id,
});
};

Expand All @@ -92,14 +94,17 @@ function CredentialDetails({
if (!credential.data || !secretKey) return null;

const result = decrypt(credential.data.content, credential.data.encryptor_public_key, secretKey);
const content = JSON.parse(result);

const content = result ? safeParse(result) : { credentialSubject: {} };
const hasValidContent = !!result;

const subject = Object.entries(content.credentialSubject).filter(
([key]) => !["emails", "wallets"].includes(key) && !key.endsWith("_file"),
) as [string, string][];

const emails = content.credentialSubject.emails;
const wallets = content.credentialSubject.wallets;
const emails: { address: string; verified: boolean }[] = content.credentialSubject?.emails || [];
const wallets: { address: string; currency: string; verified: boolean }[] =
content.credentialSubject?.wallets || [];
const files = (
Object.entries(content.credentialSubject).filter(([key]) => key.endsWith("_file")) as [
string,
Expand All @@ -122,63 +127,64 @@ function CredentialDetails({
<DrawerTitle>Credential details</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<Stack>
<DataListRoot orientation="horizontal" divideY="1px">
{subject.map(([key, value]) => (
{!hasValidContent ? (
<Text color="red.500">
Can't decrypt credential — please make sure you're using the right encryption key
</Text>
) : (
<Stack>
<DataListRoot orientation="horizontal" divideY="1px">
{subject.map(([key, value]) => (
<DataListItem
key={key}
pt="4"
grow
textTransform="uppercase"
label={changeCase(key)}
value={value}
/>
))}

<DataListItem
key={key}
pt="4"
grow
alignItems={{
base: "flex-start",
md: "center",
}}
flexDir={{
base: "column",
md: "row",
}}
textTransform="uppercase"
label={changeCase(key)}
value={value}
label="EMAILS"
value={
<List.Root align="center" gap="2">
{emails.map(({ address, verified }) => (
<List.Item key={address} alignItems="center" display="inline-flex">
{address}
{verified ? " (verified)" : ""}
</List.Item>
))}
</List.Root>
}
/>
))}

<DataListItem
pt="4"
grow
alignItems={{
base: "flex-start",
md: "center",
}}
flexDir={{
base: "column",
md: "row",
}}
textTransform="uppercase"
label="EMAILS"
value={
<List.Root align="center" gap="2">
{emails.map(({ address, verified }: { address: string; verified: boolean }) => (
<List.Item key={address} alignItems="center" display="inline-flex">
{address}
{verified ? " (verified)" : ""}
</List.Item>
))}
</List.Root>
}
/>
<DataListItem
pt="4"
grow
alignItems={{
base: "flex-start",
md: "center",
}}
flexDir={{
base: "column",
md: "row",
}}
textTransform="uppercase"
label="WALLETS"
value={
<List.Root align="center" gap="2">
{wallets.map(
({
address,
currency,
}: { address: string; currency: string; verified: boolean }) => (
<DataListItem
pt="4"
grow
alignItems={{
base: "flex-start",
md: "center",
}}
flexDir={{
base: "column",
md: "row",
}}
textTransform="uppercase"
label="WALLETS"
value={
<List.Root align="center" gap="2">
{wallets.map(({ address, currency }) => (
<List.Item
key={address}
display="inline-flex"
Expand All @@ -187,54 +193,54 @@ function CredentialDetails({
>
{address} ({currency})
</List.Item>
),
)}
</List.Root>
}
/>
{files.length > 0 ? (
<DataListItem
pt="4"
grow
alignItems="start"
flexDir="column"
label="FILES"
value={
<List.Root
variant="plain"
display="flex"
flexDirection="row"
gap="4"
overflowX="auto"
>
{files.map(([key, value]) => (
<List.Item
flexShrink="0"
key={key}
transition="transform 0.2s"
cursor="pointer"
_hover={{ transform: "scale(1.02)" }}
onClick={() => openImageInNewTab(value)}
>
<chakra.button className="button">
<Image
src={value}
alt="Image from credential"
rounded="md"
loading="lazy"
width="120px"
height="120px"
title="Click to open the image in full size"
/>
</chakra.button>
</List.Item>
))}
</List.Root>
}
/>
) : null}
</DataListRoot>
</Stack>
{files.length > 0 ? (
<DataListItem
pt="4"
grow
alignItems="start"
flexDir="column"
label="FILES"
value={
<List.Root
variant="plain"
display="flex"
flexDirection="row"
gap="4"
overflowX="auto"
>
{files.map(([key, value]) => (
<List.Item
flexShrink="0"
key={key}
transition="transform 0.2s"
cursor="pointer"
_hover={{ transform: "scale(1.02)" }}
onClick={() => openImageInNewTab(value)}
>
<chakra.button className="button">
<Image
src={value}
alt="Image from credential"
rounded="md"
loading="lazy"
width="120px"
height="120px"
title="Click to open the image in full size"
/>
</chakra.button>
</List.Item>
))}
</List.Root>
}
/>
) : null}
</DataListRoot>
</Stack>
)}
</DrawerBody>
<DrawerFooter>
<DrawerActionTrigger asChild>
Expand All @@ -256,6 +262,7 @@ function SearchResults({
const [openSecretKeyPrompt, toggleSecretKeyPrompt] = useToggle();
const [openCredentialDetails, toggleCredentialDetails] = useToggle();
const [secretKey, setSecretKey] = useSecretKey();
const credentialSample = useFetchCredential(results.records[0]?.dataId);

if (!results.records.length) {
return <EmptyState title="No results found" bg="gray.900" rounded="lg" />;
Expand Down Expand Up @@ -294,7 +301,7 @@ function SearchResults({
pt="4"
grow
label="ID"
value={grant.dataId}
value={grant.id}
truncate
/>
<DataListItem
Expand Down Expand Up @@ -359,6 +366,7 @@ function SearchResults({
page={page}
/>
<SecretKeyPrompt
credentialSample={credentialSample.data}
{...{ open: openSecretKeyPrompt, toggle: toggleSecretKeyPrompt, onSubmit: onKeySubmit }}
/>

Expand Down
Loading