diff --git a/api-schema.graphql b/api-schema.graphql index 541ec479..8f8bac0a 100644 --- a/api-schema.graphql +++ b/api-schema.graphql @@ -597,6 +597,7 @@ type NetworkAsset { group: String id: String! imageUrl: String + logs: [Log!] metadata: JSON mint: String! name: String! diff --git a/libs/api/network-asset/data-access/src/lib/api-network-asset-data.service.ts b/libs/api/network-asset/data-access/src/lib/api-network-asset-data.service.ts index 80fc003d..c1cddc11 100644 --- a/libs/api/network-asset/data-access/src/lib/api-network-asset-data.service.ts +++ b/libs/api/network-asset/data-access/src/lib/api-network-asset-data.service.ts @@ -34,6 +34,12 @@ export class ApiNetworkAssetDataService { async findOneByAccount(cluster: NetworkCluster, account: string) { return this.core.data.networkAsset.findUnique({ + include: { + logs: { + include: { identity: { include: { owner: true } } }, + orderBy: { createdAt: 'desc' }, + }, + }, where: { account_cluster: { cluster, account } }, }) } diff --git a/libs/api/network-asset/data-access/src/lib/api-network-asset-sync.service.ts b/libs/api/network-asset/data-access/src/lib/api-network-asset-sync.service.ts index 9f904449..452d8b35 100644 --- a/libs/api/network-asset/data-access/src/lib/api-network-asset-sync.service.ts +++ b/libs/api/network-asset/data-access/src/lib/api-network-asset-sync.service.ts @@ -231,6 +231,7 @@ export class ApiNetworkAssetSyncService { await this.core.logInfo(`[${cluster}] syncIdentity: Removed ${removedIds.count} assets for ${owner}`, { identityProvider: IdentityProvider.Solana, identityProviderId: owner, + data: { assetIds }, }) } @@ -308,6 +309,7 @@ export class ApiNetworkAssetSyncService { if (isNetworkAssetEqual({ found, asset })) { return true } + const data = findNetworkAssetDiff({ found, asset }) const updated = await this.core.data.networkAsset.update({ where: { account_cluster: { account: asset.account, cluster } }, data: { @@ -315,10 +317,10 @@ export class ApiNetworkAssetSyncService { logs: { create: { level: LogLevel.Info, - message: 'Asset updated', + message: `Asset updated. Type ${asset.type}, diff keys: ${Object.keys(data)}`, identityProviderId: linkIdentity ? asset.owner : undefined, identityProvider: linkIdentity ? IdentityProvider.Solana : undefined, - data: findNetworkAssetDiff({ found, asset }), + data, }, }, }, @@ -331,7 +333,7 @@ export class ApiNetworkAssetSyncService { logs: { create: { level: LogLevel.Info, - message: `Asset created: ${asset.name} (${asset.symbol})`, + message: `Asset created: ${asset.name} (${asset.symbol}). Owner ${asset.owner ?? 'unknown'}`, identityProviderId: linkIdentity ? asset.owner : undefined, identityProvider: linkIdentity ? IdentityProvider.Solana : undefined, data: findNetworkAssetDiff({ found: {}, asset }), diff --git a/libs/api/network-asset/data-access/src/lib/entity/network-asset.entity.ts b/libs/api/network-asset/data-access/src/lib/entity/network-asset.entity.ts index 23e58d6e..aff1739b 100644 --- a/libs/api/network-asset/data-access/src/lib/entity/network-asset.entity.ts +++ b/libs/api/network-asset/data-access/src/lib/entity/network-asset.entity.ts @@ -1,5 +1,5 @@ import { Field, Int, ObjectType } from '@nestjs/graphql' -import { Prisma } from '@prisma/client' +import { Log, Prisma } from '@prisma/client' import { NetworkResolver, PagingResponse } from '@pubkey-link/api-core-data-access' import { NetworkCluster } from '@pubkey-link/api-network-data-access' import { NetworkTokenType } from '@pubkey-link/api-network-token-data-access' @@ -35,6 +35,7 @@ export class NetworkAsset { burnt?: boolean @Field() mint!: string + logs?: Log[] @Field() owner!: string @Field({ nullable: true }) diff --git a/libs/api/network-asset/feature/src/lib/api-network-asset.resolver.ts b/libs/api/network-asset/feature/src/lib/api-network-asset.resolver.ts index 6064da79..87054293 100644 --- a/libs/api/network-asset/feature/src/lib/api-network-asset.resolver.ts +++ b/libs/api/network-asset/feature/src/lib/api-network-asset.resolver.ts @@ -1,6 +1,7 @@ import { Parent, ResolveField, Resolver } from '@nestjs/graphql' import { ApiNetworkAssetService, NetworkAsset } from '@pubkey-link/api-network-asset-data-access' import { getNetworkExplorerUrl } from '@pubkey-link/api-network-util' +import { Log } from '@pubkey-link/api-log-data-access' @Resolver(() => NetworkAsset) export class ApiNetworkAssetResolver { @@ -10,4 +11,9 @@ export class ApiNetworkAssetResolver { explorerUrl(@Parent() networkAsset: NetworkAsset) { return getNetworkExplorerUrl(networkAsset.cluster).replace('{path}', `address/${networkAsset.account}`) } + + @ResolveField(() => [Log], { nullable: true }) + logs(@Parent() networkAsset: NetworkAsset) { + return networkAsset.logs ?? [] + } } diff --git a/libs/api/network/data-access/src/lib/resolver/api-network-resolver-realms-voter.service.ts b/libs/api/network/data-access/src/lib/resolver/api-network-resolver-realms-voter.service.ts index d015f7a7..40d1848e 100644 --- a/libs/api/network/data-access/src/lib/resolver/api-network-resolver-realms-voter.service.ts +++ b/libs/api/network/data-access/src/lib/resolver/api-network-resolver-realms-voter.service.ts @@ -13,10 +13,12 @@ export class ApiNetworkResolverRealmsVoterService { async resolve({ owner, tokens }: { owner: string; tokens: NetworkToken[] }): Promise { const cluster = NetworkCluster.SolanaMainnet - const tag = `resolveNetworkAssetsSolanaRealmsVoter(${owner}, ${cluster}, ${tokens.map((t) => t.account).join(',')})` - this.logger.verbose(`${tag}: Start resolving assets`) + const groups = tokens.map((token) => token.account) - const voters = await getRealmsVoters({ realms: tokens.map((t) => t.account), owner }) + const tag = `resolveNetworkAssetsSolanaRealmsVoter(${owner}, ${cluster})` + this.logger.verbose(`${tag}: Start resolving assets for groups: ${groups.join(',')}`) + + const voters = await getRealmsVoters({ realms: groups, owner }) if (!voters.length) { return [] } diff --git a/libs/api/network/util/src/lib/resolver-realms/index.ts b/libs/api/network/util/src/lib/resolver-realms/index.ts index e8c3b62a..f59f7750 100644 --- a/libs/api/network/util/src/lib/resolver-realms/index.ts +++ b/libs/api/network/util/src/lib/resolver-realms/index.ts @@ -85,6 +85,7 @@ function convertVoterToNetworkAsset(voter: RealmVoter): Prisma.NetworkAssetCreat network: { connect: { cluster: NetworkCluster.SolanaMainnet } }, owner: voter.voter, resolver: NetworkResolver.SolanaRealms, + symbol: 'VOTER', type: NetworkTokenType.RealmsVoter, } } diff --git a/libs/sdk/src/generated/graphql-sdk.ts b/libs/sdk/src/generated/graphql-sdk.ts index a71c011c..b43b6564 100644 --- a/libs/sdk/src/generated/graphql-sdk.ts +++ b/libs/sdk/src/generated/graphql-sdk.ts @@ -960,6 +960,7 @@ export type NetworkAsset = { group?: Maybe id: Scalars['String']['output'] imageUrl?: Maybe + logs?: Maybe> metadata?: Maybe mint: Scalars['String']['output'] name: Scalars['String']['output'] @@ -6299,6 +6300,194 @@ export type UserFindOneNetworkAssetQuery = { explorerUrl: string metadata?: any | null attributes?: any | null + logs?: Array<{ + __typename?: 'Log' + createdAt?: Date | null + id: string + message: string + level: LogLevel + relatedId?: string | null + relatedType?: LogRelatedType | null + communityId?: string | null + identityProvider?: IdentityProvider | null + identityProviderId?: string | null + networkAssetId?: string | null + botId?: string | null + userId?: string | null + roleId?: string | null + updatedAt?: Date | null + data?: any | null + bot?: { + __typename?: 'Bot' + avatarUrl?: string | null + communityId: string + createdAt?: Date | null + developersUrl: string + id: string + inviteUrl: string + name: string + redirectUrl: string + redirectUrlSet?: boolean | null + started: boolean + status: BotStatus + updatedAt?: Date | null + verificationUrl: string + verificationUrlSet?: boolean | null + } | null + identity?: { + __typename?: 'Identity' + avatarUrl?: string | null + createdAt?: Date | null + syncStarted?: Date | null + syncEnded?: Date | null + expired?: boolean | null + id: string + name: string + ownerId?: string | null + profile?: any | null + provider: IdentityProvider + providerId: string + updatedAt?: Date | null + url?: string | null + verified?: boolean | null + } | null + networkAsset?: { + __typename?: 'NetworkAsset' + id: string + createdAt?: Date | null + updatedAt?: Date | null + cluster: NetworkCluster + resolver: NetworkResolver + type: NetworkTokenType + account: string + balance?: string | null + name: string + symbol?: string | null + program?: string | null + decimals: number + mint: string + burnt?: boolean | null + owner: string + group?: string | null + imageUrl?: string | null + explorerUrl: string + metadata?: any | null + attributes?: any | null + } | null + role?: { + __typename?: 'Role' + createdAt?: Date | null + id: string + communityId: string + name: string + updatedAt?: Date | null + viewUrl?: string | null + conditions?: Array<{ + __typename?: 'RoleCondition' + createdAt?: Date | null + id: string + type: NetworkTokenType + amount?: string | null + amountMax?: string | null + filters?: any | null + config?: any | null + tokenId?: string | null + roleId?: string | null + updatedAt?: Date | null + valid?: boolean | null + token?: { + __typename?: 'NetworkToken' + id: string + createdAt?: Date | null + updatedAt?: Date | null + cache?: boolean | null + cluster: NetworkCluster + type: NetworkTokenType + account: string + program: string + name: string + mintList?: Array | null + symbol?: string | null + description?: string | null + imageUrl?: string | null + metadataUrl?: string | null + raw?: any | null + } | null + asset?: { __typename?: 'SolanaNetworkAsset'; owner: string; amount: string; accounts: Array } | null + }> | null + permissions?: Array<{ + __typename?: 'RolePermission' + createdAt?: Date | null + id: string + updatedAt?: Date | null + botId?: string | null + roleId?: string | null + botRole?: { + __typename?: 'BotRole' + botId?: string | null + createdAt?: Date | null + id: string + serverId?: string | null + updatedAt?: Date | null + serverRoleId?: string | null + serverRole?: { + __typename?: 'DiscordRole' + id: string + name: string + managed: boolean + color: number + position: number + } | null + server?: { + __typename?: 'DiscordServer' + id: string + name: string + icon?: string | null + permissions?: Array | null + } | null + } | null + }> | null + member?: { + __typename?: 'CommunityMember' + communityId: string + createdAt?: Date | null + id: string + admin: boolean + updatedAt?: Date | null + userId: string + user?: { + __typename?: 'User' + avatarUrl?: string | null + createdAt?: Date | null + developer?: boolean | null + private?: boolean | null + lastLogin?: Date | null + id: string + name?: string | null + profileUrl: string + role?: UserRole | null + status?: UserStatus | null + updatedAt?: Date | null + username?: string | null + } | null + } | null + } | null + user?: { + __typename?: 'User' + avatarUrl?: string | null + createdAt?: Date | null + developer?: boolean | null + private?: boolean | null + lastLogin?: Date | null + id: string + name?: string | null + profileUrl: string + role?: UserRole | null + status?: UserStatus | null + updatedAt?: Date | null + username?: string | null + } | null + }> | null } | null } @@ -10348,9 +10537,13 @@ export const UserFindOneNetworkAssetDocument = gql` query userFindOneNetworkAsset($account: String!, $cluster: NetworkCluster!) { item: userFindOneNetworkAsset(account: $account, cluster: $cluster) { ...NetworkAssetDetails + logs { + ...LogDetails + } } } ${NetworkAssetDetailsFragmentDoc} + ${LogDetailsFragmentDoc} ` export const AdminFindManyNetworkAssetDocument = gql` query adminFindManyNetworkAsset($input: AdminFindManyNetworkAssetInput!) { diff --git a/libs/sdk/src/graphql/feature-network-asset.graphql b/libs/sdk/src/graphql/feature-network-asset.graphql index e3e0e65a..47259bc3 100644 --- a/libs/sdk/src/graphql/feature-network-asset.graphql +++ b/libs/sdk/src/graphql/feature-network-asset.graphql @@ -35,6 +35,9 @@ query userFindManyNetworkAsset($input: UserFindManyNetworkAssetInput!) { query userFindOneNetworkAsset($account: String!, $cluster: NetworkCluster!) { item: userFindOneNetworkAsset(account: $account, cluster: $cluster) { ...NetworkAssetDetails + logs { + ...LogDetails + } } } diff --git a/libs/web/log/ui/src/index.ts b/libs/web/log/ui/src/index.ts index fc5f2597..a0a1bcb4 100644 --- a/libs/web/log/ui/src/index.ts +++ b/libs/web/log/ui/src/index.ts @@ -2,3 +2,4 @@ export * from './lib/admin-log-ui-table' export * from './lib/log-ui-item' export * from './lib/user-log-list-feature' export * from './lib/user-log-ui-table' +export * from './lib/user-log-ui-table-simple' diff --git a/libs/web/log/ui/src/lib/user-log-ui-table-simple.tsx b/libs/web/log/ui/src/lib/user-log-ui-table-simple.tsx new file mode 100644 index 00000000..9d8c8c1f --- /dev/null +++ b/libs/web/log/ui/src/lib/user-log-ui-table-simple.tsx @@ -0,0 +1,63 @@ +import { Log } from '@pubkey-link/sdk' +import { Anchor, Group, ScrollArea } from '@mantine/core' +import { DataTable } from 'mantine-datatable' +import { Link } from 'react-router-dom' +import { UiDebugModal, UiTime } from '@pubkey-ui/core' +import { UserUiAvatarLoader } from '@pubkey-link/web-user-ui' +import { LogUiLevelBadge } from './user-log-ui-table' + +export function UserLogUiTableSimple({ logs = [] }: { logs: Log[] }) { + return ( + + , + }, + { + accessor: 'message', + render: (item) => ( + + {item.message} + + ), + }, + { + width: '150px', + accessor: 'createdAt', + textAlign: 'right', + title: 'Created', + render: (item) => ( + {item.createdAt ? : null} + ), + }, + { + accessor: 'user', + width: '60px', + title: 'User', + render: (item) => (item.userId ? : null), + }, + { + width: '75px', + accessor: 'actions', + title: 'Actions', + textAlign: 'right', + render: (item) => ( + + + + + ), + }, + ]} + records={logs} + /> + + ) +} diff --git a/libs/web/network-asset/feature/src/lib/user-network-asset-detail.feature.tsx b/libs/web/network-asset/feature/src/lib/user-network-asset-detail.feature.tsx index 49d0baee..c4247930 100644 --- a/libs/web/network-asset/feature/src/lib/user-network-asset-detail.feature.tsx +++ b/libs/web/network-asset/feature/src/lib/user-network-asset-detail.feature.tsx @@ -1,7 +1,7 @@ import { Grid, Group } from '@mantine/core' import { NetworkCluster } from '@pubkey-link/sdk' import { AppUiDebugModal } from '@pubkey-link/web-core-ui' -import { UserLogListFeature } from '@pubkey-link/web-log-ui' +import { UserLogListFeature, UserLogUiTable, UserLogUiTableSimple } from '@pubkey-link/web-log-ui' import { useUserFindOneNetworkAsset } from '@pubkey-link/web-network-asset-data-access' import { NetworkAssetUiItem } from '@pubkey-link/web-network-asset-ui' import { NetworkUiClusterBadge } from '@pubkey-link/web-network-ui' @@ -38,7 +38,7 @@ export default function UserNetworkAssetDetailFeature() { label: 'Overview', element: , }, - { path: 'logs', label: 'Logs', element: }, + { path: 'logs', label: 'Logs', element: }, ]} /> diff --git a/libs/web/network-asset/ui/src/lib/admin-network-asset-ui-table.tsx b/libs/web/network-asset/ui/src/lib/admin-network-asset-ui-table.tsx index c03498e5..8a264440 100644 --- a/libs/web/network-asset/ui/src/lib/admin-network-asset-ui-table.tsx +++ b/libs/web/network-asset/ui/src/lib/admin-network-asset-ui-table.tsx @@ -1,7 +1,7 @@ import { ActionIcon, Group, ScrollArea } from '@mantine/core' import { NetworkAsset } from '@pubkey-link/sdk' import { UiCopy, UiDebugModal } from '@pubkey-ui/core' -import { IconPencil, IconTrash } from '@tabler/icons-react' +import { IconEye, IconPencil, IconTrash } from '@tabler/icons-react' import { DataTable, DataTableProps } from 'mantine-datatable' import { Link } from 'react-router-dom' import { NetworkAssetUiAttributesIcon } from './network-asset-ui-attributes-icon' @@ -47,6 +47,15 @@ export function AdminNetworkAssetUiTable({ render: (item) => ( + + +