Skip to content

Activity Log ​

Overview ​

  1. Per-User Audit Trail: Admins can view a chronological log of actions taken by any workspace user — logins, asset operations, permission changes — displayed in a modal dialog directly from the user management table.
  2. Last 30 Days: The system surfaces activity from the trailing 30-day window per user. Older records are not shown in the UI.
  3. Infinite Scroll Pagination: Activity records are paginated server-side. The dialog fetches the next page automatically when the admin scrolls past 85% of the visible list, appending results without a full reload.
  4. Contextual Links: Each activity entry can include an optional activity_url that opens the related asset, folder, or portal in a new browser tab, giving admins direct navigation to the referenced resource.
  5. Access Control: The dialog is available to workspace admins only. It is exposed via the per-user options menu in the admin users table and is not accessible to standard workspace members or external users.

Architecture ​

The activity log feature is composed of three layers: the options menu, the parent table component, and the dialog itself.

When an admin opens the three-dot menu on any user row in the workspace settings users table, they see an "Activity Log" option. Clicking it emits a viewActivity event from usersMenuOption.vue up to wpAdminsTable.vue, which sets activeUser to the selected user object and flips user_log_dialog to true. The ActivityLogsDialog component, which is embedded at the bottom of wpAdminsTable's template, watches the dialog prop and responds by resetting its internal state and immediately fetching page one of that user's activity.

Inside the dialog, handleFetchActivities() calls GET /digital-assets/instance/user-activity with the workspace ID, the target user's ID, and the current page number. The API returns a standard paginated response with data, last_page, and current_page fields. The dialog appends each page's records to the local activities array and tracks pagination boundaries so it can stop fetching once the last page is reached. The scroll handler monitors a ref-attached container and triggers another fetch when the scroll position exceeds 85%.

Each activity row shows a formatted timestamp (MMM D, YYYY h:mm a via moment.js) and the log_details string. If activity_url is present, a small new-tab icon button appears inline, opening the link without closing the dialog.

File Structure ​

JavaScript Files (.js) ​

  • store/analytics/index.js — Analytics Vuex module (shared with insights)
    • Not directly used by activity log, but the module namespace is analytics
    • Holds general date and state utilities shared across analytics-adjacent features

Vue Component Files (.vue) ​

  • components/dam/Dialogs/Org-Settings/ActivityLogsDialog.vue — Activity log modal

    • Renders a 850px-wide persistent dialog with a two-column table (Date/Time + Logs)
    • Fetches paginated activity via GET /digital-assets/instance/user-activity
    • Infinite scroll triggered at 85% scroll depth on the list container
    • Displays skeleton loaders during initial fetch and during pagination load-more
    • Shows empty state (CollageEmptyIcon) when no records are found
  • components/dam/table/usersMenuOption.vue — Per-user options menu

    • Renders a v-menu with action items for active users: Edit, Activity Log, Reset Password, Deactivate
    • Emits viewActivity with the user object when "Activity Log" is clicked
    • Renders different menu lists for active vs. pending users (pending users get Resend/Revoke options instead)
  • components/dam/table/wpAdminsTable.vue — Admin users table (dialog host)

    • Owns activeUser (the user whose log to show) and user_log_dialog (boolean dialog flag)
    • Mounts ActivityLogsDialog as a persistent child component
    • Handles viewActivity from usersMenuOption via viewUserLog(user) method
    • Handles dialog close event via closeUserLogs(), resetting both state values

SVG Icon Components ​

  • components/svg/CollageEyeIcon.vue — Eye icon used for the "Activity Log" menu item
  • components/svg/CollageNewTabIcon.vue — New-tab icon rendered next to activity entries that have a URL
  • components/svg/CollageEmptyIcon.vue — Empty state illustration when no activity is found
  • components/svg/CollageCloseIcon.vue — Close button icon in the dialog header

ActivityLogsDialog Component ​

Features ​

  • Persistent dialog (non-dismissable by clicking outside) with a close button
  • Two-column layout: timestamp column (left) and log details column (65% width, right)
  • New-tab link button rendered conditionally per activity row when activity_url is present
  • Initial full-screen skeleton loader (9 skeleton rows) during first fetch
  • Inline load-more skeleton (6 rows) appended below the list during pagination
  • Auto-reset on dialog close: clears activities, currentPage, lastPage
  • Guards against duplicate fetches: skips if lastPage is set and currentPage >= lastPage, or if a load-more is already in progress

Props ​

