Skip to content

Commit abf37b5

Browse files
authored
Dashboard on one page (#3185)
* Dashboard on one page * Fix E2E * Fix E2E * Fix footer background * Remove unused path
1 parent 43f877d commit abf37b5

File tree

9 files changed

+249
-132
lines changed

9 files changed

+249
-132
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<span class="relative inline-block aspect-square h-[1em] align-middle">
2+
<svg
3+
xmlns="http://www.w3.org/2000/svg"
4+
viewBox="0 0 24 24"
5+
fill="currentColor"
6+
class="pulsating"
7+
>
8+
<circle cx="12" cy="12" r="10" />
9+
</svg>
10+
</span>
11+
12+
<style>
13+
.pulsating {
14+
animation: pulsate 2s infinite;
15+
fill: var(--color-success-500);
16+
}
17+
18+
@keyframes pulsate {
19+
0% {
20+
fill-opacity: 1;
21+
}
22+
50% {
23+
fill-opacity: 0.5;
24+
}
25+
100% {
26+
fill-opacity: 1;
27+
}
28+
}
29+
</style>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<script lang="ts">
2+
import { type Snippet } from "svelte";
3+
4+
type Props = {
5+
content: Snippet;
6+
header: Snippet;
7+
footer: Snippet;
8+
};
9+
10+
const { content, header, footer }: Props = $props();
11+
</script>
12+
13+
<div class="sidebar-layout">
14+
<div
15+
class="bg-bg-primary_alt col-start-1 col-end-4 row-start-1 row-end-6"
16+
></div>
17+
<div class="col-start-2 col-end-3 row-start-3 row-end-4 p-4 md:p-0">
18+
{@render content?.()}
19+
</div>
20+
<header class="col-start-2 col-end-5 row-start-1 row-end-2 pt-2 pr-0 md:pr-6">
21+
{@render header?.()}
22+
</header>
23+
<footer class="col-start-1 col-end-5 row-start-5 row-end-6 px-4 py-4 md:px-6">
24+
{@render footer?.()}
25+
</footer>
26+
</div>
27+
28+
<style>
29+
.sidebar-layout {
30+
display: grid;
31+
grid-template-columns: 1fr 4fr 1fr;
32+
33+
grid-template-rows: min-content 1fr max-content 1fr min-content;
34+
min-height: 100dvh;
35+
min-width: 100dvw;
36+
}
37+
38+
@media (max-width: 768px) {
39+
.sidebar-layout {
40+
display: grid;
41+
grid-template-columns: 0fr 1fr 0fr;
42+
43+
grid-template-rows: min-content 1fr max-content 1fr min-content;
44+
min-height: 100dvh;
45+
min-width: 100dvw;
46+
}
47+
}
48+
</style>

src/frontend/src/lib/components/ui/AccessMethod.svelte

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
import { fade } from "svelte/transition";
99
import PlaceHolder from "./PlaceHolder.svelte";
1010
import Ellipsis from "../utils/Ellipsis.svelte";
11+
import PulsatingCircleIcon from "../icons/PulsatingCircleIcon.svelte";
1112
1213
let {
1314
accessMethod,
1415
class: classes,
16+
isCurrent,
1517
}: {
1618
accessMethod: AuthnMethodData | OpenIdCredential | null;
1719
class?: string;
20+
isCurrent?: boolean;
1821
} = $props();
1922
2023
const getAuthnMethodAlias = (authnMethod: AuthnMethodData) => {
@@ -73,9 +76,22 @@
7376
>
7477
<div class="flex min-w-32 items-center pr-3">
7578
{getAuthnMethodAlias(accessMethod)}
79+
{#if isCurrent}
80+
<span class="ml-2">
81+
<PulsatingCircleIcon />
82+
</span>
83+
{/if}
7684
</div>
77-
{#if nonNullish(accessMethod.last_authentication[0])}
78-
<div class="text-text-tertiary flex items-center font-normal">
85+
{#if isCurrent}
86+
<div
87+
class="text-text-tertiary flex items-center font-normal md:justify-end"
88+
>
89+
<span>Last used now</span>
90+
</div>
91+
{:else if nonNullish(accessMethod.last_authentication[0])}
92+
<div
93+
class="text-text-tertiary flex items-center font-normal md:justify-end"
94+
>
7995
Last used {formatLastUsage(
8096
new Date(
8197
Number(accessMethod.last_authentication[0] / BigInt(1000000)),
@@ -96,23 +112,46 @@
96112
>
97113
{#if openIdHasName && openIdHasEmail}
98114
<div class="flex min-w-32 flex-col justify-center pr-3">
99-
<div>{getOpenIdCredentialName(accessMethod)}</div>
115+
<div class="flex items-center gap-2">
116+
<span>
117+
{getOpenIdCredentialName(accessMethod)}
118+
</span>
119+
{#if isCurrent}
120+
<PulsatingCircleIcon />
121+
{/if}
122+
</div>
100123
<div class="text-text-tertiary font-extralight">
101124
<Ellipsis text={getOpenIdCredentialEmail(accessMethod)!}></Ellipsis>
102125
</div>
103126
</div>
104127
{:else if !openIdHasName && openIdHasEmail}
105-
<div class="flex min-w-32 items-center pr-3">
128+
<div class="flex min-w-32 items-center gap-2 pr-3">
106129
<Ellipsis text={getOpenIdCredentialEmail(accessMethod)!}></Ellipsis>
130+
{#if isCurrent}
131+
<PulsatingCircleIcon />
132+
{/if}
107133
</div>
108134
{:else if openIdHasName && !openIdHasEmail}
109-
<div class="min-w-32 pr-3">
110-
{getOpenIdCredentialName(accessMethod)}
135+
<div class="flex min-w-32 items-center gap-2 pr-3">
136+
<span>
137+
{getOpenIdCredentialName(accessMethod)}
138+
</span>
139+
{#if isCurrent}
140+
<PulsatingCircleIcon />
141+
{/if}
111142
</div>
112143
{/if}
113144

114-
{#if nonNullish(accessMethod.last_usage_timestamp[0])}
115-
<div class="text-text-tertiary flex items-center font-normal">
145+
{#if isCurrent}
146+
<div
147+
class="text-text-tertiary flex items-center font-normal md:justify-end"
148+
>
149+
Last used now
150+
</div>
151+
{:else if nonNullish(accessMethod.last_usage_timestamp[0])}
152+
<div
153+
class="text-text-tertiary flex items-center font-normal md:justify-end"
154+
>
116155
Last used {formatLastUsage(
117156
new Date(
118157
Number(accessMethod.last_usage_timestamp[0] / BigInt(1000000)),
Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,22 @@
1818
import { nonNullish } from "@dfinity/utils";
1919
import { handleError } from "$lib/components/utils/error";
2020
import AddAccessMethodWizard from "$lib/components/wizards/AddAccessMethodWizard.svelte";
21+
import {
22+
getLastUsedAccessMethod,
23+
isWebAuthnMetaData,
24+
} from "$lib/utils/accessMethods";
25+
import { authnMethodEqual } from "$lib/utils/webAuthn";
2126
2227
const MAX_PASSKEYS = 8;
2328
2429
let isAddAccessMethodWizardOpen = $state(false);
2530
31+
const lastUsedAccessMethod = $derived(
32+
getLastUsedAccessMethod(
33+
identityInfo.authnMethods,
34+
identityInfo.openIdCredentials,
35+
),
36+
);
2637
const openIdCredentials = $derived(identityInfo.openIdCredentials);
2738
const authnMethods = $derived(identityInfo.authnMethods);
2839
const isMaxOpenIdCredentialsReached = $derived(
@@ -83,10 +94,6 @@
8394
};
8495
</script>
8596

86-
<h1 class="text-text-primary mb-4 text-3xl font-semibold">Security</h1>
87-
<p class="text-text-tertiary text-md mb-12">
88-
Settings and recommendations to keep your identity secure
89-
</p>
9097
<Panel>
9198
<div class="flex flex-col justify-between gap-5 p-4 pb-5 md:flex-row">
9299
<div>
@@ -122,7 +129,12 @@
122129
>
123130
<PasskeyIcon />
124131
</div>
125-
<AccessMethod accessMethod={authnMethod} />
132+
<AccessMethod
133+
accessMethod={authnMethod}
134+
isCurrent={nonNullish(lastUsedAccessMethod) &&
135+
isWebAuthnMetaData(lastUsedAccessMethod) &&
136+
authnMethodEqual(authnMethod, lastUsedAccessMethod)}
137+
/>
126138
<div class="flex items-center justify-center pr-4">
127139
{#if isRemoveAccessMethodVisible}
128140
<Button
@@ -147,7 +159,12 @@
147159
<GoogleIcon />
148160
</div>
149161

150-
<AccessMethod accessMethod={credential} />
162+
<AccessMethod
163+
accessMethod={credential}
164+
isCurrent={nonNullish(lastUsedAccessMethod) &&
165+
!isWebAuthnMetaData(lastUsedAccessMethod) &&
166+
lastUsedAccessMethod.sub === credential.sub}
167+
/>
151168

152169
<div class="flex items-center justify-center pr-4">
153170
{#if isRemoveAccessMethodVisible}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script lang="ts">
2+
import Panel from "$lib/components/ui/Panel.svelte";
3+
import identityInfo from "$lib/stores/identity-info.state.svelte";
4+
import Tooltip from "$lib/components/ui/Tooltip.svelte";
5+
import { InfoIcon } from "@lucide/svelte";
6+
import PlaceHolder from "$lib/components/ui/PlaceHolder.svelte";
7+
import { fade } from "svelte/transition";
8+
</script>
9+
10+
<Panel>
11+
<div class="p-4">
12+
<h3 class="text-text-primary mb-2 text-lg font-semibold">My Identity</h3>
13+
<h4 class="text-text-tertiary text-sm">
14+
Internet Identity is used to sign in securely and connect to apps with
15+
passkeys.
16+
</h4>
17+
</div>
18+
<div class="grid grid-cols-[1fr_2fr_min-content]">
19+
<div
20+
class="border-border-tertiary col-span-3 grid grid-cols-subgrid border-t px-4 py-4"
21+
>
22+
<h5
23+
class="text-text-tertiary flex min-w-30 items-center pr-2 text-sm md:pr-4"
24+
>
25+
Identity Name
26+
</h5>
27+
<div class="flex items-center">
28+
{#if identityInfo.name}
29+
<h5
30+
class="text-text-primary text-sm font-semibold nth-[2]:hidden"
31+
transition:fade={{ delay: 30 }}
32+
>
33+
{identityInfo.name}
34+
</h5>
35+
{:else}
36+
<PlaceHolder class="mr-8 h-4 w-full !rounded-sm" />
37+
{/if}
38+
</div>
39+
<div class="flex items-center justify-center">
40+
<Tooltip
41+
label="Your Identity name is currently not editable. It is only ever visible to you."
42+
><InfoIcon
43+
size="20"
44+
class="text-text-primary stroke-fg-tertiary"
45+
/></Tooltip
46+
>
47+
</div>
48+
</div>
49+
</div>
50+
</Panel>

src/frontend/src/lib/utils/accessMethods.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import {
44
} from "$lib/generated/internet_identity_types";
55
import { isNullish, nonNullish } from "@dfinity/utils";
66

7+
/**
8+
* Check if a `AuthnMethodData` or `OpenIdCredential` is a WebAuthn method
9+
*/
10+
export const isWebAuthnMetaData = (
11+
accessMethod: AuthnMethodData | OpenIdCredential,
12+
): accessMethod is AuthnMethodData =>
13+
"authn_method" in accessMethod && "WebAuthn" in accessMethod.authn_method;
14+
715
export const getLastUsedAccessMethod = (
816
authnMethods: AuthnMethodData[],
917
openIdCredentials: OpenIdCredential[],

0 commit comments

Comments
 (0)