Skip to content

Commit

Permalink
Merge pull request #307 from greymass/permissions-page
Browse files Browse the repository at this point in the history
Permissions page
  • Loading branch information
aaroncox authored Dec 14, 2024
2 parents 7fceff4 + d6b979e commit 24cc9c1
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 12 deletions.
11 changes: 9 additions & 2 deletions src/lib/components/button/copy.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import { browser } from '$app/environment';
import type { UnicoveContext } from '$lib/state/client.svelte';
import { getContext } from 'svelte';
import { cn } from '$lib/utils';
const context = getContext<UnicoveContext>('state');
interface Props {
data: string;
slop?: boolean;
}
let props: Props = $props();
let { slop = true, ...props }: Props = $props();
let hint = $state(false);
Expand All @@ -26,6 +28,8 @@
if (context.settings.data.debugMode) console.error('Failed to copy text: ', err);
}
}
let buttonSize = $derived(slop ? 'size-12' : 'size-4');
</script>

<!-- Styled as a trailing element. Will need to change it if we want to use it inline with other elements following it. -->
Expand All @@ -35,7 +39,10 @@
>
<button
onclick={copyToClipboard}
class="peer absolute left-1/2 top-1/2 size-12 -translate-x-1/2 -translate-y-1/2 focus-visible:outline-none"
class={cn(
'peer absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 focus-visible:outline-none',
buttonSize
)}
aria-label="Copy"
>
<!-- Button is done this way with absolute positioning so we can maintain a decent hit slop on mobile without affecting layout -->
Expand Down
8 changes: 6 additions & 2 deletions src/lib/components/elements/account.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type { HTMLAnchorAttributes } from 'svelte/elements';
import { createLinkPreview, melt, type CreateLinkPreviewProps } from '@melt-ui/svelte';
import { fly } from 'svelte/transition';
import { User } from 'lucide-svelte';
import { User, UserIcon } from 'lucide-svelte';
import { Name } from '@wharfkit/antelope';
import type { UnicoveContext } from '$lib/state/client.svelte';
import { AccountState } from '$lib/state/client/account.svelte';
Expand All @@ -15,6 +15,7 @@
contract?: boolean;
children?: Snippet;
preview?: boolean;
icon?: boolean;
}
let { name, contract = false, preview = false, children, ...props }: Props = $props();
Expand Down Expand Up @@ -51,11 +52,14 @@
<a
href={path}
class={cn(
'text-skyBlue-500 hover:text-skyBlue-400 focus-visible:outline focus-visible:outline-solar-500 ',
'inline-flex items-center gap-2 text-skyBlue-500 hover:text-skyBlue-400 focus-visible:outline focus-visible:outline-solar-500 ',
props.class
)}
use:melt={$trigger}
>
{#if props.icon}
<UserIcon class="size-4" />
{/if}
{#if children}
{@render children()}
{:else}
Expand Down
35 changes: 35 additions & 0 deletions src/lib/components/elements/contract.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import type { UnicoveContext } from '$lib/state/client.svelte';
import { cn } from '$lib/utils';
import type { Name } from '@wharfkit/antelope';
import { getContext, type Snippet } from 'svelte';
const { network } = getContext<UnicoveContext>('state');
interface Props {
children?: Snippet;
name?: Name | string;
action?: Name | string;
class?: string;
}
let { name, action, children, ...props }: Props = $props();
let href = $derived.by(() => {
const base = `/${network}/contract/${String(name)}`;
if (action) {
return base + `/actions/${action}`;
}
return base;
});
</script>

{#if name}
<a class={cn('text-skyBlue-500 hover:text-skyBlue-400', props.class)} {href}>
{#if children}
{@render children()}
{:else}
{String(name)}
{/if}
</a>
{/if}
31 changes: 31 additions & 0 deletions src/lib/components/elements/key.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import type { UnicoveContext } from '$lib/state/client.svelte';
import type { PrivateKey, PublicKey } from '@wharfkit/antelope';
import { KeyRound } from 'lucide-svelte';
import { getContext } from 'svelte';
const { network } = getContext<UnicoveContext>('state');
interface Props {
key?: PublicKey | PrivateKey | string;
icon?: boolean;
}
let { key, icon }: Props = $props();
</script>

{#if key}
<a
class="inline-grid grid-cols-[auto_1fr] items-start gap-2 text-skyBlue-500 hover:text-skyBlue-400"
href="/{network}/key/{String(key)}"
>
{#if icon}
<div class="content-center pt-1">
<KeyRound class="size-4 shrink-0 " />
</div>
{/if}
<span class="break-all">
{String(key)}
</span>
</a>
{/if}
6 changes: 6 additions & 0 deletions src/lib/utils/dayjs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(duration);
dayjs.extend(relativeTime);
1 change: 1 addition & 0 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import '../app.css';
import 'inter-ui/inter-latin.css';
import '@fontsource/jetbrains-mono/600.css'; // Semibold
import '$lib/utils/dayjs'; // setup dayjs
import extend from 'just-extend';
import { Head, type SeoConfig } from 'svead';
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
<script lang="ts">
import Code from '$lib/components/code.svelte';
import PermissionTree from './permissiontree.svelte';
const { data } = $props();
</script>

{#if data.account}
<div class="space-y-4">
<h3 class="h3">Permissions</h3>
<Code>{JSON.stringify(data.account.permissions, null, 2)}</Code>
</div>
<PermissionTree permissions={data.tree} />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Permission } from '@wharfkit/account';
import type { PageLoad } from './$types';
import { Name } from '@wharfkit/antelope';

export interface TreePermission {
permission: Permission;
children?: TreePermission[];
}

function buildTree(data: TreePermission[], parentId = Name.from('')): TreePermission[] {
const tree: TreePermission[] = [];
data.forEach((item) => {
// Check if the item belongs to the current parent
if (item.permission.parent.equals(parentId)) {
// Recursively build the children of the current item
const children = buildTree(data, item.permission.perm_name);
// If children exist, assign them to the current item
if (children.length) {
item.children = children;
}
// Add the current item to the tree
tree.push(item);
}
});
return tree;
}

export const load: PageLoad = async ({ params, parent }) => {
const { network, account } = await parent();

let tree: TreePermission[] = [];
if (account.permissions) {
const permissionTree = account.permissions.map((p) => ({ permission: p }));
tree = buildTree(permissionTree);
}

return {
subtitle: `Permissions on the ${network.chain.name} Network.`,
tree: tree,
pageMetaTags: {
title: `Permissions | ${params.name} | ${network.chain.name} Network`,
description: `Permissions for ${params.name} on the ${network.chain.name} network.`
}
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<script lang="ts">
import Self from './permission.svelte';
import type { TreePermission } from './+page';
import Key from '$lib/components/elements/key.svelte';
import CopyButton from '$lib/components/button/copy.svelte';
import Account from '$lib/components/elements/account.svelte';
import Contract from '$lib/components/elements/contract.svelte';
import dayjs from 'dayjs';
import { Clock } from 'lucide-svelte';
interface Props {
permission: TreePermission;
level?: number;
}
let { level = 0, ...props }: Props = $props();
let { permission, children } = $derived(props.permission);
const anyPermissions = $derived(
permission.required_auth.accounts.length ||
permission.required_auth.keys.length ||
permission.required_auth.waits.length
);
</script>

<li
class="relative col-span-full grid grid-cols-subgrid bg-shark-950"
class:pl-4={level !== 0}
class:pt-6={level !== 0}
>
<dl
class="z-20 col-span-full space-y-1 rounded-t-lg bg-mineShaft-950 px-4 py-3 md:col-span-1 md:rounded-l-lg"
>
<div>
<dt class="sr-only">Permission Name</dt>
<dd class="text-xl font-semibold text-white">{permission.perm_name}</dd>
</div>
<div class="text-muted text-nowrap *:inline">
<dt class="after:content-[':']"><span class="sr-only">Threshold</span> Required</dt>
<dd>{permission.required_auth.threshold}</dd>
</div>
{#if permission.linked_actions}
<div class="">
<dt class="sr-only">Actions</dt>
{#each permission.linked_actions as { action, account }}
<dd>
<Contract name={account} {action} class="flex">
<span class="account">{account}</span>
{#if action}
<span class="action">::{action}</span>
{/if}
</Contract>
</dd>
{/each}
</div>
{/if}
</dl>

<div class="rounded-b-lg bg-mineShaft-950/50 px-4 py-3 md:rounded-r-lg">
{#if anyPermissions}
<table class="grid grid-cols-[auto_1fr_auto] gap-x-4 gap-y-2">
<thead class="col-span-full grid grid-cols-subgrid">
<tr
class="col-span-full grid grid-cols-subgrid text-left *:pt-1 *:text-base *:font-medium"
>
<th>Weight</th>
<th>Authorization</th>
</tr>
</thead>
<tbody class="col-span-full grid grid-cols-subgrid gap-x-4 gap-y-2">
{#if permission.required_auth.keys}
{#each permission.required_auth.keys as { weight, key }}
<tr
class="col-span-full grid grid-cols-subgrid items-start bg-none text-white"
data-hover-effect="false"
>
<td>
+{weight.toString()}
</td>
<td>
<Key {key} icon />
</td>
<td>
<CopyButton data={key.toString()} slop={false} />
</td>
</tr>
{/each}
{/if}
{#if permission.required_auth.accounts}
{#each permission.required_auth.accounts as { weight, permission: account }}
<tr
class="col-span-full grid grid-cols-subgrid bg-none text-white"
data-hover-effect="false"
>
<td>
+{weight.toString()}
</td>
<td>
<Account name={account.actor} icon>
{account}
</Account>
</td>
<td class="*:pt-1">
<CopyButton data={account.toString()} slop={false} />
</td>
</tr>
{/each}
{/if}
{#if permission.required_auth.waits}
{#each permission.required_auth.waits as { weight, wait_sec }}
<tr
class="col-span-full grid grid-cols-subgrid bg-none text-white"
data-hover-effect="false"
>
<td>
+{weight.toString()}
</td>
<td class="flex items-center gap-2 text-mineShaft-100">
<Clock class="size-4" />
{wait_sec.toString()}s ({dayjs
.duration(wait_sec.toNumber(), 'seconds')
.humanize()})
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
{/if}
</div>
<!-- The curved connector line -->
{#if level > 0}
<div class="absolute -left-px z-10 size-12 rounded-bl-xl border-b border-l"></div>
{/if}
</li>

{#if children}
<li class="col-span-full grid grid-cols-subgrid">
<!-- The border on this ul is the through line -->
<ul
data-solo={children.length === 1}
class="children col-span-full grid grid-cols-subgrid *:data-[solo=false]:border-l last:*:data-[solo=false]:border-transparent"
class:ml-8={level > 0}
class:ml-4={level === 0}
>
<!-- style={`margin-left:calc(1rem * ${level + 1})`} -->
{#each children as child}
<Self permission={child} level={level + 1} />
{/each}
</ul>
</li>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import Permission from './permission.svelte';
let props = $props();
</script>

<ul class="grid grid-cols-[auto_1fr] overflow-x-auto">
{#each props.permissions as permission}
<Permission {permission} />
{/each}
</ul>
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,12 @@ import {
type Transaction
} from '@wharfkit/antelope';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

import type { UnicoveContext } from '$lib/state/client.svelte';
import type { WharfState } from '$lib/state/client/wharf.svelte';
import type { NetworkState } from '$lib/state/network.svelte';
import * as SystemContract from '$lib/wharf/contracts/system';

dayjs.extend(relativeTime);

type Proposal = {
proposer: string;
name: string;
Expand Down

0 comments on commit 24cc9c1

Please sign in to comment.