javascript
{
  user: {
    type: Object,
    required: true    // Must have at least { id: String }
  },
  dialog: {
    type: Boolean,
    default: false    // Watched — opening triggers reset + fetch
  }
}

Events ​

javascript
{
  'close': () => {}  // Emitted when admin clicks the X button; parent resets dialog flag
}

Methods ​

javascript
{
  // Reset all pagination state and clear the activity list
  reset() {},

  // Close dialog, emit 'close', then reset
  closeDialog() {},

  // Fetch one page of activities; flag = 'default' (initial) or 'scroll' (pagination)
  // Sets isLoading for initial fetch, loadMore for scroll-triggered fetches
  async handleFetchActivities(flag = 'default') {},

  // Scroll event handler on the list container ref
  // Triggers handleFetchActivities('scroll') when scrolled >= 85%
  async handleScroll() {}
}

Usage ​

vue
<template>
  <ActivityLogsDialog
    :dialog="user_log_dialog"
    :user="activeUser || {}"
    @close="closeUserLogs()"
  />
</template>

<script>
export default {
  components: {
    ActivityLogsDialog: () =>
      import('~/components/dam/Dialogs/Org-Settings/ActivityLogsDialog.vue'),
  },
  data() {
    return {
      activeUser: null,
      user_log_dialog: false,
    }
  },
  methods: {
    viewUserLog(user) {
      this.activeUser = user
      this.user_log_dialog = true
    },
    closeUserLogs() {
      this.user_log_dialog = false
      this.activeUser = null
    },
  },
}
</script>

usersMenuOption Component ​

Features ​

  • Context menu rendered inside a v-menu offset-y
  • Two menu lists: activeMenuList (Edit, Activity Log, Reset Password, Deactivate) and pendingMenuList (Resend Invitation, Revoke Access)
  • Menu items disabled based on checkDisabled(menu) — prevents actions on users in incompatible states
  • All click events are defined as closures in the data() function using EventBus or $emit

Props ​

javascript
{
  user: {
    type: Object,
    required: true  // The user row object from the parent table
  },
  userType: {
    type: String,
    default: ''     // 'active-users' | 'pending-users' — controls which menu list to show
  }
}

Events ​

javascript
{
  'viewActivity': (user) => {}  // Emitted when "Activity Log" is clicked; carries full user object
}

Methods ​

javascript
{
  // Returns true if the menu item should be rendered disabled for this user state
  checkDisabled(menu) {}
}

wpAdminsTable Component (activity log surface) ​

Features ​

  • Hosts ActivityLogsDialog at the bottom of its template (always mounted, toggled via dialog prop)
  • Renders usersMenuOption per row; receives viewActivity events to open the log dialog
  • Shows a last_activity timestamp column for active users (formatted as MMM D, YYYY h:mm a)
  • Supports infinite scroll on the user list itself; emits load-more to the parent page

Props ​

javascript
{
  userList: {
    type: Array,
    required: true   // Array of user objects from the API
  },
  instances: {
    type: Array,
    default: () => []  // Available portal instances for name resolution
  },
  mainFlag: {
    type: String,
    default: ''      // 'users-list' | 'admins-list' | 'portal-users'
  },
  subFlag: {
    type: String,
    default: 'active-users'  // 'active-users' | 'pending-users' | 'deactive-users'
  },
  loadMore: {
    type: Boolean,
    default: false   // Shows skeleton loader rows when true
  },
  showSortArrow: {
    type: Boolean,
    default: false
  },
  sortObj: {
    type: Object,
    default: null    // { field: 'name'|'email', sort_by: 'ASC'|'DESC' }
  },
  sortLoaderVal: {
    type: String,
    default: ''
  }
}

Events ​

javascript
{
  'sort': (field) => {},     // Emitted when a sortable column header is clicked
  'load-more': () => {}      // Emitted when the user list is scrolled past 85%
}

Methods ​

javascript
{
  // Opens ActivityLogsDialog for the given user
  viewUserLog(user) {},

  // Closes the dialog and clears activeUser
  closeUserLogs() {},

  // Scroll handler for the user list container — emits 'load-more' at 85%
  async handleScroll() {},

  // Maps a name string to a CSS class for avatar letter color
  getFirstCharClass(item) {},

  // Resolves instance_ids to portal brand names (comma-separated)
  getPortalNames(user) {}
}

Workflows ​

View Activity Log for a User ​

1. Admin navigates to workspace settings → users list
   Route: /:workspace_id/workspace-settings/user
   Page renders wpAdminsTable with mainFlag: 'users-list', subFlag: 'active-users'
   ↓
