Skip to content

Commit

Permalink
feat(front): Admin activity
Browse files Browse the repository at this point in the history
  • Loading branch information
GordiNoki committed Mar 1, 2024
1 parent 051202e commit 80056d1
Show file tree
Hide file tree
Showing 12 changed files with 485 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Component, Input } from '@angular/core';
import { AdminActivity } from '@momentum/constants';
import { RouterLink } from '@angular/router';
import { AvatarComponent } from '../../../../components';
import { TimeAgoPipe } from '../../../../pipes';
import { NgStyle } from '@angular/common';
import { IconComponent } from '../../../../icons';
import { AdminActivityEntryData } from './admin-activity-entry.component';

@Component({
selector: 'm-admin-activity-entry-header',
template: `
<div class="flex h-[40px] flex-wrap items-center gap-1">
<a routerLink="/profile/{{ activity.userID }}" class="contents">
<m-avatar [url]="activity.user.avatarURL" class="mr-2 !h-7" />
<p>{{ activity.user.alias }}</p>
</a>
<p>{{ activityData.actionText }}</p>
<p
[ngStyle]="{ cursor: activityData.targetLink ? 'pointer' : 'auto' }"
[routerLink]="activityData.targetLink ? activityData.targetLink : null"
>
<b>{{ activityData.targetName }}</b>
</p>
<p class="ml-auto">{{ activity.createdAt | timeAgo }}</p>
</div>
`,
standalone: true,
imports: [NgStyle, RouterLink, AvatarComponent, IconComponent, TimeAgoPipe]
})
export class AdminActivityEntryHeaderComponent {
@Input({ required: true }) activity: AdminActivity;
@Input({ required: true }) activityData: AdminActivityEntryData;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { Component, Input, OnInit } from '@angular/core';
import {
AdminActivity,
AdminActivityType,
Role,
RoleNames,
Ban,
BanNames
} from '@momentum/constants';
import { RouterLink } from '@angular/router';
import { NgStyle } from '@angular/common';
import { Bitflags } from '@momentum/bitflags';
import { Enum } from '@momentum/enum';

@Component({
selector: 'm-admin-activity-entry',
template: `
<div class="flex flex-col gap-1">
@for (diffEntry of diffEntries; track diffEntry.key) {
<div class="flex w-full">
<p class="font-bold p-2">{{ diffEntry.key }}:</p>
@if (diffEntry.old != null) {
<p class="grow bg-red-900 p-2">{{ diffEntry.old }}</p>
}
@if (diffEntry.new != null) {
<p class="grow bg-green-900 p-2">{{ diffEntry.new }}</p>
}
</div>
}
</div>
`,
standalone: true,
imports: [NgStyle, RouterLink]
})
export class AdminActivityEntryComponent implements OnInit {
@Input({ required: true }) activityData: AdminActivityEntryData;

diffEntries: {
key: string;
old?: string;
new?: string;
}[] = [];

ngOnInit() {
if (this.activityData.diff)
this.diffEntries = Object.entries(this.activityData.diff).map(
([key, value]) => ({ key, old: value[0], new: value[1] })
);
}

static getActivityData(activity: AdminActivity): AdminActivityEntryData {
switch (activity.type) {
case AdminActivityType.USER_UPDATE_ROLES:
return {
actionText: 'updated roles for user',
targetName: (activity.newData as any).alias,
targetLink: '/profile/' + activity.target,
diff: {
roles: [
Enum.values(Role)
.filter((role) =>
Bitflags.has((activity.oldData as any).roles, role)
)
.map((role) => RoleNames.get(role))
.join(', ') || null,
Enum.values(Role)
.filter((role) =>
Bitflags.has((activity.newData as any).roles, role)
)
.map((role) => RoleNames.get(role))
.join(', ') || null
]
}
};

case AdminActivityType.USER_UPDATE_BANS:
return {
actionText: 'updated bans for user',
targetName: (activity.newData as any).alias,
targetLink: '/profile/' + activity.target,
diff: {
bans: [
Enum.values(Ban)
.filter((ban) =>
Bitflags.has((activity.oldData as any).bans, ban)
)
.map((ban) => BanNames.get(ban))
.join(', ') || null,
Enum.values(Ban)
.filter((ban) =>
Bitflags.has((activity.newData as any).bans, ban)
)
.map((ban) => BanNames.get(ban))
.join(', ') || null
]
}
};

case AdminActivityType.USER_UPDATE_ALIAS:
return {
actionText: 'updated alias for user',
targetName: (activity.newData as any).alias,
targetLink: '/profile/' + activity.target,
diff: {
alias: [
(activity.oldData as any).alias,
(activity.newData as any).alias
]
}
};

case AdminActivityType.USER_UPDATE_BIO:
return {
actionText: 'updated bio for user',
targetName: (activity.newData as any).alias,
targetLink: '/profile/' + activity.target,
diff: {
bio: [
(activity.oldData as any).profile?.bio,
(activity.newData as any).profile?.bio
]
}
};

case AdminActivityType.USER_CREATE_PLACEHOLDER:
return {
actionText: 'created placeholder',
targetName: (activity.newData as any).alias,
targetLink: '/profile/' + activity.target
};

case AdminActivityType.USER_MERGE:
return {
actionText: 'merged user',
targetName: (activity.newData as any).alias,
targetLink: '/profile/' + (activity.newData as any).id
};

case AdminActivityType.USER_DELETE:
return {
actionText: 'deleted user',
targetName: (activity.oldData as any).alias,
targetLink: '/profile/' + activity.target
};

case AdminActivityType.MAP_UPDATE:
return {
actionText: 'updated map',
targetName: (activity.newData as any).name,
targetLink: '/maps/' + (activity.newData as any).id,
diff: AdminActivityEntryComponent.calculateDiff(
activity.oldData,
activity.newData
)
};

case AdminActivityType.MAP_DELETE:
return {
actionText: 'deleted map',
targetName: (activity.oldData as any).name
};

case AdminActivityType.REPORT_UPDATE:
return {
actionText: 'updated report',
targetName: 'ID ' + activity.target
};

case AdminActivityType.REPORT_RESOLVE:
return {
actionText: 'resolved report',
targetName: 'ID ' + activity.target
};

default:
return {
actionText: 'did unknown activity',
targetName: 'ID ' + activity.type
};
}
}

static calculateDiff(
oldData: object,
newData: object
): AdminActivityEntryData['diff'] {
let keys = [...Object.keys(oldData), ...Object.keys(newData)];
keys = keys.filter((v, i) => keys.indexOf(v) === i);

const result = {};
for (const key of keys) {
let oldValue = oldData[key];
let newValue = newData[key];

if (oldValue instanceof Date || newValue instanceof Date) {
oldValue = oldValue.toString();
newValue = newValue.toString();
}

if (typeof oldValue == 'object' || typeof newValue == 'object') {
oldValue = JSON.stringify(oldValue);
newValue = JSON.stringify(newValue);
}

if (oldValue !== newValue) {
result[key] = [oldValue, newValue];
}
}

return result;
}
}

export interface AdminActivityEntryData {
actionText: string;
targetName?: string;
targetLink?: string;
diff?: Record<string, [string, string]>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<m-card title="Admin Activity">
<ng-container header>
<p-dropdown
class="[&>*]:w-48"
placeholder="Activity type"
[options]="ADMIN_ACTIVITIES_FILTERS"
optionLabel="text"
optionValue="value"
(onChange)="filterChange.next()"
[(ngModel)]="filter"
/>
</ng-container>
<div [mSpinner]="loading">
<m-accordion>
@for (activity of activities; track activity) {
<m-accordion-item [hasContent]="!!activity.entry.diff">
<ng-container header>
<m-admin-activity-entry-header [activity]="activity.activity" [activityData]="activity.entry" />
</ng-container>
<m-admin-activity-entry [activityData]="activity.entry" />
</m-accordion-item>
} @empty {
@if (!loading) {
<h3>Activities not found!</h3>
}
}
</m-accordion>
<p-paginator
(onPageChange)="pageChange.next($event)"
[first]="first"
[rows]="rows"
[totalRecords]="totalRecords"
[showCurrentPageReport]="true"
[alwaysShow]="false"
/>
</div>
</m-card>
Loading

0 comments on commit 80056d1

Please sign in to comment.