Commit 484fe31b authored by Abdul Wadood's avatar Abdul Wadood Committed by Ezekiel Kigbo

Add "Created on" & "Last activity" columns to groups and projects page

The columns are sortable.
We want to achieve parity between self and SaaS user. Until now this
info was only present in the admin panel.

Changelog: added
parent 7680b4b9
......@@ -5,6 +5,7 @@ import MembersTableCell from 'ee_else_ce/members/components/table/members_table_
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
FIELD_KEY_ACTIONS,
FIELDS,
......@@ -40,6 +41,7 @@ export default {
RemoveGroupLinkModal,
RemoveMemberModal,
ExpirationDatepicker,
UserDate,
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
......@@ -287,6 +289,14 @@ export default {
</members-table-cell>
</template>
<template #cell(userCreatedAt)="{ item: member }">
<user-date :date="member.user.createdAt" />
</template>
<template #cell(lastActivityOn)="{ item: member }">
<user-date :date="member.user.lastActivityOn" />
</template>
<template #cell(actions)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
<member-action-buttons
......
......@@ -9,6 +9,8 @@ export const FIELD_KEY_GRANTED = 'granted';
export const FIELD_KEY_INVITED = 'invited';
export const FIELD_KEY_REQUESTED = 'requested';
export const FIELD_KEY_MAX_ROLE = 'maxRole';
export const FIELD_KEY_USER_CREATED_AT = 'userCreatedAt';
export const FIELD_KEY_LAST_ACTIVITY_ON = 'lastActivityOn';
export const FIELD_KEY_EXPIRATION = 'expiration';
export const FIELD_KEY_LAST_SIGN_IN = 'lastSignIn';
export const FIELD_KEY_ACTIONS = 'actions';
......@@ -66,6 +68,22 @@ export const FIELDS = [
thClass: 'col-expiration',
tdClass: 'col-expiration',
},
{
key: FIELD_KEY_USER_CREATED_AT,
label: __('Created on'),
sort: {
asc: 'oldest_created_user',
desc: 'recent_created_user',
},
},
{
key: FIELD_KEY_LAST_ACTIVITY_ON,
label: __('Last activity'),
sort: {
asc: 'oldest_last_activity',
desc: 'recent_last_activity',
},
},
{
key: FIELD_KEY_LAST_SIGN_IN,
label: __('Last sign-in'),
......
......@@ -12,9 +12,16 @@ const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-group-members-list-app'), {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
tableSortableFields: [
'account',
'granted',
'maxRole',
'lastSignIn',
'userCreatedAt',
'lastActivityOn',
],
requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
show: true,
......
......@@ -18,9 +18,16 @@ initInviteGroupTrigger();
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list-app'), {
[MEMBER_TYPES.user]: {
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
tableSortableFields: [
'account',
'granted',
'maxRole',
'lastSignIn',
'userCreatedAt',
'lastActivityOn',
],
requestFormatter: projectMemberRequestFormatter,
filteredSearchBar: {
show: true,
......
......@@ -181,6 +181,10 @@ class Member < ApplicationRecord
scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
scope :order_recent_last_activity, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_activity_on', 'DESC')) }
scope :order_oldest_last_activity, -> { left_join_users.reorder(Gitlab::Database.nulls_first_order('users.last_activity_on', 'ASC')) }
scope :order_recent_created_user, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.created_at', 'DESC')) }
scope :order_oldest_created_user, -> { left_join_users.reorder(Gitlab::Database.nulls_first_order('users.created_at', 'ASC')) }
scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) }
......@@ -232,6 +236,10 @@ class Member < ApplicationRecord
when 'access_level_desc' then reorder(access_level: :desc)
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
when 'recent_created_user' then order_recent_created_user
when 'oldest_created_user' then order_oldest_created_user
when 'recent_last_activity' then order_recent_last_activity
when 'oldest_last_activity' then order_oldest_last_activity
when 'last_joined' then order_created_desc
when 'oldest_joined' then order_created_asc
else
......
......@@ -5,6 +5,9 @@ class MemberUserEntity < UserEntity
unexpose :state
unexpose :status_tooltip_html
expose :created_at
expose :last_activity_on
expose :avatar_url do |user|
user.avatar_url(size: Member::AVATAR_SIZE, only_path: false)
end
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Groups > Members > Sort members', :js do
include Spec::Support::Helpers::Features::MembersHelpers
let(:owner) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:owner) { create(:user, name: 'John Doe', created_at: 5.days.ago, last_activity_on: Date.today) }
let(:developer) { create(:user, name: 'Mary Jane', created_at: 1.day.ago, last_sign_in_at: 5.days.ago, last_activity_on: Date.today - 5) }
let(:group) { create(:group) }
before do
......@@ -50,6 +50,42 @@ RSpec.describe 'Groups > Members > Sort members', :js do
expect_sort_by('Max role', :desc)
end
it 'sorts by user created on ascending' do
visit_members_list(sort: :oldest_created_user)
expect(first_row.text).to include(owner.name)
expect(second_row.text).to include(developer.name)
expect_sort_by('Created on', :asc)
end
it 'sorts by user created on descending' do
visit_members_list(sort: :recent_created_user)
expect(first_row.text).to include(developer.name)
expect(second_row.text).to include(owner.name)
expect_sort_by('Created on', :desc)
end
it 'sorts by last activity ascending' do
visit_members_list(sort: :oldest_last_activity)
expect(first_row.text).to include(developer.name)
expect(second_row.text).to include(owner.name)
expect_sort_by('Last activity', :asc)
end
it 'sorts by last activity descending' do
visit_members_list(sort: :recent_last_activity)
expect(first_row.text).to include(owner.name)
expect(second_row.text).to include(developer.name)
expect_sort_by('Last activity', :desc)
end
it 'sorts by access granted ascending' do
visit_members_list(sort: :last_joined)
......
......@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe 'Projects > Members > Sorting', :js do
include Spec::Support::Helpers::Features::MembersHelpers
let(:maintainer) { create(:user, name: 'John Doe') }
let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
let(:maintainer) { create(:user, name: 'John Doe', created_at: 5.days.ago, last_activity_on: Date.today) }
let(:developer) { create(:user, name: 'Mary Jane', created_at: 1.day.ago, last_sign_in_at: 5.days.ago, last_activity_on: Date.today - 5) }
let(:project) { create(:project, namespace: maintainer.namespace, creator: maintainer) }
before do
......@@ -42,6 +42,42 @@ RSpec.describe 'Projects > Members > Sorting', :js do
expect_sort_by('Max role', :desc)
end
it 'sorts by user created on ascending' do
visit_members_list(sort: :oldest_created_user)
expect(first_row.text).to have_content(maintainer.name)
expect(second_row.text).to have_content(developer.name)
expect_sort_by('Created on', :asc)
end
it 'sorts by user created on descending' do
visit_members_list(sort: :recent_created_user)
expect(first_row.text).to have_content(developer.name)
expect(second_row.text).to have_content(maintainer.name)
expect_sort_by('Created on', :desc)
end
it 'sorts by last activity ascending' do
visit_members_list(sort: :oldest_last_activity)
expect(first_row.text).to have_content(developer.name)
expect(second_row.text).to have_content(maintainer.name)
expect_sort_by('Last activity', :asc)
end
it 'sorts by last activity descending' do
visit_members_list(sort: :recent_last_activity)
expect(first_row.text).to have_content(maintainer.name)
expect(second_row.text).to have_content(developer.name)
expect_sort_by('Last activity', :desc)
end
it 'sorts by access granted ascending' do
visit_members_list(sort: :last_joined)
......
{
"type": "object",
"required": ["id", "name", "username", "avatar_url", "web_url", "blocked", "two_factor_enabled", "show_status"],
"required": [
"id",
"name",
"username",
"created_at",
"last_activity_on",
"avatar_url",
"web_url",
"blocked",
"two_factor_enabled",
"show_status"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"created_at": { "type": ["string"] },
"avatar_url": { "type": ["string", "null"] },
"web_url": { "type": "string" },
"blocked": { "type": "boolean" },
"two_factor_enabled": { "type": "boolean" },
"availability": { "type": ["string", "null"] },
"last_activity_on": { "type": ["string", "null"] },
"status": {
"type": "object",
"required": ["emoji"],
......
......@@ -10,6 +10,7 @@ import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import UserDate from '~/vue_shared/components/user_date.vue';
import {
MEMBER_TYPES,
MEMBER_STATE_CREATED,
......@@ -106,14 +107,16 @@ describe('MembersTable', () => {
};
it.each`
field | label | member | expectedComponent
${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
field | label | member | expectedComponent
${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
${'userCreatedAt'} | ${'Created on'} | ${memberMock} | ${UserDate}
${'lastActivityOn'} | ${'Last activity'} | ${memberMock} | ${UserDate}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({
members: [member],
......
......@@ -17,6 +17,7 @@ export const member = {
state: MEMBER_STATE_CREATED,
user: {
id: 123,
createdAt: '2022-03-10T18:03:04.812Z',
name: 'Administrator',
username: 'root',
webUrl: 'https://gitlab.com/root',
......@@ -26,6 +27,7 @@ export const member = {
oncallSchedules: [{ name: 'schedule 1' }],
escalationPolicies: [{ name: 'policy 1' }],
availability: null,
lastActivityOn: '2022-03-15',
showStatus: true,
},
id: 238,
......
......@@ -926,4 +926,64 @@ RSpec.describe Member do
end
end
end
describe '.sort_by_attribute' do
let_it_be(:user1) { create(:user, created_at: Date.today, last_sign_in_at: Date.today, last_activity_on: Date.today, name: 'Alpha') }
let_it_be(:user2) { create(:user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, last_activity_on: Date.today - 1, name: 'Omega') }
let_it_be(:user3) { create(:user, created_at: Date.today - 2, name: 'Beta') }
let_it_be(:group) { create(:group) }
let_it_be(:member1) { create(:group_member, :reporter, group: group, user: user1) }
let_it_be(:member2) { create(:group_member, :developer, group: group, user: user2) }
let_it_be(:member3) { create(:group_member, :maintainer, group: group, user: user3) }
it 'sort users in ascending order by access-level' do
expect(described_class.sort_by_attribute('access_level_asc')).to eq([member1, member2, member3])
end
it 'sort users in descending order by access-level' do
expect(described_class.sort_by_attribute('access_level_desc')).to eq([member3, member2, member1])
end
context 'when sort by recent_sign_in' do
subject { described_class.sort_by_attribute('recent_sign_in') }
it 'sorts users by recent sign-in time' do
expect(subject.first).to eq(member1)
expect(subject.second).to eq(member2)
end
it 'pushes users who never signed in to the end' do
expect(subject.third).to eq(member3)
end
end
context 'when sort by oldest_sign_in' do
subject { described_class.sort_by_attribute('oldest_sign_in') }
it 'sorts users by the oldest sign-in time' do
expect(subject.first).to eq(member2)
expect(subject.second).to eq(member1)
end
it 'pushes users who never signed in to the end' do
expect(subject.third).to eq(member3)
end
end
it 'sorts users in descending order by their creation time' do
expect(described_class.sort_by_attribute('recent_created_user')).to eq([member1, member2, member3])
end
it 'sorts users in ascending order by their creation time' do
expect(described_class.sort_by_attribute('oldest_created_user')).to eq([member3, member2, member1])
end
it 'sort users by recent last activity' do
expect(described_class.sort_by_attribute('recent_last_activity')).to eq([member1, member2, member3])
end
it 'sort users by oldest last activity' do
expect(described_class.sort_by_attribute('oldest_last_activity')).to eq([member3, member2, member1])
end
end
end
......@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe MemberUserEntity do
let_it_be(:user) { create(:user) }
let_it_be(:user) { create(:user, last_activity_on: Date.today) }
let_it_be(:emoji) { 'slight_smile' }
let_it_be(:user_status) { create(:user_status, user: user, emoji: emoji) }
......@@ -36,4 +36,12 @@ RSpec.describe MemberUserEntity do
it 'correctly exposes `status.emoji`' do
expect(entity_hash[:status][:emoji]).to match(emoji)
end
it 'correctly exposes `created_at`' do
expect(entity_hash[:created_at]).to be(user.created_at)
end
it 'correctly exposes `last_activity_on`' do
expect(entity_hash[:last_activity_on]).to be(user.last_activity_on)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment