Commit 456be7e9 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '345203-polish-slack-application-installation-page' into 'master'

Clean up Slack Application installation page

See merge request gitlab-org/gitlab!78094
parents 3d2d0430 81099a12
......@@ -874,14 +874,6 @@ Image Commenting cursor
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 12;
/*
Add GitLab Slack Application
*/
$add-to-slack-popup-max-width: 400px;
$add-to-slack-gif-max-width: 850px;
$add-to-slack-well-max-width: 750px;
$add-to-slack-logo-size: 100px;
/*
Security & Compliance Carousel
*/
......
......@@ -363,25 +363,12 @@ table.u2f-registrations {
color: $gl-text-color-secondary;
}
.gitlab-slack-gif {
width: 100%;
max-width: $add-to-slack-gif-max-width;
.gitlab-slack-body {
max-width: 420px;
}
.gitlab-slack-well {
background-color: $white;
box-shadow: none;
max-width: $add-to-slack-well-max-width;
}
.gitlab-slack-logo {
width: $add-to-slack-logo-size;
height: $add-to-slack-logo-size;
}
.gitlab-slack-popup {
width: 100%;
max-width: $add-to-slack-popup-max-width;
.gitlab-slack-slack-logo {
transform: scale(200%); // Slack logo SVG is scaled down 50% and has empty space around it
}
.skype-icon {
......
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlButton, GlIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import GitlabSlackService from '../services/gitlab_slack_service';
export default {
components: {
GlButton,
GlIcon,
},
props: {
projects: {
type: Array,
required: false,
default: () => [],
},
isSignedIn: {
type: Boolean,
required: true,
},
gitlabForSlackGifPath: {
type: String,
required: true,
},
signInPath: {
type: String,
required: true,
},
slackLinkPath: {
type: String,
required: true,
},
gitlabLogoPath: {
type: String,
required: true,
},
slackLogoPath: {
type: String,
required: true,
},
docsPath: {
type: String,
required: true,
},
},
data() {
return {
popupOpen: false,
selectedProjectId: this.projects && this.projects.length ? this.projects[0].id : 0,
};
},
computed: {
hasProjects() {
return this.projects.length > 0;
},
},
methods: {
togglePopup() {
this.popupOpen = !this.popupOpen;
},
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then((response) => redirectTo(response.data.add_to_slack_link))
.catch(() =>
createFlash({
message: __('Unable to build Slack link.'),
}),
);
},
},
};
</script>
<template>
<div class="text-center">
<h1>{{ __('GitLab for Slack') }}</h1>
<p>{{ __('Track your GitLab projects with GitLab for Slack.') }}</p>
<div v-once class="gl-my-5 gl-display-flex gl-justify-content-center gl-align-items-center">
<img :src="gitlabLogoPath" class="gitlab-slack-logo" />
<gl-icon name="double-headed-arrow" :size="72" class="gl-mx-5 gl-text-gray-200" />
<img :src="slackLogoPath" class="gitlab-slack-logo" />
</div>
<gl-button
category="primary"
variant="success"
class="js-popup-button gl-mt-3"
@click="togglePopup"
>
{{ __('Add GitLab to Slack') }}
</gl-button>
<div
v-if="popupOpen"
class="popup gitlab-slack-popup mx-auto prepend-top-20 text-center js-popup"
>
<div v-if="isSignedIn && hasProjects" class="inline">
<strong>{{ __('Select GitLab project to link with your Slack team') }}</strong>
<select v-model="selectedProjectId" class="js-project-select form-control gl-mt-3 gl-mb-3">
<option v-for="project in projects" :key="project.id" :value="project.id">
{{ project.name }}
</option>
</select>
<gl-button
category="primary"
variant="success"
class="float-right js-add-button"
@click="addToSlack"
>
{{ __('Add to Slack') }}
</gl-button>
</div>
<span v-else-if="isSignedIn && !hasProjects" class="js-no-projects">{{
__("You don't have any projects available.")
}}</span>
<span v-else>
You have to
<a v-once :href="signInPath" class="js-gitlab-slack-sign-in-link">{{ __('log in') }}</a>
</span>
</div>
<div class="center prepend-top-20 gl-mb-3 gl-mr-2 gl-ml-2">
<img v-once :src="gitlabForSlackGifPath" class="gitlab-slack-gif" />
</div>
<div v-once class="text-center">
<h3>{{ __('How it works') }}</h3>
<div class="well gitlab-slack-well mx-auto">
<code class="code mx-auto gl-mb-3"
>/gitlab &lt;project-alias&gt; issue show &lt;id&gt;</code
>
<span>
<gl-icon name="arrow-right" class="gl-mr-2 gl-text-gray-200" />
Shows the issue with id <strong>&lt;id&gt;</strong>
</span>
<div class="gl-mt-3">
<a v-once :href="docsPath">{{ __('More Slack commands') }}</a>
</div>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import AddGitlabSlackApplication from './components/add_gitlab_slack_application.vue';
export default () => {
const el = document.getElementById('js-add-gitlab-slack-application-entry-point');
if (!el) return;
const dataNode = document.getElementById('js-add-gitlab-slack-application-entry-data');
const initialData = JSON.parse(dataNode.innerHTML);
const AddGitlabSlackApplicationComp = Vue.extend(AddGitlabSlackApplication);
new AddGitlabSlackApplicationComp({
propsData: {
projects: initialData.projects,
isSignedIn: initialData.is_signed_in,
gitlabForSlackGifPath: initialData.gitlab_for_slack_gif_path,
signInPath: initialData.sign_in_path,
slackLinkPath: initialData.slack_link_profile_slack_path,
gitlabLogoPath: initialData.gitlab_logo_path,
slackLogoPath: initialData.slack_logo_path,
docsPath: initialData.docs_path,
},
}).$mount(el);
};
import axios from '~/lib/utils/axios_utils';
export default {
addToSlack(url, projectId) {
return axios.get(url, {
params: {
project_id: projectId,
},
});
},
};
import axios from '~/lib/utils/axios_utils';
export const addProjectToSlack = (url, projectId) => {
return axios.get(url, {
params: { project_id: projectId },
});
};
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { addProjectToSlack } from '../api';
import { i18n } from '../constants';
import ProjectsDropdown from './projects_dropdown.vue';
export default {
components: {
GlButton,
GlIcon,
ProjectsDropdown,
},
props: {
projects: {
type: Array,
required: false,
default: () => [],
},
isSignedIn: {
type: Boolean,
required: true,
},
signInPath: {
type: String,
required: true,
},
slackLinkPath: {
type: String,
required: true,
},
gitlabLogoPath: {
type: String,
required: true,
},
slackLogoPath: {
type: String,
required: true,
},
},
i18n,
data() {
return {
selectedProject: null,
};
},
computed: {
hasProjects() {
return this.projects.length > 0;
},
},
methods: {
selectProject(project) {
this.selectedProject = project;
},
addToSlack() {
addProjectToSlack(this.slackLinkPath, this.selectedProject.id)
.then((response) => redirectTo(response.data.add_to_slack_link))
.catch(() =>
createFlash({
message: i18n.slackErrorMessage,
}),
);
},
},
};
</script>
<template>
<div class="gitlab-slack-body gl-mx-auto gl-mt-11 gl-text-center">
<div v-once class="gl-my-5 gl-display-flex gl-justify-content-center gl-align-items-center">
<img :src="gitlabLogoPath" :alt="$options.i18n.gitlabLogoAlt" class="gl-h-11 gl-w-11" />
<gl-icon name="arrow-right" :size="32" class="gl-mx-5 gl-text-gray-200" />
<img
:src="slackLogoPath"
:alt="$options.i18n.slackLogoAlt"
class="gitlab-slack-slack-logo gl-h-11 gl-w-11"
/>
</div>
<h2>{{ $options.i18n.title }}</h2>
<div class="gl-mt-6" data-testid="gitlab-slack-content">
<template v-if="isSignedIn">
<template v-if="hasProjects">
<p>
{{ $options.i18n.dropdownLabel }}
</p>
<projects-dropdown
:projects="projects"
:selected-project="selectedProject"
@project-selected="selectProject"
/>
<div class="gl-display-flex gl-justify-content-end">
<gl-button
category="primary"
variant="confirm"
:disabled="!selectedProject"
@click="addToSlack"
>
{{ $options.i18n.dropdownButtonText }}
</gl-button>
</div>
</template>
<p v-else>{{ $options.i18n.noProjects }}</p>
</template>
<template v-else>
<p>{{ $options.i18n.signInLabel }}</p>
<gl-button category="primary" variant="confirm" :href="signInPath">
{{ $options.i18n.signInButtonText }}
</gl-button>
</template>
</div>
</div>
</template>
<script>
import { GlDropdown } from '@gitlab/ui';
import { __ } from '~/locale';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
export default {
components: {
GlDropdown,
ProjectListItem,
},
props: {
projectDropdownText: {
type: String,
required: false,
default: __('Select a project'),
},
projects: {
type: Array,
required: false,
default: () => [],
},
selectedProject: {
type: Object,
required: false,
default: null,
},
},
computed: {
dropdownText() {
return this.selectedProject
? this.selectedProject.name_with_namespace
: this.projectDropdownText;
},
},
methods: {
onClick(project) {
this.$emit('project-selected', project);
this.$refs.dropdown.hide(true);
},
},
};
</script>
<template>
<gl-dropdown ref="dropdown" block :text="dropdownText" menu-class="gl-w-full!">
<project-list-item
v-for="project in projects"
:key="project.id"
:project="project"
:selected="false"
@click="onClick(project)"
/>
</gl-dropdown>
</template>
import { __, s__ } from '~/locale';
export const i18n = {
slackErrorMessage: __('Unable to build Slack link.'),
gitlabLogoAlt: __('GitLab logo'),
slackLogoAlt: __('Slack logo'),
title: s__('SlackIntegration|GitLab for Slack'),
dropdownLabel: s__('SlackIntegration|Select a GitLab project to link with your Slack workspace.'),
dropdownButtonText: __('Continue'),
noProjects: __("You don't have any projects available."),
signInLabel: s__('JiraService|Sign in to GitLab.com to get started.'),
signInButtonText: __('Sign in to GitLab'),
};
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import GitlabSlackApplication from './components/gitlab_slack_application.vue';
export default () => {
const el = document.querySelector('.js-gitlab-slack-application');
if (!el) return null;
const {
projects,
isSignedIn,
signInPath,
slackLinkPath,
gitlabLogoPath,
slackLogoPath,
} = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(GitlabSlackApplication, {
props: {
projects: JSON.parse(projects),
isSignedIn: parseBoolean(isSignedIn),
signInPath,
slackLinkPath,
gitlabLogoPath,
slackLogoPath,
},
});
},
});
};
import mountAddGitlabSlackApplication from 'ee/add_gitlab_slack_application';
import initGitlabSlackApplication from 'ee/integrations/gitlab_slack_application';
mountAddGitlabSlackApplication();
initGitlabSlackApplication();
......@@ -10,7 +10,7 @@ class Profiles::SlacksController < Profiles::ApplicationController
feature_category :users
def edit
@projects = disabled_projects.select([:id, :name]) if current_user
@projects = disabled_projects.inc_routes if current_user
end
def slack_link
......
......@@ -34,17 +34,15 @@ module EE
"https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_auth_project_settings_slack_url(project)}&state=#{escaped_form_authenticity_token}"
end
def add_to_slack_data(projects)
def gitlab_slack_application_data(projects)
{
projects: projects.as_json(only: [:id, :name]),
projects: (projects || []).to_json(only: [:id, :name], methods: [:avatar_url, :name_with_namespace]),
sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'),
is_signed_in: current_user.present?,
slack_link_profile_slack_path: slack_link_profile_slack_path,
gitlab_for_slack_gif_path: image_path('gitlab_for_slack.gif'),
is_signed_in: current_user.present?.to_s,
slack_link_path: slack_link_profile_slack_path,
gitlab_logo_path: image_path('illustrations/gitlab_logo.svg'),
slack_logo_path: image_path('illustrations/slack_logo.svg'),
docs_path: help_page_path('user/project/integrations/gitlab_slack_application.md')
}.to_json.html_safe
slack_logo_path: image_path('illustrations/slack_logo.svg')
}
end
def escaped_form_authenticity_token
......
-# haml-lint:disable InlineJavaScript
%script#js-add-gitlab-slack-application-entry-data{ type: "application/json" }
= add_to_slack_data(@projects)
- @hide_breadcrumbs = true
- @hide_top_links = true
- @content_class = 'limit-container-width'
- page_title s_('SlackIntegration|GitLab for Slack')
#js-add-gitlab-slack-application-entry-point
.js-gitlab-slack-application{ data: gitlab_slack_application_data(@projects) }
import Vue from 'vue';
import addGitlabSlackApplication from 'ee/add_gitlab_slack_application/components/add_gitlab_slack_application.vue';
import GitlabSlackService from 'ee/add_gitlab_slack_application/services/gitlab_slack_service';
import mountComponent from 'helpers/vue_mount_component_helper';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
describe('AddGitlabSlackApplication', () => {
const redirectLink = '//redirectLink';
const gitlabForSlackGifPath = '//gitlabForSlackGifPath';
const signInPath = '//signInPath';
const slackLinkPath = '//slackLinkPath';
const docsPath = '//docsPath';
const gitlabLogoPath = '//gitlabLogoPath';
const slackLogoPath = '//slackLogoPath';
const projects = [
{
id: 4,
name: 'test',
},
{
id: 6,
name: 'nope',
},
];
const DEFAULT_PROPS = {
projects,
gitlabForSlackGifPath,
signInPath,
slackLinkPath,
docsPath,
gitlabLogoPath,
slackLogoPath,
isSignedIn: false,
};
const AddGitlabSlackApplication = Vue.extend(addGitlabSlackApplication);
it('opens popup when button is clicked', () => {
const vm = mountComponent(AddGitlabSlackApplication, DEFAULT_PROPS);
vm.$el.querySelector('.js-popup-button').click();
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-popup')).toBeDefined();
});
});
it('hides popup when button is clicked', () => {
const vm = mountComponent(AddGitlabSlackApplication, DEFAULT_PROPS);
vm.popupOpen = true;
return vm
.$nextTick()
.then(() => vm.$el.querySelector('.js-popup-button').click())
.then(vm.$nextTick)
.then(() => {
expect(vm.$el.querySelector('.js-popup')).toBeNull();
});
});
it('popup has a project select when signed in', () => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
isSignedIn: true,
});
vm.popupOpen = true;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-project-select')).toBeDefined();
});
});
it('popup has a message when there is no projects', () => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
projects: [],
isSignedIn: true,
});
vm.popupOpen = true;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-no-projects').textContent).toMatch(
"You don't have any projects available.",
);
});
});
it('popup has a sign in link when logged out', () => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
});
vm.popupOpen = true;
vm.selectedProjectId = 4;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.js-gitlab-slack-sign-in-link').href).toMatch(
new RegExp(signInPath, 'i'),
);
});
});
it('redirects user to external link when submitted', () => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
isSignedIn: true,
});
const addToSlackPromise = Promise.resolve({ data: { add_to_slack_link: redirectLink } });
jest.spyOn(GitlabSlackService, 'addToSlack').mockReturnValue(addToSlackPromise);
vm.popupOpen = true;
return vm
.$nextTick()
.then(() => vm.$el.querySelector('.js-add-button').click())
.then(vm.$nextTick)
.then(addToSlackPromise)
.then(() => {
expect(redirectTo).toHaveBeenCalledWith(redirectLink);
});
});
});
import { GlButton } from '@gitlab/ui';
import GitlabSlackApplication from 'ee/integrations/gitlab_slack_application/components/gitlab_slack_application.vue';
import { addProjectToSlack } from 'ee/integrations/gitlab_slack_application/api';
import { i18n } from 'ee/integrations/gitlab_slack_application/constants';
import ProjectsDropdown from 'ee/integrations/gitlab_slack_application/components/projects_dropdown.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { redirectTo } from '~/lib/utils/url_utility';
import { mockProjects } from '../mock_data';
jest.mock('ee/integrations/gitlab_slack_application/api');
jest.mock('~/lib/utils/url_utility');
describe('GitlabSlackApplication', () => {
let wrapper;
const defaultProps = {
projects: [],
gitlabForSlackGifPath: '//gitlabForSlackGifPath',
signInPath: '//signInPath',
slackLinkPath: '//slackLinkPath',
docsPath: '//docsPath',
gitlabLogoPath: '//gitlabLogoPath',
slackLogoPath: '//slackLogoPath',
isSignedIn: true,
};
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(GitlabSlackApplication, {
propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlButton = () => wrapper.findComponent(GlButton);
const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdown);
const findAppContent = () => wrapper.findByTestId('gitlab-slack-content');
describe('template', () => {
describe('when user is not signed in', () => {
it('renders "Sign in" button', () => {
createComponent({
props: { isSignedIn: false },
});
expect(findGlButton().attributes('href')).toBe(defaultProps.signInPath);
});
});
describe('when user is signed in', () => {
describe('user does not have any projects', () => {
it('renders empty text', () => {
createComponent();
expect(findAppContent().text()).toBe(i18n.noProjects);
});
});
describe('user has projects', () => {
beforeEach(() => {
createComponent({
props: {
projects: mockProjects,
},
});
});
it('renders ProjectsDropdown', () => {
expect(findProjectsDropdown().props('projects')).toBe(mockProjects);
});
it('redirects to slackLinkPath when submitted', async () => {
const redirectLink = '//redirectLink';
const mockProject = mockProjects[1];
const addToSlackData = { data: { add_to_slack_link: redirectLink } };
addProjectToSlack.mockResolvedValue(addToSlackData);
findProjectsDropdown().vm.$emit('project-selected', mockProject);
await wrapper.vm.$nextTick();
expect(findProjectsDropdown().props('selectedProject')).toBe(mockProject);
expect(findGlButton().props('disabled')).toBe(false);
findGlButton().vm.$emit('click');
await waitForPromises();
expect(redirectTo).toHaveBeenCalledWith(redirectLink);
});
});
});
});
});
export const mockProjects = [
{
id: 1,
name: 'Test',
avatar_url: 'avatar.jpg',
name_with_namespace: 'Test org / Test',
},
{
id: 2,
name: 'Shell',
avatar_url: 'avatar.jpg',
name_with_namespace: 'Test org / Shell',
},
];
......@@ -108,11 +108,11 @@ RSpec.describe EE::IntegrationsHelper do
end
end
describe '#add_to_slack_data' do
describe '#gitlab_slack_application_data' do
let_it_be(:projects) { create_list(:project, 3) }
def relation
Project.id_in(projects.pluck(:id))
Project.id_in(projects.pluck(:id)).inc_routes
end
let(:request) do
......@@ -132,29 +132,27 @@ RSpec.describe EE::IntegrationsHelper do
end
it 'includes the required keys' do
additions = Gitlab::Json.parse(subject.add_to_slack_data(relation))
expect(additions.keys).to match_array %w[
projects
sign_in_path
is_signed_in
slack_link_profile_slack_path
gitlab_for_slack_gif_path
gitlab_logo_path
slack_logo_path
docs_path
]
additions = subject.gitlab_slack_application_data(relation)
expect(additions.keys).to include(
:projects,
:sign_in_path,
:is_signed_in,
:slack_link_path,
:gitlab_logo_path,
:slack_logo_path
)
end
it 'does not suffer from N+1 performance issues' do
baseline = ActiveRecord::QueryRecorder.new { subject.add_to_slack_data(relation.limit(1)) }
baseline = ActiveRecord::QueryRecorder.new { subject.gitlab_slack_application_data(relation.limit(1)) }
expect do
subject.add_to_slack_data(relation)
subject.gitlab_slack_application_data(relation)
end.not_to exceed_query_limit(baseline)
end
it 'serializes nil projects without error' do
expect(subject.add_to_slack_data(nil)).to include('"projects":null')
expect(subject.gitlab_slack_application_data(nil)).to include(projects: '[]')
end
end
end
......@@ -1982,9 +1982,6 @@ msgstr ""
msgid "Add CONTRIBUTING"
msgstr ""
msgid "Add GitLab to Slack"
msgstr ""
msgid "Add Jaeger URL"
msgstr ""
......@@ -2198,9 +2195,6 @@ msgstr ""
msgid "Add text to the sign-in page. Markdown enabled."
msgstr ""
msgid "Add to Slack"
msgstr ""
msgid "Add to board"
msgstr ""
......@@ -16166,9 +16160,6 @@ msgstr ""
msgid "GitLab export"
msgstr ""
msgid "GitLab for Slack"
msgstr ""
msgid "GitLab group: %{source_link}"
msgstr ""
......@@ -16193,6 +16184,9 @@ msgstr ""
msgid "GitLab is undergoing maintenance and is operating in read-only mode."
msgstr ""
msgid "GitLab logo"
msgstr ""
msgid "GitLab member or Email address"
msgstr ""
......@@ -17724,9 +17718,6 @@ msgstr ""
msgid "How do I use file templates?"
msgstr ""
msgid "How it works"
msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr ""
......@@ -23140,9 +23131,6 @@ msgstr ""
msgid "More Information"
msgstr ""
msgid "More Slack commands"
msgstr ""
msgid "More actions"
msgstr ""
......@@ -32065,9 +32053,6 @@ msgstr ""
msgid "Select Git revision"
msgstr ""
msgid "Select GitLab project to link with your Slack team"
msgstr ""
msgid "Select Page"
msgstr ""
......@@ -33118,6 +33103,15 @@ msgstr ""
msgid "Slack integration allows you to interact with GitLab via slash commands in a chat window."
msgstr ""
msgid "Slack logo"
msgstr ""
msgid "SlackIntegration|GitLab for Slack"
msgstr ""
msgid "SlackIntegration|Select a GitLab project to link with your Slack workspace."
msgstr ""
msgid "SlackIntegration|Sends notifications about project events to Slack channels."
msgstr ""
......@@ -37464,9 +37458,6 @@ msgstr ""
msgid "Track time with quick actions"
msgstr ""
msgid "Track your GitLab projects with GitLab for Slack."
msgstr ""
msgid "Training mode"
msgstr ""
......@@ -42391,9 +42382,6 @@ msgstr ""
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
msgid "log in"
msgstr ""
msgid "manual"
msgstr ""
......
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