Commit 9a442498 authored by Valery Sizov's avatar Valery Sizov Committed by Jacob Schatz

Slack application landing page

parent aaa8d8cf
{"iconCount":175,"spriteSize":76745,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file
{"iconCount":174,"spriteSize":76324,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<script>
import Flash from '~/flash';
import GitlabSlackService from '../services/gitlab_slack_service';
import * as UrlUtility from '../../lib/utils/url_utility';
export default {
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: {
doubleHeadedArrowSvg() {
return gl.utils.spriteIcon('double-headed-arrow');
},
arrowRightSvg() {
return gl.utils.spriteIcon('arrow-right');
},
hasProjects() {
return this.projects.length > 0;
},
},
methods: {
togglePopup() {
this.popupOpen = !this.popupOpen;
},
addToSlack() {
GitlabSlackService.addToSlack(this.slackLinkPath, this.selectedProjectId)
.then(response => UrlUtility.redirectTo(response.data.add_to_slack_link))
.catch(() => Flash('Unable to build Slack link.'));
},
},
mounted() {
GitlabSlackService.init();
},
};
</script>
<template>
<div>
<div class="center append-right-default">
<h1>GitLab for Slack</h1>
<p>Track your GitLab projects with GitLab for Slack.</p>
</div>
<div class="append-bottom-20 center" v-once>
<img
class="gitlab-slack-logo"
:src="gitlabLogoPath"></img>
<div
class="gitlab-slack-double-headed-arrow inline prepend-left-20 append-right-20"
v-html="doubleHeadedArrowSvg"></div>
<img
class="gitlab-slack-logo"
:src="slackLogoPath"></img>
</div>
<button
type="button"
class="btn btn-red center-block js-popup-button"
@click="togglePopup">
Add GitLab to Slack
</button>
<div
class="popup gitlab-slack-popup center-block prepend-top-20 text-center js-popup"
v-if="popupOpen">
<div
class="inline"
v-if="isSignedIn && hasProjects">
<strong>Select GitLab project to link with your Slack team</strong>
<select
class="gitlab-slack-project-select js-project-select form-control prepend-top-10 append-bottom-10"
v-model="selectedProjectId">
<option
v-for="project in projects"
:key="project.id"
:value="project.id">
{{ project.name }}
</option>
</select>
<button
type="button"
class="btn btn-red pull-right js-add-button"
@click="addToSlack">
Add to Slack
</button>
</div>
<span
class="js-no-projects"
v-else-if="isSignedIn && !hasProjects">
You don't have any projects available.
</span>
<span v-else>
You have to
<a
class="js-gitlab-slack-sign-in-link"
v-once
:href="signInPath">
log in
</a>
</span>
</div>
<div class="center prepend-top-20 append-bottom-10 append-right-5 prepend-left-5">
<img
v-once
class="gitlab-slack-gif"
:src="gitlabForSlackGifPath">
</div>
<div
class="gitlab-slack-example"
v-once>
<h3 class="center">How it works</h3>
<div class="well gitlab-slack-well center-block">
<code class="code center-block append-bottom-10">/project-name issue show &lt;id&gt;</code>
<span>
<div
class="gitlab-slack-right-arrow inline append-right-5"
v-html="arrowRightSvg"></div>
Shows the issue with id
<strong>&lt;id&gt;</strong>
</span>
</div>
<div class="center">
<a :href="docsPath">
More Slack commands
</a>
</div>
</div>
</div>
</template>
import Vue from 'vue';
import AddGitlabSlackApplication from './components/add_gitlab_slack_application.vue';
function mountAddGitlabSlackApplication() {
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);
}
document.addEventListener('DOMContentLoaded', mountAddGitlabSlackApplication);
export default mountAddGitlabSlackApplication;
import axios from 'axios';
import setAxiosCsrfToken from '../../lib/utils/axios_utils';
export default {
init() {
setAxiosCsrfToken();
},
addToSlack(url, projectId) {
return axios.get(url, {
params: {
project_id: projectId,
},
});
},
};
......@@ -100,6 +100,10 @@ export function visitUrl(url, external = false) {
}
}
export function redirectTo(url) {
return window.location.assign(url);
}
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
......
......@@ -33,6 +33,7 @@
@import "framework/modal";
@import "framework/pagination";
@import "framework/panels";
@import "framework/popup";
@import "framework/secondary-navigation-elements";
@import "framework/selects";
@import "framework/sidebar";
......
......@@ -216,3 +216,31 @@
display: none;
}
}
@mixin triangle($color, $border-color, $size, $border-size) {
&::before,
&::after {
bottom: 100%;
left: 50%;
border: solid transparent;
content: '';
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
&::before {
border-color: transparent;
border-bottom-color: $border-color;
border-width: ($size + $border-size);
margin-left: -($size + $border-size);
}
&::after {
border-color: transparent;
border-bottom-color: $color;
border-width: $size;
margin-left: -$size;
}
}
.popup {
@include triangle(
$gray-lighter,
$gray-darker,
$popup-triangle-size,
$popup-triangle-border-size
);
padding: $gl-padding;
background-color: $gray-lighter;
border: 1px solid $gray-darker;
border-radius: $border-radius-default;
box-shadow: 0 5px 8px $popup-box-shadow-color;
position: relative;
}
......@@ -741,3 +741,21 @@ Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
$image-comment-cursor-top-offset: 30;
/*
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;
$double-headed-arrow-width: 100px;
$double-headed-arrow-height: 25px;
$right-arrow-size: 16px;
/*
Popup
*/
$popup-triangle-size: 15px;
$popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
......@@ -420,3 +420,41 @@ table.u2f-registrations {
}
}
}
.gitlab-slack-gif {
width: 100%;
max-width: $add-to-slack-gif-max-width;
}
.gitlab-slack-well {
background-color: $white-light;
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-right-arrow svg {
fill: $white-dark;
width: $right-arrow-size;
height: $right-arrow-size;
vertical-align: text-bottom;
}
.gitlab-slack-double-headed-arrow {
vertical-align: text-top;
svg {
fill: $gray-darker;
width: $double-headed-arrow-width;
height: $double-headed-arrow-height;
}
}
module ServicesHelper
prepend EE::ServicesHelper
def service_event_description(event)
case event
when "push", "push_events"
......
= webpack_bundle_tag 'add_gitlab_slack_application'
%script#js-add-gitlab-slack-application-entry-data{ type: "application/json" }
= add_to_slack_data(@projects)
#js-add-gitlab-slack-application-entry-point
%a{ href: "https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_redirect_uri(@project)}" }
%a#slack-button{ href: add_to_slack_link(project, slack_app_id) }
%img{ alt:"Add to Slack", height: "40", src: "https://platform.slack-edge.com/img/add_to_slack.png", srcset: "https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x", width: "139" }
......@@ -27,4 +27,4 @@
= link_to 'Remove', project_settings_slack_path(project), method: :delete, class: 'btn btn-danger btn-sm', data: { confirm: 'Are you sure?' }
- else
%p To set up this service press "Add to Slack"
= render "projects/services/#{@service.to_param}/slack_button"
= render "projects/services/#{@service.to_param}/slack_button", project: @project
......@@ -459,9 +459,9 @@
:versions: []
:when: 2017-09-13 17:31:16.425819400 Z
- - :approve
- gitlab-svgs
- "@gitlab-org/gitlab-svgs"
- :who: Tim Zallmann
:why: Our own library - https://gitlab.com/gitlab-org/gitlab-svgs
:why: Our own library - GitLab License https://gitlab.com/gitlab-org/gitlab-svgs
:versions: []
:when: 2017-09-19 14:36:32.795496000 Z
- - :license
......
......@@ -35,6 +35,14 @@ resource :profile, only: [:show, :update] do
put :resend_confirmation_instructions
end
end
## EE-specific
resource :slack, only: [:edit] do
member do
get :slack_link
end
end
resources :chat_names, only: [:index, :new, :create, :destroy] do
collection do
delete :deny
......
......@@ -2,8 +2,9 @@
const path = require('path');
const fs = require('fs');
const sourcePath = path.join('node_modules', 'gitlab-svgs', 'dist');
const sourcePathIllustrations = path.join('node_modules', 'gitlab-svgs', 'dist', 'illustrations');
const svgsPackageName = '@gitlab-org/gitlab-svgs';
const sourcePath = path.join('node_modules', svgsPackageName, 'dist');
const sourcePathIllustrations = path.join('node_modules', svgsPackageName, 'dist', 'illustrations');
const destPath = path.normalize(path.join('app', 'assets', 'images'));
// Actual Task copying the 2 files + all illustrations
......
......@@ -27,6 +27,7 @@ var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: {
account: './profile/account/index.js',
add_gitlab_slack_application: './add_gitlab_slack_application/index.js',
balsamiq_viewer: './blob/balsamiq_viewer.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
......
class Profiles::SlacksController < Profiles::ApplicationController
include ServicesHelper
skip_before_action :authenticate_user!
layout 'application'
def edit
@projects = disabled_projects if current_user
end
def slack_link
project = disabled_projects.find(params[:project_id])
link = add_to_slack_link(project, current_application_settings.slack_app_id)
render json: { add_to_slack_link: link }
end
private
def disabled_projects
@disabled_projects ||= current_user
.authorized_projects(Gitlab::Access::MASTER)
.with_slack_application_disabled
end
end
module EE
module ServicesHelper
def add_to_slack_link(project, slack_app_id)
"https://slack.com/oauth/authorize?scope=commands&client_id=#{slack_app_id}&redirect_uri=#{slack_auth_project_settings_slack_url(project)}"
end
def add_to_slack_data(projects)
{
projects: projects,
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'),
gitlab_logo_path: image_path('illustrations/gitlab_logo.svg'),
slack_logo_path: image_path('illustrations/slack_logo.svg'),
docs_path: help_page_path('integration/slash_commands.md')
}.to_json.html_safe
end
end
end
......@@ -75,6 +75,11 @@ module EE
def search_by_visibility(level)
where(visibility_level: ::Gitlab::VisibilityLevel.string_options[level])
end
def with_slack_application_disabled
joins('LEFT JOIN services ON services.project_id = projects.id AND services.type = \'GitlabSlackApplicationService\' AND services.active IS true')
.where('services.id IS NULL')
end
end
def mirror
......
require 'spec_helper'
describe Profiles::SlacksController do
let(:user) { create(:user) }
before do
sign_in(user)
allow(subject).to receive(:current_user).and_return(user)
end
describe 'GET edit' do
before do
get :edit
end
it 'renders' do
expect(response).to render_template :edit
end
it 'assigns projects' do
expect(assigns[:projects]).to eq []
end
it 'assigns disabled_projects' do
expect(assigns[:disabled_projects]).to eq []
end
end
end
......@@ -1087,4 +1087,17 @@ describe Project do
expect(project.import_data.password).to eq('pass')
end
end
describe '#with_slack_application_disabled' do
it 'returns projects where Slack application is disabled' do
project1 = create(:project)
project2 = create(:project)
create(:gitlab_slack_application_service, project: project2)
projects = described_class.with_slack_application_disabled
expect(projects).to include(project1)
expect(projects).not_to include(project2)
end
end
end
......@@ -52,6 +52,7 @@ FactoryGirl.define do
factory :gitlab_slack_application_service do
project
active true
type 'GitlabSlackApplicationService'
end
end
import Vue from 'vue';
import addGitlabSlackApplication from '~/add_gitlab_slack_application/components/add_gitlab_slack_application.vue';
import GitlabSlackService from '~/add_gitlab_slack_application/services/gitlab_slack_service';
import * as UrlUtility from '~/lib/utils/url_utility';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('AddGitlabSlackApplication', () => {
const redirectLink = '//redirectLink';
const gitlabForSlackGifPath = '//gitlabForSlackGifPath';
const signInPath = '//signInPath';
const slackLinkPath = '//slackLinkPath';
const docsPath = '//s';
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', (done) => {
const vm = mountComponent(AddGitlabSlackApplication, DEFAULT_PROPS);
vm.$el.querySelector('.js-popup-button').click();
vm.$nextTick()
.then(() => expect(vm.$el.querySelector('.js-popup')).toBeDefined())
.then(done)
.catch(done.fail);
});
it('hides popup when button is clicked', (done) => {
const vm = mountComponent(AddGitlabSlackApplication, DEFAULT_PROPS);
vm.popupOpen = true;
vm.$nextTick()
.then(() => vm.$el.querySelector('.js-popup-button').click())
.then(vm.$nextTick)
.then(() => expect(vm.$el.querySelector('.js-popup')).toBeNull())
.then(done)
.catch(done.fail);
});
it('popup has a project select when signed in', (done) => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
isSignedIn: true,
});
vm.popupOpen = true;
vm.$nextTick()
.then(() => expect(vm.$el.querySelector('.js-project-select')).toBeDefined())
.then(done)
.catch(done.fail);
});
it('popup has a message when there is no projects', (done) => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
projects: [],
isSignedIn: true,
});
vm.popupOpen = true;
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-no-projects').textContent)
.toMatch("You don't have any projects available.");
})
.then(done)
.catch(done.fail);
});
it('popup has a sign in link when logged out', (done) => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
});
vm.popupOpen = true;
vm.selectedProjectId = 4;
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.js-gitlab-slack-sign-in-link').href)
.toMatch(new RegExp(signInPath, 'i'));
})
.then(done)
.catch(done.fail);
});
it('redirects user to external link when submitted', (done) => {
const vm = mountComponent(AddGitlabSlackApplication, {
...DEFAULT_PROPS,
isSignedIn: true,
});
const addToSlackPromise = Promise.resolve({ data: { add_to_slack_link: redirectLink } });
spyOn(GitlabSlackService, 'addToSlack').and.returnValue(addToSlackPromise);
spyOn(UrlUtility, 'redirectTo');
vm.popupOpen = true;
vm.$nextTick()
.then(() => vm.$el.querySelector('.js-add-button').click())
.then(vm.$nextTick)
.then(addToSlackPromise)
.then(() => expect(UrlUtility.redirectTo).toHaveBeenCalledWith(redirectLink))
.then(done)
.catch(done.fail);
});
});
......@@ -2,6 +2,10 @@
# yarn lockfile v1
"@gitlab-org/gitlab-svgs@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.0.2.tgz#e4d29058e2bb438ba71ac525c6397ef15ae2877b"
abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
......@@ -2720,10 +2724,6 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
"gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git":
version "1.1.0"
resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#f73ef3cb04d9bde261c2552c679c431cc44928f2"
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
......
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