2. Admin clicks the three-dot menu icon on a user row
   Component: usersMenuOption.vue
   Renders v-menu with activeMenuList options
   ↓
3. Admin selects "Activity Log"
   usersMenuOption emits: $emit('viewActivity', this.user)
   wpAdminsTable.viewUserLog(user) called
   Sets: activeUser = user, user_log_dialog = true
   ↓
4. ActivityLogsDialog receives dialog: true via prop
   watch(dialog) fires → calls reset() then handleFetchActivities('default')
   Sets: isLoading = true, currentPage = 1
   ↓
5. GET /digital-assets/instance/user-activity
   Params: { url_workspace_id, workspace_id, page: 1, user_id }
   Response: { data: [...], last_page: N, current_page: 1 }
   ↓
6. Dialog renders activity list
   Columns: Date/Time | Logs
   Each row: moment-formatted timestamp + log_details text
   Optional new-tab link button if activity_url present
   ↓
7. Admin scrolls the activity list
   handleScroll() fires on container scroll event
   When scrollTop / scrollHeight >= 85%:
     handleFetchActivities('scroll') called
     Sets: loadMore = true, currentPage += 1
   ↓
8. Next page fetched
   New records appended to activities array
   loadMore set to false once response arrives
   Stops fetching when currentPage >= lastPage
   ↓
9. Admin closes the dialog
   Clicks X button → closeDialog() → $emit('close')
   wpAdminsTable.closeUserLogs(): user_log_dialog = false, activeUser = null
   Dialog.reset(): clears activities, currentPage, lastPage

API Integration ​

Endpoints ​

MethodPathDescription
GET/digital-assets/instance/user-activityPaginated activity log for a specific user

Fetch Activity Log Request ​

json
GET /digital-assets/instance/user-activity
    ?url_workspace_id=ws_abc123
    &workspace_id=ws_abc123
    &page=1
    &user_id=usr_xyz789

Fetch Activity Log Response (success) ​

json
{
  "data": {
    "current_page": 1,
    "last_page": 3,
    "data": [
      {
        "id": 1001,
        "created_at": "2026-08-10T14:32:00.000Z",
        "log_details": "Uploaded asset brand-logo.png to folder /Marketing/2026",
        "activity_url": "https://app.collage.inc/ws_abc123/dam/files/asset_001"
      },
      {
        "id": 1000,
        "created_at": "2026-08-10T12:15:00.000Z",
        "log_details": "Downloaded 3 assets from portal Summer Campaign",
        "activity_url": null
      },
      {
        "id": 999,
        "created_at": "2026-08-09T09:00:00.000Z",
        "log_details": "Logged in from Chrome on macOS",
        "activity_url": null
      }
    ]
  }
}

Fetch Activity Log Response (no activity) ​

json
{
  "data": {
    "current_page": 1,
    "last_page": 1,
    "data": []
  }
}

Component Integration ​

Full working example of the three-component stack: the settings page integrating the table, which hosts the dialog.

vue
<template>
  <div>
    <wpAdminsTable
      :userList="userList"
      :instances="portalInstances"
      mainFlag="users-list"
      subFlag="active-users"
      :loadMore="loadingMore"
      @load-more="fetchNextPage"
      @sort="handleSort"
    />
  </div>
</template>

<script>
export default {
  layout: 'damLayout',
  components: {
    wpAdminsTable: () => import('~/components/dam/table/wpAdminsTable.vue'),
  },
  data() {
    return {
      userList: [],
      portalInstances: [],
      loadingMore: false,
      currentPage: 1,
      lastPage: null,
    }
  },
  async mounted() {
    await this.fetchUsers()
  },
  methods: {
    async fetchUsers() {
      try {
        const { data } = await this.$axios.$get(
          '/digital-assets/instance/users',
          {
            params: {
              workspace_id: this.$route.params.workspace_id,
              url_workspace_id: this.$route.params.workspace_id,
              page: this.currentPage,
            },
          }
        )
        this.lastPage = data.last_page
        this.userList.push(...data.data)
      } catch (error) {
        this.$snackbar.error(this.$getErrorMessage(error))
      }
    },
    async fetchNextPage() {
      if (this.loadingMore || (this.lastPage && this.currentPage >= this.lastPage)) return
      this.loadingMore = true
      this.currentPage += 1
      await this.fetchUsers()
      this.loadingMore = false
    },
    handleSort(field) {
      // Re-fetch with sort params
    },
  },
}
</script>