Make bot user profiles stand out from other users

This makes a few changes to how bot users' information is displayed in
the UI to make them more distinguishable from normal users.

These changes affect the follow views:

- User profile view (made more activity-centric)
- User popover (show a link to bot's documentation, only for security
bot at the moment)
- Show a link to the docs in the users admin
parent 7573c25d
......@@ -42,6 +42,7 @@ const populateUserInfo = user => {
bio: userData.bio,
bioHtml: sanitize(userData.bio_html),
workInformation: userData.work_information,
websiteUrl: userData.website_url,
loaded: true,
});
}
......
<script>
/* eslint-disable vue/no-v-html */
import { GlPopover, GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlIcon } from '@gitlab/ui';
import {
GlPopover,
GlLink,
GlDeprecatedSkeletonLoading as GlSkeletonLoading,
GlIcon,
} from '@gitlab/ui';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
const MAX_SKELETON_LINES = 4;
const SECURITY_BOT_USER_DATA = {
username: 'security-bot',
name: 'GitLab Security Bot',
};
export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
components: {
GlIcon,
GlLink,
GlPopover,
GlSkeletonLoading,
UserAvatarImage,
......@@ -43,6 +54,15 @@ export default {
userIsLoading() {
return !this.user?.loaded;
},
isSecurityBot() {
const { username, name, websiteUrl = '' } = this.user;
return (
gon.features?.securityAutoFix &&
username === SECURITY_BOT_USER_DATA.username &&
name === SECURITY_BOT_USER_DATA.name &&
websiteUrl.length
);
},
},
};
</script>
......@@ -89,6 +109,12 @@ export default {
<div v-if="statusHtml" class="js-user-status gl-mt-3">
<span v-html="statusHtml"></span>
</div>
<div v-if="isSecurityBot" class="gl-text-blue-500">
<gl-icon name="question" />
<gl-link data-testid="user-popover-security-bot-docs-link" :href="user.websiteUrl">
{{ sprintf(__('Learn more about %{username}'), { username: user.name }) }}
</gl-link>
</div>
</template>
</div>
</div>
......
......@@ -15,3 +15,5 @@
.row-second-line.str-truncated-100
= mail_to user.email, user.email, class: 'text-secondary'
- unless Feature.disabled?(:security_auto_fix) || !user.internal? || user.website_url.blank?
= link_to "(#{_('more information')})", user.website_url
- activity_pane_class = Feature.enabled?(:security_auto_fix) && @user.bot? ? "col-12" : "col-md-12 col-lg-6"
.row
.col-12
.calendar-block.gl-mt-3.gl-mb-3
......@@ -6,25 +8,26 @@
.spinner.spinner-md
.user-calendar-activities.d-none.d-sm-block
.row
.col-md-12.col-lg-6
%div{ class: activity_pane_class }
- if can?(current_user, :read_cross_project)
.activities-block
.gl-mt-5
.d-flex.align-items-center.border-bottom
%h4.flex-grow
= s_('UserProfile|Activity')
.gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_path } }
.center.light.loading
.spinner.spinner-md
.col-md-12.col-lg-6
.projects-block
.gl-mt-5
.d-flex.align-items-center.border-bottom
%h4.flex-grow
= s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
.spinner.spinner-md
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
.col-md-12.col-lg-6
.projects-block
.gl-mt-5
.gl-display-flex.gl-align-items-center.gl-border-b-1.gl-border-b-gray-100.gl-border-b-solid
%h4.gl-flex-grow-1
= s_('UserProfile|Personal projects')
= link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_projects_path } }
.center.light.loading
.spinner.spinner-md
......@@ -78,6 +78,8 @@
= sprite_icon('twitter')
- unless @user.website_url.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
- if Feature.enabled?(:security_auto_fix) && @user.bot?
= sprite_icon('question', css_class: 'gl-text-blue-600')
= link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow'
- unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider-sm.d-block.d-sm-inline.mt-1.mt-sm-0
......@@ -101,26 +103,27 @@
%li.js-activity-tab
= link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
= s_('UserProfile|Activity')
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
= s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
= s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
- unless Feature.enabled?(:security_auto_fix) && @user.bot?
- if profile_tab?(:groups)
%li.js-groups-tab
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
= s_('UserProfile|Groups')
- if profile_tab?(:contributed)
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
= s_('UserProfile|Contributed projects')
- if profile_tab?(:projects)
%li.js-projects-tab
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
= s_('UserProfile|Personal projects')
- if profile_tab?(:starred)
%li.js-starred-tab
= link_to user_starred_projects_path, data: { target: 'div#starred', action: 'starred', toggle: 'tab', endpoint: user_starred_projects_path(format: :json) } do
= s_('UserProfile|Starred projects')
- if profile_tab?(:snippets)
%li.js-snippets-tab
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
= s_('UserProfile|Snippets')
%div{ class: container_class }
.tab-content
......@@ -136,26 +139,26 @@
.content_list{ data: { href: user_path } }
.loading
.spinner.spinner-md
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
- unless @user.bot?
- if profile_tab?(:groups)
#groups.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:contributed)
#contributed.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:projects)
#projects.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:starred)
#starred.tab-pane
-# This tab is always loaded via AJAX
- if profile_tab?(:snippets)
#snippets.tab-pane
-# This tab is always loaded via AJAX
.loading.hide
.spinner.spinner-md
......
......@@ -46,6 +46,7 @@ module Gitlab
push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
push_frontend_feature_flag(:snippets_binary_blob, default_enabled: false)
push_frontend_feature_flag(:usage_data_api, default_enabled: true)
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
# Startup CSS feature is a special one as it can be enabled by means of cookies and params
gon.push({ features: { 'startupCss' => use_startup_css? } }, true)
......
......@@ -15244,6 +15244,9 @@ msgstr ""
msgid "Learn more"
msgstr ""
msgid "Learn more about %{username}"
msgstr ""
msgid "Learn more about Auto DevOps"
msgstr ""
......@@ -28600,6 +28603,9 @@ msgstr ""
msgid "UserProfile|Blocked user"
msgstr ""
msgid "UserProfile|Bot activity"
msgstr ""
msgid "UserProfile|Contributed projects"
msgstr ""
......@@ -31217,6 +31223,9 @@ msgstr ""
msgid "missing"
msgstr ""
msgid "more information"
msgstr ""
msgid "most recent deployment"
msgstr ""
......
......@@ -21,15 +21,15 @@ RSpec.describe 'Overview tab on a user profile', :js do
sign_in user
end
describe 'activities section' do
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'activities section' do
describe 'user has no activities' do
include_context 'visit overview tab'
......@@ -84,14 +84,6 @@ RSpec.describe 'Overview tab on a user profile', :js do
end
describe 'projects section' do
shared_context 'visit overview tab' do
before do
visit user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'user has no personal projects' do
include_context 'visit overview tab'
......@@ -158,4 +150,52 @@ RSpec.describe 'Overview tab on a user profile', :js do
end
end
end
describe 'bot user' do
let(:bot_user) { create(:user, user_type: :security_bot) }
shared_context "visit bot's overview tab" do
before do
visit bot_user.username
page.find('.js-overview-tab a').click
wait_for_requests
end
end
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
end
include_context "visit bot's overview tab"
it "activity panel's title is 'Bot activity'" do
page.within('.activities-block') do
expect(page).to have_text('Bot activity')
end
end
it 'does not show projects panel' do
expect(page).not_to have_selector('.projects-block')
end
end
describe 'feature flag disabled' do
before do
stub_feature_flags(security_auto_fix: false)
end
include_context "visit bot's overview tab"
it "activity panel's title is not 'Bot activity'" do
page.within('.activities-block') do
expect(page).not_to have_text('Bot activity')
end
end
it 'shows projects panel' do
expect(page).to have_selector('.projects-block')
end
end
end
end
......@@ -182,4 +182,46 @@ RSpec.describe 'User page' do
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
end
context 'with a bot user' do
let(:user) { create(:user, user_type: :security_bot) }
describe 'feature flag enabled' do
before do
stub_feature_flags(security_auto_fix: true)
end
it 'only shows Overview and Activity tabs' do
visit(user_path(user))
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).not_to have_link('Groups')
expect(page).not_to have_link('Contributed projects')
expect(page).not_to have_link('Personal projects')
expect(page).not_to have_link('Snippets')
end
end
end
describe 'feature flag disabled' do
before do
stub_feature_flags(security_auto_fix: false)
end
it 'only shows Overview and Activity tabs' do
visit(user_path(user))
page.within '.nav-links' do
expect(page).to have_link('Overview')
expect(page).to have_link('Activity')
expect(page).to have_link('Groups')
expect(page).to have_link('Contributed projects')
expect(page).to have_link('Personal projects')
expect(page).to have_link('Snippets')
end
end
end
end
end
......@@ -21,6 +21,9 @@ describe('User Popover Component', () => {
let wrapper;
beforeEach(() => {
window.gon.features = {
securityAutoFix: true,
};
loadFixtures(fixtureTemplate);
});
......@@ -28,6 +31,7 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
const findByTestId = testid => wrapper.find(`[data-testid="${testid}"]`);
const findUserStatus = () => wrapper.find('.js-user-status');
const findTarget = () => document.querySelector('.js-user-link');
......@@ -196,4 +200,30 @@ describe('User Popover Component', () => {
expect(findUserStatus().exists()).toBe(false);
});
});
describe('security bot', () => {
const SECURITY_BOT_USER = {
...DEFAULT_PROPS.user,
name: 'GitLab Security Bot',
username: 'security-bot',
websiteUrl: '/security/bot/docs',
};
const findSecurityBotDocsLink = () => findByTestId('user-popover-security-bot-docs-link');
it("shows a link to the bot's documentation", () => {
createWrapper({ user: SECURITY_BOT_USER });
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.exists()).toBe(true);
expect(securityBotDocsLink.attributes('href')).toBe(SECURITY_BOT_USER.websiteUrl);
});
it('does not show the link if the feature flag is disabled', () => {
window.gon.features = {
securityAutoFix: false,
};
createWrapper({ user: SECURITY_BOT_USER });
expect(findSecurityBotDocsLink().exists()).toBe(false);
});
});
});
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