628 lines
18 KiB
TypeScript
628 lines
18 KiB
TypeScript
import {
|
|
DeesElement,
|
|
type TemplateResult,
|
|
property,
|
|
customElement,
|
|
html,
|
|
css,
|
|
cssManager,
|
|
state,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import * as domtools from '@design.estate/dees-domtools';
|
|
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
|
import '../../dees-icon/dees-icon.js';
|
|
import '@design.estate/dees-wcctools/demotools';
|
|
import type { IActivityEntry, IActivityLogAPI } from '../../interfaces/appconfig.js';
|
|
|
|
@customElement('dees-appui-activitylog')
|
|
export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI {
|
|
// STATIC
|
|
public static demo = () => {
|
|
// Create the activity log element
|
|
const activityLog = document.createElement('dees-appui-activitylog') as DeesAppuiActivitylog;
|
|
|
|
// Add demo entries after the element is connected
|
|
setTimeout(() => {
|
|
activityLog.addMany([
|
|
{ type: 'login', user: 'John Doe', message: 'logged in from Chrome on macOS' },
|
|
{ type: 'create', user: 'John Doe', message: 'created a new project "Frontend App"' },
|
|
{ type: 'update', user: 'Jane Smith', message: 'updated API documentation' },
|
|
{ type: 'view', user: 'John Doe', message: 'viewed dashboard analytics' },
|
|
{ type: 'delete', user: 'Admin', message: 'removed deprecated endpoint' },
|
|
{ type: 'custom', user: 'System', message: 'scheduled backup completed', iconName: 'lucide:database' },
|
|
{ type: 'logout', user: 'Alice Brown', message: 'logged out' },
|
|
{ type: 'create', user: 'Jane Smith', message: 'created invoice #1234' },
|
|
]);
|
|
|
|
// Subscribe to updates
|
|
activityLog.entries$.subscribe((entries) => {
|
|
console.log('Activity log updated:', entries.length, 'entries');
|
|
});
|
|
}, 100);
|
|
|
|
return html`
|
|
<dees-demowrapper>
|
|
<style>
|
|
.demo-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 600px;
|
|
background: ${cssManager.bdTheme('#f4f4f5', '#09090b')};
|
|
padding: 32px;
|
|
}
|
|
</style>
|
|
<div class="demo-container">
|
|
${activityLog}
|
|
</div>
|
|
</dees-demowrapper>
|
|
`;
|
|
};
|
|
|
|
// INSTANCE PROPERTIES
|
|
@state()
|
|
accessor entries: IActivityEntry[] = [];
|
|
|
|
@state()
|
|
accessor searchQuery: string = '';
|
|
|
|
@state()
|
|
accessor filterCriteria: { user?: string; type?: IActivityEntry['type'] } = {};
|
|
|
|
// RxJS Subject for reactive updates
|
|
public entries$ = new domtools.plugins.smartrx.rxjs.Subject<IActivityEntry[]>();
|
|
|
|
// STYLES
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
css`
|
|
:host {
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
position: relative;
|
|
display: block;
|
|
width: 100%;
|
|
max-width: 320px;
|
|
height: 100%;
|
|
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
|
font-family: 'Geist Mono', monospace;
|
|
border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
cursor: default;
|
|
box-shadow: ${cssManager.bdTheme(
|
|
'-4px 0 12px rgba(0, 0, 0, 0.02)',
|
|
'-4px 0 12px rgba(0, 0, 0, 0.2)'
|
|
)};
|
|
}
|
|
.maincontainer {
|
|
position: absolute;
|
|
top: 0px;
|
|
left: 0px;
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
.topbar {
|
|
position: absolute;
|
|
top: 0px;
|
|
height: 48px;
|
|
width: 100%;
|
|
padding: 0px 16px;
|
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
display: flex;
|
|
align-items: center;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.topbar .heading {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
font-family: 'Geist Sans', sans-serif;
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
}
|
|
|
|
.activityContainer {
|
|
position: absolute;
|
|
top: 48px;
|
|
bottom: 48px;
|
|
width: 100%;
|
|
padding: 12px 0px;
|
|
overflow-y: auto;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: ${cssManager.bdTheme('#e5e7eb', '#27272a')} transparent;
|
|
}
|
|
|
|
.activityContainer::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.activityContainer::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.activityContainer::-webkit-scrollbar-thumb {
|
|
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.activityContainer::-webkit-scrollbar-thumb:hover {
|
|
background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
|
}
|
|
|
|
.empty-state {
|
|
font-size: 13px;
|
|
text-align: center;
|
|
padding: 32px 16px;
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
font-family: 'Geist Sans', sans-serif;
|
|
}
|
|
|
|
.streamingIndicator {
|
|
font-size: 11px;
|
|
text-align: center;
|
|
padding: 16px;
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
font-family: 'Geist Sans', sans-serif;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-weight: 500;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.streamingIndicator::before {
|
|
content: '';
|
|
width: 6px;
|
|
height: 6px;
|
|
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
|
border-radius: 50%;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
|
50% { opacity: 1; transform: scale(1.2); }
|
|
}
|
|
|
|
.date-separator {
|
|
padding: 12px 16px 8px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
background: ${cssManager.bdTheme('#f9fafb', '#09090b')};
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
.activityentry {
|
|
min-height: 36px;
|
|
font-size: 13px;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
transition: all 0.15s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
line-height: 1.4;
|
|
animation: fadeIn 0.3s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-4px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.activityentry:last-of-type {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.activityentry:hover {
|
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
}
|
|
|
|
.timestamp {
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
font-weight: 500;
|
|
font-size: 12px;
|
|
font-variant-numeric: tabular-nums;
|
|
flex-shrink: 0;
|
|
min-width: 45px;
|
|
}
|
|
|
|
.activity-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.activity-icon.login {
|
|
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.1)')};
|
|
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
|
|
}
|
|
|
|
.activity-icon.logout {
|
|
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
|
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
}
|
|
|
|
.activity-icon.view {
|
|
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
|
color: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
|
|
}
|
|
|
|
.activity-icon.create {
|
|
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.1)')};
|
|
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
|
|
}
|
|
|
|
.activity-icon.update {
|
|
background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.1)', 'rgba(251, 146, 60, 0.1)')};
|
|
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
|
|
}
|
|
|
|
.activity-icon.delete {
|
|
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
|
|
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
|
}
|
|
|
|
.activity-icon.custom {
|
|
background: ${cssManager.bdTheme('rgba(100, 116, 139, 0.1)', 'rgba(100, 116, 139, 0.1)')};
|
|
color: ${cssManager.bdTheme('#475569', '#94a3b8')};
|
|
}
|
|
|
|
.activity-text {
|
|
flex: 1;
|
|
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
|
|
}
|
|
|
|
.activity-user {
|
|
font-weight: 600;
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
}
|
|
|
|
.searchbox {
|
|
position: absolute;
|
|
bottom: 0px;
|
|
width: 100%;
|
|
height: 48px;
|
|
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
padding: 8px;
|
|
}
|
|
|
|
.search-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 32px;
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
font-size: 14px;
|
|
pointer-events: none;
|
|
transition: color 0.15s ease;
|
|
}
|
|
|
|
.searchbox input {
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
|
width: 100%;
|
|
height: 100%;
|
|
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
border-radius: 6px;
|
|
padding: 0 12px 0 36px;
|
|
font-family: 'Geist Sans', sans-serif;
|
|
font-size: 13px;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.searchbox input::placeholder {
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
}
|
|
|
|
.searchbox input:focus {
|
|
outline: none;
|
|
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
|
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
|
}
|
|
|
|
.searchbox input:focus ~ .search-icon,
|
|
.search-wrapper:has(input:focus) .search-icon {
|
|
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
|
}
|
|
|
|
.bottomShadow {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 24px;
|
|
bottom: 48px;
|
|
background: ${cssManager.bdTheme(
|
|
'linear-gradient(180deg, transparent 0%, #fafafa 100%)',
|
|
'linear-gradient(180deg, transparent 0%, #0a0a0a 100%)'
|
|
)};
|
|
pointer-events: none;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.topShadow {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 24px;
|
|
top: 48px;
|
|
background: ${cssManager.bdTheme(
|
|
'linear-gradient(0deg, transparent 0%, #fafafa 100%)',
|
|
'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)'
|
|
)};
|
|
pointer-events: none;
|
|
opacity: 0.8;
|
|
}
|
|
`,
|
|
];
|
|
|
|
// RENDER
|
|
public render(): TemplateResult {
|
|
const filteredEntries = this.getFilteredEntries();
|
|
const groupedEntries = this.groupEntriesByDate(filteredEntries);
|
|
|
|
return html`
|
|
${domtools.elementBasic.styles}
|
|
<style></style>
|
|
<div class="maincontainer">
|
|
<div class="topbar">
|
|
<div class="heading">Activity Log</div>
|
|
</div>
|
|
<div class="activityContainer">
|
|
${filteredEntries.length > 0
|
|
? html`<div class="streamingIndicator">Live Updates</div>`
|
|
: ''}
|
|
|
|
${filteredEntries.length === 0
|
|
? html`<div class="empty-state">No activity entries</div>`
|
|
: groupedEntries.map(
|
|
(group) => html`
|
|
<div class="date-separator">${group.label}</div>
|
|
${group.entries.map((entry) => this.renderActivityEntry(entry))}
|
|
`
|
|
)}
|
|
</div>
|
|
<div class="searchbox">
|
|
<div class="search-wrapper">
|
|
<dees-icon class="search-icon" .icon=${'lucide:search'}></dees-icon>
|
|
<input
|
|
type="text"
|
|
placeholder="Search activities, users..."
|
|
.value=${this.searchQuery}
|
|
@input=${this.handleSearchInput}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="topShadow"></div>
|
|
<div class="bottomShadow"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderActivityEntry(entry: IActivityEntry): TemplateResult {
|
|
const timestamp = entry.timestamp || new Date();
|
|
const timeStr = this.formatTime(timestamp);
|
|
const iconName = entry.iconName || this.getIconForType(entry.type);
|
|
|
|
return html`
|
|
<div
|
|
class="activityentry"
|
|
@contextmenu=${(e: MouseEvent) => this.handleContextMenu(e, entry)}
|
|
>
|
|
<span class="timestamp">${timeStr}</span>
|
|
<div class="activity-icon ${entry.type}">
|
|
<dees-icon .icon=${iconName}></dees-icon>
|
|
</div>
|
|
<div class="activity-text">
|
|
<span class="activity-user">${entry.user}</span> ${entry.message}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// API METHODS
|
|
public add(entry: IActivityEntry): void {
|
|
const newEntry: IActivityEntry = {
|
|
...entry,
|
|
id: entry.id || this.generateId(),
|
|
timestamp: entry.timestamp || new Date(),
|
|
};
|
|
this.entries = [newEntry, ...this.entries];
|
|
this.entries$.next(this.entries);
|
|
}
|
|
|
|
public addMany(entries: IActivityEntry[]): void {
|
|
const newEntries = entries.map((entry) => ({
|
|
...entry,
|
|
id: entry.id || this.generateId(),
|
|
timestamp: entry.timestamp || new Date(),
|
|
}));
|
|
this.entries = [...newEntries.reverse(), ...this.entries];
|
|
this.entries$.next(this.entries);
|
|
}
|
|
|
|
public clear(): void {
|
|
this.entries = [];
|
|
this.entries$.next(this.entries);
|
|
}
|
|
|
|
public getEntries(): IActivityEntry[] {
|
|
return [...this.entries];
|
|
}
|
|
|
|
public filter(criteria: { user?: string; type?: IActivityEntry['type'] }): IActivityEntry[] {
|
|
return this.entries.filter((entry) => {
|
|
if (criteria.user && entry.user !== criteria.user) return false;
|
|
if (criteria.type && entry.type !== criteria.type) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public search(query: string): IActivityEntry[] {
|
|
const lowerQuery = query.toLowerCase();
|
|
return this.entries.filter(
|
|
(entry) =>
|
|
entry.message.toLowerCase().includes(lowerQuery) ||
|
|
entry.user.toLowerCase().includes(lowerQuery)
|
|
);
|
|
}
|
|
|
|
// PRIVATE HELPERS
|
|
private generateId(): string {
|
|
return `activity-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
private getFilteredEntries(): IActivityEntry[] {
|
|
let result = this.entries;
|
|
|
|
if (this.searchQuery) {
|
|
const lowerQuery = this.searchQuery.toLowerCase();
|
|
result = result.filter(
|
|
(entry) =>
|
|
entry.message.toLowerCase().includes(lowerQuery) ||
|
|
entry.user.toLowerCase().includes(lowerQuery)
|
|
);
|
|
}
|
|
|
|
if (this.filterCriteria.user || this.filterCriteria.type) {
|
|
result = result.filter((entry) => {
|
|
if (this.filterCriteria.user && entry.user !== this.filterCriteria.user) return false;
|
|
if (this.filterCriteria.type && entry.type !== this.filterCriteria.type) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private groupEntriesByDate(
|
|
entries: IActivityEntry[]
|
|
): Array<{ label: string; entries: IActivityEntry[] }> {
|
|
const groups: Map<string, IActivityEntry[]> = new Map();
|
|
const today = new Date();
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
for (const entry of entries) {
|
|
const date = entry.timestamp || new Date();
|
|
let label: string;
|
|
|
|
if (this.isSameDay(date, today)) {
|
|
label = 'Today';
|
|
} else if (this.isSameDay(date, yesterday)) {
|
|
label = 'Yesterday';
|
|
} else {
|
|
label = date.toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: date.getFullYear() !== today.getFullYear() ? 'numeric' : undefined,
|
|
});
|
|
}
|
|
|
|
if (!groups.has(label)) {
|
|
groups.set(label, []);
|
|
}
|
|
groups.get(label)!.push(entry);
|
|
}
|
|
|
|
return Array.from(groups.entries()).map(([label, entries]) => ({
|
|
label,
|
|
entries,
|
|
}));
|
|
}
|
|
|
|
private isSameDay(date1: Date, date2: Date): boolean {
|
|
return (
|
|
date1.getFullYear() === date2.getFullYear() &&
|
|
date1.getMonth() === date2.getMonth() &&
|
|
date1.getDate() === date2.getDate()
|
|
);
|
|
}
|
|
|
|
private formatTime(date: Date): string {
|
|
return date.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false,
|
|
});
|
|
}
|
|
|
|
private getIconForType(type: IActivityEntry['type']): string {
|
|
const icons: Record<IActivityEntry['type'], string> = {
|
|
login: 'lucide:logIn',
|
|
logout: 'lucide:logOut',
|
|
view: 'lucide:eye',
|
|
create: 'lucide:plus',
|
|
update: 'lucide:edit',
|
|
delete: 'lucide:trash2',
|
|
custom: 'lucide:activity',
|
|
};
|
|
return icons[type] || icons.custom;
|
|
}
|
|
|
|
private handleSearchInput(e: InputEvent): void {
|
|
const target = e.target as HTMLInputElement;
|
|
this.searchQuery = target.value;
|
|
}
|
|
|
|
private handleContextMenu(e: MouseEvent, entry: IActivityEntry): void {
|
|
e.preventDefault();
|
|
DeesContextmenu.openContextMenuWithOptions(e, [
|
|
{
|
|
name: 'Copy activity',
|
|
iconName: 'lucide:copy',
|
|
action: async () => {
|
|
await navigator.clipboard.writeText(`${entry.user} ${entry.message}`);
|
|
},
|
|
},
|
|
{
|
|
name: 'Filter by user',
|
|
iconName: 'lucide:user',
|
|
action: async () => {
|
|
this.filterCriteria = { user: entry.user };
|
|
},
|
|
},
|
|
{
|
|
name: 'Filter by type',
|
|
iconName: 'lucide:filter',
|
|
action: async () => {
|
|
this.filterCriteria = { type: entry.type };
|
|
},
|
|
},
|
|
{
|
|
name: 'Clear filters',
|
|
iconName: 'lucide:x',
|
|
action: async () => {
|
|
this.filterCriteria = {};
|
|
this.searchQuery = '';
|
|
},
|
|
},
|
|
]);
|
|
}
|
|
}
|