Skip to content

Commit

Permalink
feat: dashboard sessions management screen (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
JackHamer09 authored Dec 4, 2024
1 parent 95d7b49 commit d1e2bfd
Show file tree
Hide file tree
Showing 32 changed files with 1,323 additions and 542 deletions.
108 changes: 62 additions & 46 deletions packages/auth-server/components/app/NavMobileMenu.vue
Original file line number Diff line number Diff line change
@@ -1,60 +1,76 @@
<template>
<client-only>
<Dialog.Root v-model:open="open">
<Dialog.Trigger>
<zk-button-icon icon="menu" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-neutral-950/50"
/>
<Dialog.Content
class="data-[state=open]:animate-slideDownAndFade fixed top-0 left-4 right-4 rounded-zk bg-white focus:outline-none z-[100] p-4 dark:bg-neutral-900 border border-transparent dark:border-neutral-800 dark:text-neutral-100 mt-4"
>
<Dialog.Title class="text-lg flex items-center mb-8">
<span class="flex-auto">Menu</span>
<app-color-mode />
<Dialog.Close
class="ml-2 p-2 inline-flex appearance-none items-center justify-center focus:outline-none focus:ring-1 rounded-full"
aria-label="Close"
>
<zk-icon icon="close" />
</Dialog.Close>
</Dialog.Title>
<Dialog.Description class="mb-10 text-lg text-center">
<slot />
</Dialog.Description>
<Dialog.Root v-model:open="open">
<Dialog.Trigger>
<zk-button-icon icon="menu" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-neutral-950/50"
/>
<Dialog.Content
class="data-[state=open]:animate-slideDownAndFade fixed top-0 left-4 right-4 rounded-zk bg-white focus:outline-none z-[100] p-4 dark:bg-neutral-950 border border-transparent dark:border-neutral-900 dark:text-neutral-100 mt-4"
>
<Dialog.Title class="text-lg flex items-center mb-8">
<span class="flex-auto">Menu</span>
<app-color-mode />
<Dialog.Close
class="ml-2 p-2 inline-flex appearance-none items-center justify-center focus:outline-none focus:ring-1 rounded-full"
aria-label="Close"
>
<zk-icon icon="close" />
</Dialog.Close>
</Dialog.Title>
<Dialog.Description class="mb-10 text-lg text-center">
<slot />
</Dialog.Description>

<div class="flex flex-col gap-2 justify-end">
<ZkLink
v-for="link in mainNav"
:key="link.href"
:href="link.href"
class="w-full"
type="secondary"
ui="justify-start dark:border-neutral-800 border-neutral-300"
@click="open = false"
>
<zk-icon
:icon="link.icon"
class="mr-1 -ml-1"
/> {{ link.name }}
</ZkLink>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</client-only>
<div class="flex flex-col gap-2 justify-end">
<ZkLink
v-for="link in mainNav"
:key="link.href"
:href="link.href"
class="w-full"
type="secondary"
ui="justify-start dark:border-neutral-800 border-neutral-300"
@click="open = false"
>
<zk-icon
:icon="link.icon"
class="mr-1 -ml-1"
/> {{ link.name }}
</ZkLink>
<ZkLink
as="button"
class="w-full"
type="secondary"
ui="justify-start dark:border-neutral-800 border-neutral-300 mt-4"
@click="logout()"
>
<zk-icon
icon="Logout"
class="mr-1 -ml-1 scale-[-1]"
/> Logout
</ZkLink>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</template>

<script setup lang="ts">
import { Dialog } from "radix-vue/namespaced";
const { mainNav } = useNav();
const { logout: _logout } = useAccountStore();
const open = ref(false);
const logout = () => {
_logout();
navigateTo("/");
};
</script>

<style lang="postcss" scoped>
<style lang="scss" scoped>
.router-link-exact-active {
@apply bg-neutral-950 text-neutral-100 hover:bg-neutral-800 hover:text-white active:bg-neutral-950 active:text-neutral-200 disabled:bg-neutral-700 disabled:text-neutral-400 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200 dark:hover:text-neutral-950 dark:focus:bg-neutral-100 dark:focus:text-neutral-950 dark:active:bg-neutral-300 dark:disabled:hover:bg-neutral-700 dark:disabled:hover:text-neutral-400;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/auth-server/components/app/nav.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div
class="border border-neutral-200 rounded-zk flex justify-between dark:border-neutral-700 dark:bg-neutral-900"
class="border border-neutral-200 rounded-zk flex justify-between dark:border-neutral-900 dark:bg-neutral-950"
>
<div class="flex items-center pl-3">
<NuxtLink to="/dashboard">
Expand Down Expand Up @@ -82,7 +82,7 @@ onBeforeUnmount(() => {
watch(windowWidth, checkWidths);
</script>

<style lang="postcss" scoped>
<style lang="scss" scoped>
.router-link-exact-active {
@apply border-b-neutral-700 text-neutral-900 dark:text-neutral-100 dark:border-b-neutral-200 dark:hover:text-neutral-100
}
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-server/components/layout/header.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="flex items-center py-8 pt-0 dark:text-neutral-100">
<div class="flex gap-4 items-center py-8 pt-0 dark:text-neutral-100">
<div class="text-[40px] flex-auto">
<slot />
</div>
Expand Down
60 changes: 60 additions & 0 deletions packages/auth-server/components/session/row/Expiry.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<div>
<div
v-if="isExpired"
class="text-neutral-500"
>
Expired
</div>
<div
v-else-if="isRevoked"
class="text-neutral-500"
>
Revoked
</div>
<div v-else-if="status === SessionStatus.NotInitialized">
Not initialized
</div>
<div v-else>
<div :title="`${sessionExpiry.formattedDate} at ${sessionExpiry.formattedTime}`">
<span v-if="sessionExpiry.isToday">Expires {{ expiresIn }}</span>
<span v-else-if="sessionExpiry.isTomorrow">Expires tomorrow at {{ sessionExpiry.formattedTime }}</span>
<span v-else>Expires on {{ sessionExpiry.formattedDate }} at {{ sessionExpiry.formattedTime }}</span>
</div>
<div class="session-row-line">
<div
class="bg-white rounded-full h-full will-change-[width] transition-[width] duration-300"
:style="{ width: `${timeLeftPercentage}%` }"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { SessionStatus } from "zksync-sso/utils";
const props = defineProps<{
status: SessionStatus;
isExpired: boolean;
now: number;
createdAt: number;
expiresAt: number;
}>();
const expiresIn = useTimeAgo(props.expiresAt, { showSecond: true, updateInterval: 1000 });
const sessionExpiry = computed(() => {
const expiresDate = new Date(props.expiresAt);
const nowDate = new Date(props.now);
return formatExpiryDate({
expiresAt: expiresDate,
now: nowDate,
});
});
const timeLeft = computed<number>(() => Math.max(0, props.expiresAt - props.now));
const timeTotal = computed<number>(() => Math.max(0, props.expiresAt - props.createdAt));
const timeLeftPercentage = computed<number>(() => Math.min(100, (timeLeft.value / timeTotal.value) * 100));
const isRevoked = computed(() => props.status === SessionStatus.Closed);
</script>
154 changes: 154 additions & 0 deletions packages/auth-server/components/session/row/Row.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<template>
<div class="session-row">
<div class="session-id-container">
<div :title="sessionId">
#{{ index }}
</div>
<a
class="session-created-time-ago"
:title="fullCreatedAtDate || ''"
:href="`${defaultChain.blockExplorers?.native.url}/tx/${transactionHash}`"
target="_blank"
>
{{ createdTimeAgo }}
</a>
</div>
<div class="session-expiry-container">
<SessionRowExpiry
v-if="sessionState"
:status="sessionState.status"
:is-expired="isExpired"
:created-at="timestamp"
:expires-at="expiresAt"
:now="now"
/>
</div>
<div class="session-spend-limit-container">
<SessionRowSpendLimit
v-if="sessionState"
:config="session"
:state="sessionState"
:now="now"
:is-inactive="isInactive"
/>
</div>
<div class="session-buttons-container">
<ZkButton
v-if="sessionState && sessionState.status === SessionStatus.Active && !isExpired"
title="Revoke Session"
type="danger"
class="ml-auto"
:ui="{ button: 'block p-2.5 aspect-square', base: 'p-0' }"
:loading="sessionsInProgress"
@click="revokeSession()"
>
<HandRaisedIcon
class="h-5 w-5"
aria-hidden="true"
/>
<span class="sr-only">Revoke Session</span>
</ZkButton>
</div>
</div>
</template>

<script setup lang="ts">
import { HandRaisedIcon } from "@heroicons/vue/24/outline";
import type { Hash } from "viem";
import { SessionKeyModuleAbi } from "zksync-sso/abi";
import { type SessionConfig, type SessionState, SessionStatus } from "zksync-sso/utils";
const props = defineProps<{
session: SessionConfig;
index: number;
sessionId: Hash;
transactionHash: Hash;
blockNumber: bigint;
timestamp: number;
}>();
const _now = useNow({ interval: 1000 });
const now = computed(() => _now.value.getTime());
const createdTimeAgo = useTimeAgo(props.timestamp);
const fullCreatedAtDate = computed(() => new Date(props.timestamp).toLocaleString());
const expiresAt = computed<number>(() => bigintDateToDate(props.session.expiresAt).getTime());
const timeLeft = computed<number>(() => Math.max(0, expiresAt.value - now.value));
const isExpired = computed(() => timeLeft.value <= 0);
const { defaultChain, getClient, getPublicClient } = useClientStore();
const { address } = storeToRefs(useAccountStore());
const {
inProgress: sessionsInProgress,
execute: revokeSession,
} = useAsync(async () => {
const client = getClient({ chainId: defaultChain.id });
const paymasterAddress = contractsByChain[defaultChain.id].accountPaymaster;
await client.revokeSession({
sessionId: props.sessionId,
paymaster: {
address: paymasterAddress,
},
});
await fetchSessionState();
});
const {
result: sessionState,
execute: fetchSessionState,
} = useAsync(async () => {
const client = getPublicClient({ chainId: defaultChain.id });
const res = await client.readContract({
address: contractsByChain[defaultChain.id].session,
abi: SessionKeyModuleAbi,
functionName: "sessionState",
args: [address.value!, props.session],
});
return res as SessionState;
});
const isInactive = computed(() => isExpired.value || !sessionState.value || sessionState.value.status === SessionStatus.Closed || sessionState.value.status === SessionStatus.NotInitialized);
fetchSessionState();
</script>

<style lang="scss">
/* Not scoped for a reason. Shares classes with RowLoader.vue */
.session-row {
@apply grid px-4 items-center text-sm;
@apply grid-cols-2 gap-y-2 py-4 gap-x-8;
@apply md:grid-cols-[6rem_1fr_1fr_45px] md:py-7 md:h-[100px];
grid-template-areas:
"session-id-container session-buttons-container"
"session-expiry-container session-expiry-container"
"session-spend-limit-container session-spend-limit-container";
@media screen and (min-width: 768px) {
grid-template-areas: "session-id-container session-expiry-container session-spend-limit-container session-buttons-container";
}
.session-id-container {
grid-area: session-id-container;
}
.session-expiry-container {
grid-area: session-expiry-container;
}
.session-spend-limit-container {
grid-area: session-spend-limit-container;
}
.session-buttons-container {
grid-area: session-buttons-container;
}
.session-expiry-container, .session-spend-limit-container {
@apply py-2 md:py-0;
}
.session-created-time-ago {
@apply text-neutral-500 text-xs;
}
.session-row-line {
@apply bg-neutral-900 rounded-full w-full h-1 mt-1;
}
}
</style>
26 changes: 26 additions & 0 deletions packages/auth-server/components/session/row/RowLoader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<div class="session-row">
<div class="session-id-container">
<div>
<CommonContentLoader :length="10" />
</div>
<CommonContentLoader
class="session-created-time-ago"
:length="20"
/>
</div>
<div class="session-expiry-container">
<CommonContentLoader :length="30" />
<CommonContentLoader class="block session-row-line" />
</div>
<div class="session-spend-limit-container">
<CommonContentLoader :length="30" />
<CommonContentLoader class="block session-row-line" />
</div>
<div class="session-buttons-container">
<div class="rounded-full w-[43px] h-[43px] overflow-hidden">
<CommonContentLoader class="block h-full w-full" />
</div>
</div>
</div>
</template>
Loading

0 comments on commit d1e2bfd

Please sign in to comment.