diff options
author | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-12-20 20:07:59 +0100 |
---|---|---|
committer | Kamil Trzcinski <ayufan@ayufan.eu> | 2016-12-20 20:07:59 +0100 |
commit | 3c61b13efeb52c95d13fbb75fd3016555095276b (patch) | |
tree | bc4ac0b4c818e27a4bdea376f249c0950352b6b6 /app | |
parent | dec1e90e505d9ab9e8b088b6a348f5bec293fed1 (diff) | |
parent | 2bc3084d68ac64fcc31276f4ec5e76f79d6fa296 (diff) | |
download | gitlab-ce-3c61b13efeb52c95d13fbb75fd3016555095276b.tar.gz |
Merge remote-tracking branch 'origin/master' into zj-mattermost-slash-config
Diffstat (limited to 'app')
43 files changed, 842 insertions, 198 deletions
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 88c3d257cea..facd653fd72 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -18,7 +18,7 @@ * The environments array is a recursive tree structure and we need to filter * both root level environments and children environments. * - * In order to acomplish that, both `filterState` and `filterEnvironmnetsByState` + * In order to acomplish that, both `filterState` and `filterEnvironmentsByState` * functions work together. * The first one works as the filter that verifies if the given environment matches * the given state. @@ -34,9 +34,9 @@ * @param {Array} array * @return {Array} */ - const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => { + const filterEnvironmentsByState = (fn, arr) => arr.map((item) => { if (item.children) { - const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean); + const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean); if (filteredChildren.length) { item.children = filteredChildren; return item; @@ -76,12 +76,13 @@ helpPagePath: environmentsData.helpPagePath, commitIconSvg: environmentsData.commitIconSvg, playIconSvg: environmentsData.playIconSvg, + terminalIconSvg: environmentsData.terminalIconSvg, }; }, computed: { filteredEnvironments() { - return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments); + return filterEnvironmentsByState(filterState(this.visibility), this.state.environments); }, scope() { @@ -102,7 +103,7 @@ }, /** - * Fetches all the environmnets and stores them. + * Fetches all the environments and stores them. * Toggles loading property. */ created() { @@ -230,6 +231,7 @@ :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" :commit-icon-svg="commitIconSvg"></tr> <tr v-if="model.isOpen && model.children && model.children.length > 0" @@ -240,6 +242,7 @@ :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" :play-icon-svg="playIconSvg" + :terminal-icon-svg="terminalIconSvg" :commit-icon-svg="commitIconSvg"> </tr> diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 4674d5202e6..b26a40aa268 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -8,6 +8,7 @@ /*= require ./environment_external_url */ /*= require ./environment_stop */ /*= require ./environment_rollback */ +/*= require ./environment_terminal_button */ (() => { /** @@ -33,6 +34,7 @@ 'external-url-component': window.gl.environmentsList.ExternalUrlComponent, 'stop-component': window.gl.environmentsList.StopComponent, 'rollback-component': window.gl.environmentsList.RollbackComponent, + 'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent, }, props: { @@ -68,6 +70,12 @@ type: String, required: false, }, + + terminalIconSvg: { + type: String, + required: false, + }, + }, data() { @@ -506,6 +514,14 @@ </stop-component> </div> + <div v-if="model.terminal_path" + class="inline js-terminal-button-container"> + <terminal-button-component + :terminal-icon-svg="terminalIconSvg" + :terminal-path="model.terminal_path"> + </terminal-button-component> + </div> + <div v-if="canRetry && canCreateDeployment" class="inline js-rollback-component-container"> <rollback-component diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 new file mode 100644 index 00000000000..25e6ac7f3c9 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -0,0 +1,27 @@ +/*= require vue */ +/* global Vue */ + +(() => { + window.gl = window.gl || {}; + window.gl.environmentsList = window.gl.environmentsList || {}; + + window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { + props: { + terminalPath: { + type: String, + default: '', + }, + terminalIconSvg: { + type: String, + default: '', + }, + }, + + template: ` + <a class="btn terminal-button" + :href="terminalPath"> + <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> + </a> + `, + }); +})(); diff --git a/app/assets/javascripts/terminal/terminal.js.es6 b/app/assets/javascripts/terminal/terminal.js.es6 new file mode 100644 index 00000000000..6b9422b1816 --- /dev/null +++ b/app/assets/javascripts/terminal/terminal.js.es6 @@ -0,0 +1,62 @@ +/* global Terminal */ + +(() => { + class GLTerminal { + + constructor(options) { + this.options = options || {}; + + this.options.cursorBlink = options.cursorBlink || true; + this.options.screenKeys = options.screenKeys || true; + this.container = document.querySelector(options.selector); + + this.setSocketUrl(); + this.createTerminal(); + $(window).off('resize.terminal').on('resize.terminal', () => { + this.terminal.fit(); + }); + } + + setSocketUrl() { + const { protocol, hostname, port } = window.location; + const wsProtocol = protocol === 'https:' ? 'wss://' : 'ws://'; + const path = this.container.dataset.projectPath; + + this.socketUrl = `${wsProtocol}${hostname}:${port}${path}`; + } + + createTerminal() { + this.terminal = new Terminal(this.options); + this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']); + this.socket.binaryType = 'arraybuffer'; + + this.terminal.open(this.container); + this.socket.onopen = () => { this.runTerminal(); }; + this.socket.onerror = () => { this.handleSocketFailure(); }; + } + + runTerminal() { + const decoder = new TextDecoder('utf-8'); + const encoder = new TextEncoder('utf-8'); + + this.terminal.on('data', (data) => { + this.socket.send(encoder.encode(data)); + }); + + this.socket.addEventListener('message', (ev) => { + this.terminal.write(decoder.decode(ev.data)); + }); + + this.isTerminalInitialized = true; + this.terminal.fit(); + } + + handleSocketFailure() { + this.terminal.write('\r\nConnection failure'); + } + + } + + window.gl = window.gl || {}; + gl.Terminal = GLTerminal; +})(); diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 new file mode 100644 index 00000000000..ded7ee6e9fe --- /dev/null +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -0,0 +1,5 @@ +//= require xterm/xterm.js +//= require xterm/fit.js +//= require ./terminal.js + +$(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 000e591e09c..48827578d94 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -64,7 +64,7 @@ &.s32 { font-size: 20px; line-height: 30px; } &.s40 { font-size: 16px; line-height: 38px; } &.s60 { font-size: 32px; line-height: 58px; } - &.s70 { font-size: 34px; line-height: 68px; } + &.s70 { font-size: 34px; line-height: 70px; } &.s90 { font-size: 36px; line-height: 88px; } &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; } &.s140 { font-size: 72px; line-height: 138px; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 59ff17ad2c1..a11f1cd7735 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -230,6 +230,13 @@ } } +.btn-terminal { + svg { + height: 14px; + width: 18px; + } +} + .btn-lg { padding: 12px 20px; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 243c9153ded..c9d54b4f3d3 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -726,3 +726,23 @@ padding: 5px 5px 5px 7px; } } + +.terminal-icon { + margin-left: 3px; +} + +.terminal-container { + .content-block { + border-bottom: none; + } + + #terminal { + margin-top: 10px; + min-height: 450px; + box-sizing: border-box; + + > div { + min-height: 450px; + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index f1493a60b01..d6aa4c4c032 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -93,7 +93,6 @@ .group-avatar { float: none; margin: 0 auto; - border: none; &.identicon { border-radius: 50%; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4df80195ae1..bb47e2a8bf7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -25,7 +25,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :can?, :current_application_settings - helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -245,6 +245,10 @@ class ApplicationController < ActionController::Base current_application_settings.import_sources.include?('github') end + def gitea_import_enabled? + current_application_settings.import_sources.include?('gitea') + end + def github_import_configured? Gitlab::OAuth::Provider.enabled?(:github) end diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb new file mode 100644 index 00000000000..fbd851c64a7 --- /dev/null +++ b/app/controllers/import/gitea_controller.rb @@ -0,0 +1,45 @@ +class Import::GiteaController < Import::GithubController + def new + if session[access_token_key].present? && session[host_key].present? + redirect_to status_import_url + end + end + + def personal_access_token + session[host_key] = params[host_key] + super + end + + def status + @gitea_host_url = session[host_key] + super + end + + private + + def host_key + :"#{provider}_host_url" + end + + # Overriden methods + def provider + :gitea + end + + # Gitea is not yet an OAuth provider + # See https://github.com/go-gitea/gitea/issues/27 + def logged_in_with_provider? + false + end + + def provider_auth + if session[access_token_key].blank? || session[host_key].blank? + redirect_to new_import_gitea_url, + alert: 'You need to specify both an Access Token and a Host URL.' + end + end + + def client_options + { host: session[host_key], api_version: 'v1' } + end +end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index ee7d498c59c..53a5981e564 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,39 +1,37 @@ class Import::GithubController < Import::BaseController - before_action :verify_github_import_enabled - before_action :github_auth, only: [:status, :jobs, :create] + before_action :verify_import_enabled + before_action :provider_auth, only: [:status, :jobs, :create] - rescue_from Octokit::Unauthorized, with: :github_unauthorized - - helper_method :logged_in_with_github? + rescue_from Octokit::Unauthorized, with: :provider_unauthorized def new - if logged_in_with_github? - go_to_github_for_permissions - elsif session[:github_access_token] - redirect_to status_import_github_url + if logged_in_with_provider? + go_to_provider_for_permissions + elsif session[access_token_key] + redirect_to status_import_url end end def callback - session[:github_access_token] = client.get_token(params[:code]) - redirect_to status_import_github_url + session[access_token_key] = client.get_token(params[:code]) + redirect_to status_import_url end def personal_access_token - session[:github_access_token] = params[:personal_access_token] - redirect_to status_import_github_url + session[access_token_key] = params[:personal_access_token] + redirect_to status_import_url end def status @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: "github") + @already_added_projects = current_user.created_projects.where(import_type: provider) already_added_projects_names = @already_added_projects.pluck(:import_source) - @repos.reject!{ |repo| already_added_projects_names.include? repo.full_name } + @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end def jobs - jobs = current_user.created_projects.where(import_type: "github").to_json(only: [:id, :import_status]) + jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status]) render json: jobs end @@ -44,8 +42,8 @@ class Import::GithubController < Import::BaseController namespace_path = params[:target_namespace].presence || current_user.namespace_path @target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - if current_user.can?(:create_projects, @target_namespace) - @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params).execute + if can?(current_user, :create_projects, @target_namespace) + @project = Gitlab::GithubImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, access_params, type: provider).execute else render 'unauthorized' end @@ -54,34 +52,63 @@ class Import::GithubController < Import::BaseController private def client - @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token]) + @client ||= Gitlab::GithubImport::Client.new(session[access_token_key], client_options) end - def verify_github_import_enabled - render_404 unless github_import_enabled? + def verify_import_enabled + render_404 unless import_enabled? end - def github_auth - if session[:github_access_token].blank? - go_to_github_for_permissions - end + def go_to_provider_for_permissions + redirect_to client.authorize_url(callback_import_url) end - def go_to_github_for_permissions - redirect_to client.authorize_url(callback_import_github_url) + def import_enabled? + __send__("#{provider}_import_enabled?") end - def github_unauthorized - session[:github_access_token] = nil - redirect_to new_import_github_url, - alert: 'Access denied to your GitHub account.' + def new_import_url + public_send("new_import_#{provider}_url") end - def logged_in_with_github? - current_user.identities.exists?(provider: 'github') + def status_import_url + public_send("status_import_#{provider}_url") + end + + def callback_import_url + public_send("callback_import_#{provider}_url") + end + + def provider_unauthorized + session[access_token_key] = nil + redirect_to new_import_url, + alert: "Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account." + end + + def access_token_key + :"#{provider}_access_token" end def access_params - { github_access_token: session[:github_access_token] } + { github_access_token: session[access_token_key] } + end + + # The following methods are overriden in subclasses + def provider + :github + end + + def logged_in_with_provider? + current_user.identities.exists?(provider: provider) + end + + def provider_auth + if session[access_token_key].blank? + go_to_provider_for_permissions + end + end + + def client_options + {} end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 6bd4cb3f2f5..87cc36253f1 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -4,17 +4,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_deployment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] - before_action :environment, only: [:show, :edit, :update, :stop] + before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] + before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize] + before_action :verify_api_request!, only: :terminal_websocket_authorize def index @scope = params[:scope] @environments = project.environments - + respond_to do |format| format.html format.json do render json: EnvironmentSerializer - .new(project: @project) + .new(project: @project, user: current_user) .represent(@environments) end end @@ -56,8 +58,33 @@ class Projects::EnvironmentsController < Projects::ApplicationController redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action]) end + def terminal + # Currently, this acts as a hint to load the terminal details into the cache + # if they aren't there already. In the future, users will need these details + # to choose between terminals to connect to. + @terminals = environment.terminals + end + + # GET .../terminal.ws : implemented in gitlab-workhorse + def terminal_websocket_authorize + # Just return the first terminal for now. If the list is in the process of + # being looked up, this may result in a 404 response, so the frontend + # should retry those errors + terminal = environment.terminals.try(:first) + if terminal + set_workhorse_internal_api_content_type + render json: Gitlab::Workhorse.terminal_websocket(terminal) + else + render text: 'Not found', status: 404 + end + end + private + def verify_api_request! + Gitlab::Workhorse.verify_api_request!(request.headers) + end + def environment_params params.require(:environment).permit(:name, :external_url) end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 021d2b14718..a0642a1894b 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -4,8 +4,10 @@ module ImportHelper "#{namespace}/#{name}" end - def github_project_link(path_with_namespace) - link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank' + def provider_project_link(provider, path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) + + link_to path_with_namespace, url, target: '_blank' end private @@ -20,4 +22,8 @@ module ImportHelper provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' } @github_url = provider.fetch('url', 'https://github.com') if provider end + + def gitea_project_url(path_with_namespace) + "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}" + end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 4359f1d7b06..8f02c226f0b 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -1,10 +1,15 @@ module Milestoneish def closed_items_count(user) - issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size + memoize_per_user(user, :closed_items_count) do + (count_issues_by_state(user)['closed'] || 0) + merge_requests.closed_and_merged.size + end end def total_items_count(user) - issues_visible_to_user(user).size + merge_requests.size + memoize_per_user(user, :total_items_count) do + issues_count = count_issues_by_state(user).values.sum + issues_count + merge_requests.size + end end def complete?(user) @@ -30,7 +35,10 @@ module Milestoneish end def issues_visible_to_user(user) - IssuesFinder.new(user).execute.where(id: issues) + memoize_per_user(user, :issues_visible_to_user) do + params = try(:project_id) ? { project_id: project_id } : {} + IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids) + end end def upcoming? @@ -50,4 +58,18 @@ module Milestoneish def expired? due_date && due_date.past? end + + private + + def count_issues_by_state(user) + memoize_per_user(user, :count_issues_by_state) do + issues_visible_to_user(user).reorder(nil).group(:state).count + end + end + + def memoize_per_user(user, method_name) + @memoized ||= {} + @memoized[method_name] ||= {} + @memoized[method_name][user.try!(:id)] ||= yield + end end diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb new file mode 100644 index 00000000000..944519a3070 --- /dev/null +++ b/app/models/concerns/reactive_caching.rb @@ -0,0 +1,114 @@ +# The ReactiveCaching concern is used to fetch some data in the background and +# store it in the Rails cache, keeping it up-to-date for as long as it is being +# requested. If the data hasn't been requested for +reactive_cache_lifetime+, +# it stop being refreshed, and then be removed. +# +# Example of use: +# +# class Foo < ActiveRecord::Base +# include ReactiveCaching +# +# self.reactive_cache_key = ->(thing) { ["foo", thing.id] } +# +# after_save :clear_reactive_cache! +# +# def calculate_reactive_cache +# # Expensive operation here. The return value of this method is cached +# end +# +# def result +# with_reactive_cache do |data| +# # ... +# end +# end +# end +# +# In this example, the first time `#result` is called, it will return `nil`. +# However, it will enqueue a background worker to call `#calculate_reactive_cache` +# and set an initial cache lifetime of ten minutes. +# +# Each time the background job completes, it stores the return value of +# `#calculate_reactive_cache`. It is also re-enqueued to run again after +# `reactive_cache_refresh_interval`, so keeping the stored value up to date. +# Calculations are never run concurrently. +# +# Calling `#result` while a value is in the cache will call the block given to +# `#with_reactive_cache`, yielding the cached value. It will also extend the +# lifetime by `reactive_cache_lifetime`. +# +# Once the lifetime has expired, no more background jobs will be enqueued and +# calling `#result` will again return `nil` - starting the process all over +# again +module ReactiveCaching + extend ActiveSupport::Concern + + included do + class_attribute :reactive_cache_lease_timeout + + class_attribute :reactive_cache_key + class_attribute :reactive_cache_lifetime + class_attribute :reactive_cache_refresh_interval + + # defaults + self.reactive_cache_lease_timeout = 2.minutes + + self.reactive_cache_refresh_interval = 1.minute + self.reactive_cache_lifetime = 10.minutes + + def calculate_reactive_cache + raise NotImplementedError + end + + def with_reactive_cache(&blk) + within_reactive_cache_lifetime do + data = Rails.cache.read(full_reactive_cache_key) + yield data if data.present? + end + ensure + Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime) + ReactiveCachingWorker.perform_async(self.class, id) + end + + def clear_reactive_cache! + Rails.cache.delete(full_reactive_cache_key) + end + + def exclusively_update_reactive_cache! + locking_reactive_cache do + within_reactive_cache_lifetime do + enqueuing_update do + value = calculate_reactive_cache + Rails.cache.write(full_reactive_cache_key, value) + end + end + end + end + + private + + def full_reactive_cache_key(*qualifiers) + prefix = self.class.reactive_cache_key + prefix = prefix.call(self) if prefix.respond_to?(:call) + + ([prefix].flatten + qualifiers).join(':') + end + + def locking_reactive_cache + lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout) + uuid = lease.try_obtain + yield if uuid + ensure + Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid) + end + + def within_reactive_cache_lifetime + yield if Rails.cache.read(full_reactive_cache_key('alive')) + end + + def enqueuing_update + yield + ensure + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id) + end + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 8ef1c841ea3..5cde94b3509 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -128,6 +128,14 @@ class Environment < ActiveRecord::Base end end + def has_terminals? + project.deployment_service.present? && available? && last_deployment.present? + end + + def terminals + project.deployment_service.terminals(self) if has_terminals? + end + # An environment name is not necessarily suitable for use in URLs, DNS # or other third-party contexts, so provide a slugified version. A slug has # the following properties: diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index b01607dcda9..a54e478f5f8 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -24,12 +24,16 @@ class GlobalMilestone @first_milestone = milestones.find {|m| m.description.present? } || milestones.first end + def milestoneish_ids + milestones.select(:id) + end + def safe_title @title.to_slug.normalize.to_s end def projects - @projects ||= Project.for_milestones(milestones.select(:id)) + @projects ||= Project.for_milestones(milestoneish_ids) end def state @@ -49,11 +53,11 @@ class GlobalMilestone end def issues - @issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels) + @issues ||= Issue.of_milestones(milestoneish_ids).includes(:project, :assignee, :labels) end def merge_requests - @merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels) + @merge_requests ||= MergeRequest.of_milestones(milestoneish_ids).includes(:target_project, :assignee, :labels) end def participants diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 45ca97adad1..0dcfec89f14 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -129,6 +129,10 @@ class Milestone < ActiveRecord::Base self.title end + def milestoneish_ids + id + end + def can_be_closed? active? && issues.opened.count.zero? end diff --git a/app/models/project.rb b/app/models/project.rb index 009843d6aed..72d3da64f2d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -533,6 +533,10 @@ class Project < ActiveRecord::Base import_type == 'gitlab_project' end + def gitea_import? + import_type == 'gitea' + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' projects_limit = creator.projects_limit diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index a00d43773d9..4c7f4f5a429 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -5,4 +5,17 @@ class ProjectAuthorization < ActiveRecord::Base validates :project, presence: true validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true + + def self.insert_authorizations(rows, per_batch = 1000) + rows.each_slice(per_batch) do |slice| + tuples = slice.map do |tuple| + tuple.map { |value| connection.quote(value) } + end + + connection.execute <<-EOF.strip_heredoc + INSERT INTO project_authorizations (user_id, project_id, access_level) + VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')} + EOF + end + end end diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index da6be9dd7b7..ab353a1abe6 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -12,4 +12,22 @@ class DeploymentService < Service def predefined_variables [] end + + # Environments may have a number of terminals. Should return an array of + # hashes describing them, e.g.: + # + # [{ + # :selectors => {"a" => "b", "foo" => "bar"}, + # :url => "wss://external.example.com/exec", + # :headers => {"Authorization" => "Token xxx"}, + # :subprotocols => ["foo"], + # :ca_pem => "----BEGIN CERTIFICATE...", # optional + # :created_at => Time.now.utc + # }] + # + # Selectors should be a set of values that uniquely identify a particular + # terminal + def terminals(environment) + raise NotImplementedError + end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index f5fbf8b353b..085125ca9dc 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,4 +1,9 @@ class KubernetesService < DeploymentService + include Gitlab::Kubernetes + include ReactiveCaching + + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + # Namespace defaults to the project path, but can be overridden in case that # is an invalid or inappropriate name prop_accessor :namespace @@ -25,6 +30,8 @@ class KubernetesService < DeploymentService length: 1..63 end + after_save :clear_reactive_cache! + def initialize_properties if properties.nil? self.properties = {} @@ -41,7 +48,8 @@ class KubernetesService < DeploymentService end def help - '' + 'To enable terminal access to Kubernetes environments, label your ' \ + 'deployments with `app=$CI_ENVIRONMENT_SLUG`' end def to_param @@ -75,9 +83,9 @@ class KubernetesService < DeploymentService # Check we can connect to the Kubernetes API def test(*args) - kubeclient = build_kubeclient - kubeclient.discover + kubeclient = build_kubeclient! + kubeclient.discover { success: kubeclient.discovered, result: "Checked API discovery endpoint" } rescue => err { success: false, result: err } @@ -93,20 +101,48 @@ class KubernetesService < DeploymentService variables end - private + # Constructs a list of terminals from the reactive cache + # + # Returns nil if the cache is empty, in which case you should try again a + # short time later + def terminals(environment) + with_reactive_cache do |data| + pods = data.fetch(:pods, nil) + filter_pods(pods, app: environment.slug). + flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. + map { |terminal| add_terminal_auth(terminal, token, ca_pem) } + end + end - def build_kubeclient(api_path = '/api', api_version = 'v1') - return nil unless api_url && namespace && token + # Caches all pods in the namespace so other calls don't need to block on + # network access. + def calculate_reactive_cache + return unless active? && project && !project.pending_delete? - url = URI.parse(api_url) - url.path = url.path[0..-2] if url.path[-1] == "/" - url.path += api_path + kubeclient = build_kubeclient! + + # Store as hashes, rather than as third-party types + pods = begin + kubeclient.get_pods(namespace: namespace).as_json + rescue KubeException => err + raise err unless err.error_code == 404 + [] + end + + # We may want to cache extra things in the future + { pods: pods } + end + + private + + def build_kubeclient!(api_path: 'api', api_version: 'v1') + raise "Incomplete settings" unless api_url && namespace && token ::Kubeclient::Client.new( - url, + join_api_url(api_path), api_version, - ssl_options: kubeclient_ssl_options, auth_options: kubeclient_auth_options, + ssl_options: kubeclient_ssl_options, http_proxy_uri: ENV['http_proxy'] ) end @@ -125,4 +161,13 @@ class KubernetesService < DeploymentService def kubeclient_auth_options { bearer_token: token } end + + def join_api_url(*parts) + url = URI.parse(api_url) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [ prefix, *parts ].join("/") + + url.to_s + end end diff --git a/app/models/user.rb b/app/models/user.rb index 3a17c98eff6..899a89a4eaa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -311,10 +311,6 @@ class User < ActiveRecord::Base find_by(id: Key.unscoped.select(:user_id).where(id: key_id)) end - def build_user(attrs = {}) - User.new(attrs) - end - def reference_prefix '@' end @@ -443,22 +439,16 @@ class User < ActiveRecord::Base end def refresh_authorized_projects - transaction do - project_authorizations.delete_all - - # project_authorizations_union can return multiple records for the same - # project/user with different access_level so we take row with the maximum - # access_level - project_authorizations.connection.execute <<-SQL - INSERT INTO project_authorizations (user_id, project_id, access_level) - SELECT user_id, project_id, MAX(access_level) AS access_level - FROM (#{project_authorizations_union.to_sql}) sub - GROUP BY user_id, project_id - SQL - - unless authorized_projects_populated - update_column(:authorized_projects_populated, true) - end + Users::RefreshAuthorizedProjectsService.new(self).execute + end + + def remove_project_authorizations(project_ids) + project_authorizations.where(id: project_ids).delete_all + end + + def set_authorized_projects_column + unless authorized_projects_populated + update_column(:authorized_projects_populated, true) end end @@ -905,18 +895,6 @@ class User < ActiveRecord::Base private - # Returns a union query of projects that the user is authorized to access - def project_authorizations_union - relations = [ - personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), - groups_projects.select_for_project_authorization, - projects.select_for_project_authorization, - groups.joins(:shared_projects).select_for_project_authorization - ] - - Gitlab::SQL::Union.new(relations) - end - def ci_projects_union scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } groups = groups_projects.where(members: scope) diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 7e0fc9c071e..5d15eb8d3d3 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -23,5 +23,13 @@ class EnvironmentEntity < Grape::Entity environment) end + expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment| + can?(request.user, :admin_environment, environment.project) && + terminal_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + expose :created_at, :updated_at end diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb index ff8c1142abc..e159d750cb7 100644 --- a/app/serializers/request_aware_entity.rb +++ b/app/serializers/request_aware_entity.rb @@ -8,4 +8,8 @@ module RequestAwareEntity def request @options.fetch(:request) end + + def can?(object, action, subject) + Ability.allowed?(object, action, subject) + end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index d7221fe993c..cd230528743 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -4,15 +4,6 @@ module Projects class Error < StandardError; end - ALLOWED_TYPES = [ - 'bitbucket', - 'fogbugz', - 'gitlab', - 'github', - 'google_code', - 'gitlab_project' - ] - def execute add_repository_to_project unless project.gitlab_project_import? @@ -64,14 +55,11 @@ module Projects end def has_importer? - ALLOWED_TYPES.include?(project.import_type) + Gitlab::ImportSources.importer_names.include?(project.import_type) end def importer - return Gitlab::ImportExport::Importer.new(project) if @project.gitlab_project_import? - - class_name = "Gitlab::#{project.import_type.camelize}Import::Importer" - class_name.constantize.new(project) + Gitlab::ImportSources.importer(project.import_type).new(project) end def unknown_url? diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 8b48d90f60b..7613ecd5021 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -146,7 +146,7 @@ module SystemNoteService end def remove_merge_request_wip(noteable, project, author) - body = 'unmarked as a Work In Progress' + body = 'unmarked as a **Work In Progress**' create_note(noteable: noteable, project: project, author: author, note: body) end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb new file mode 100644 index 00000000000..7d38ac3a374 --- /dev/null +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -0,0 +1,128 @@ +module Users + # Service for refreshing the authorized projects of a user. + # + # This particular service class can not be used to update data for the same + # user concurrently. Doing so could lead to an incorrect state. To ensure this + # doesn't happen a caller must synchronize access (e.g. using + # `Gitlab::ExclusiveLease`). + # + # Usage: + # + # user = User.find_by(username: 'alice') + # service = Users::RefreshAuthorizedProjectsService.new(some_user) + # service.execute + class RefreshAuthorizedProjectsService + attr_reader :user + + LEASE_TIMEOUT = 1.minute.to_i + + # user - The User for which to refresh the authorized projects. + def initialize(user) + @user = user + + # We need an up to date User object that has access to all relations that + # may have been created earlier. The only way to ensure this is to reload + # the User object. + user.reload + end + + # This method returns the updated User object. + def execute + current = current_authorizations_per_project + fresh = fresh_access_levels_per_project + + remove = current.each_with_object([]) do |(project_id, row), array| + # rows not in the new list or with a different access level should be + # removed. + if !fresh[project_id] || fresh[project_id] != row.access_level + array << row.id + end + end + + add = fresh.each_with_object([]) do |(project_id, level), array| + # rows not in the old list or with a different access level should be + # added. + if !current[project_id] || current[project_id].access_level != level + array << [user.id, project_id, level] + end + end + + update_with_lease(remove, add) + end + + # Updates the list of authorizations using an exclusive lease. + def update_with_lease(remove = [], add = []) + lease_key = "refresh_authorized_projects:#{user.id}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. If we don't do so we may end up + # not updating the list of authorized projects properly. To prevent + # hammering Redis too much we'll wait for a bit between retries. + sleep(1) + end + + begin + update_authorizations(remove, add) + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end + end + + # Updates the list of authorizations for the current user. + # + # remove - The IDs of the authorization rows to remove. + # add - Rows to insert in the form `[user id, project id, access level]` + def update_authorizations(remove = [], add = []) + return if remove.empty? && add.empty? + + User.transaction do + user.remove_project_authorizations(remove) unless remove.empty? + ProjectAuthorization.insert_authorizations(add) unless add.empty? + user.set_authorized_projects_column + end + + # Since we batch insert authorization rows, Rails' associations may get + # out of sync. As such we force a reload of the User object. + user.reload + end + + def fresh_access_levels_per_project + fresh_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row.access_level + end + end + + def current_authorizations_per_project + current_authorizations.each_with_object({}) do |row, hash| + hash[row.project_id] = row + end + end + + def current_authorizations + user.project_authorizations.select(:id, :project_id, :access_level) + end + + def fresh_authorizations + ProjectAuthorization. + unscoped. + select('project_id, MAX(access_level) AS access_level'). + from("(#{project_authorizations_union.to_sql}) #{ProjectAuthorization.table_name}"). + group(:project_id) + end + + private + + # Returns a union query of projects that the user is authorized to access + def project_authorizations_union + relations = [ + user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), + user.groups_projects.select_for_project_authorization, + user.projects.select_for_project_authorization, + user.groups.joins(:shared_projects).select_for_project_authorization + ] + + Gitlab::SQL::Union.new(relations) + end + end +end diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml new file mode 100644 index 00000000000..f12f9482a51 --- /dev/null +++ b/app/views/import/_githubish_status.html.haml @@ -0,0 +1,61 @@ +- provider = local_assigns.fetch(:provider) +- provider_title = Gitlab::ImportSources.title(provider) + +%p.light + Select projects you want to import. +%hr +%p + = button_tag class: "btn btn-import btn-success js-import-all" do + Import all projects + = icon("spinner spin", class: "loading-icon") + +.table-responsive + %table.table.import-jobs + %colgroup.import-jobs-from-col + %colgroup.import-jobs-to-col + %colgroup.import-jobs-status-col + %thead + %tr + %th= "From #{provider_title}" + %th To GitLab + %th Status + %tbody + - @already_added_projects.each do |project| + %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} + %td + = provider_project_link(provider, project.import_source) + %td + = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] + %td.job-status + - if project.import_status == 'finished' + %span + %i.fa.fa-check + done + - elsif project.import_status == 'started' + %i.fa.fa-spinner.fa-spin + started + - else + = project.human_import_status_name + + - @repos.each do |repo| + %tr{id: "repo_#{repo.id}"} + %td + = provider_project_link(provider, repo.full_name) + %td.import-target + %fieldset.row + .input-group + .project-path.input-group-btn + - if current_user.can_select_namespace? + - selected = params[:namespace_id] || :current_user + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} + = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } + - else + = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true + %span.input-group-addon / + = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true + %td.import-actions.job-status + = button_tag class: "btn btn-import js-add-to-import" do + Import + = icon("spinner spin", class: "loading-icon") + +.js-importer-status{ data: { jobs_import_path: "#{url_for([:jobs, :import, provider])}", import_path: "#{url_for([:import, provider])}" } } diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml new file mode 100644 index 00000000000..02a116f996b --- /dev/null +++ b/app/views/import/gitea/new.html.haml @@ -0,0 +1,23 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path + +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + +%p + To get started, please enter your Gitea Host URL and a + = succeed '.' do + = link_to 'Personal Access Token', 'https://github.com/gogits/go-gogs-client/wiki#access-token' + += form_tag personal_access_token_import_gitea_path, class: 'form-horizontal' do + .form-group + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'control-label' + .col-sm-4 + = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' + .form-group + = label_tag :personal_access_token, 'Personal Access Token', class: 'control-label' + .col-sm-4 + = text_field_tag :personal_access_token, nil, class: 'form-control' + .form-actions + = submit_tag 'List Your Gitea Repositories', class: 'btn btn-create' diff --git a/app/views/import/gitea/status.html.haml b/app/views/import/gitea/status.html.haml new file mode 100644 index 00000000000..589ca27e45d --- /dev/null +++ b/app/views/import/gitea/status.html.haml @@ -0,0 +1,7 @@ +- page_title "Gitea Import" +- header_title "Projects", root_path +%h3.page-title + = custom_icon('go_logo') + Import Projects from Gitea + += render 'import/githubish_status', provider: 'gitea' diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index 4c721d40b55..0fe578a0036 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -1,64 +1,6 @@ -- page_title "GitHub import" +- page_title "GitHub Import" - header_title "Projects", root_path %h3.page-title - %i.fa.fa-github - Import projects from GitHub + = icon 'github', text: 'Import Projects from GitHub' -%p.light - Select projects you want to import. -%hr -%p - = button_tag class: "btn btn-import btn-success js-import-all" do - Import all projects - = icon("spinner spin", class: "loading-icon") - -.table-responsive - %table.table.import-jobs - %colgroup.import-jobs-from-col - %colgroup.import-jobs-to-col - %colgroup.import-jobs-status-col - %thead - %tr - %th From GitHub - %th To GitLab - %th Status - %tbody - - @already_added_projects.each do |project| - %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"} - %td - = github_project_link(project.import_source) - %td - = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] - %td.job-status - - if project.import_status == 'finished' - %span - %i.fa.fa-check - done - - elsif project.import_status == 'started' - %i.fa.fa-spinner.fa-spin - started - - else - = project.human_import_status_name - - - @repos.each do |repo| - %tr{id: "repo_#{repo.id}"} - %td - = github_project_link(repo.full_name) - %td.import-target - %fieldset.row - .input-group - .project-path.input-group-btn - - if current_user.can_select_namespace? - - selected = params[:namespace_id] || :current_user - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {} - = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 } - - else - = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true - %span.input-group-addon / - = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true - %td.import-actions.job-status - = button_tag class: "btn btn-import js-add-to-import" do - Import - = icon("spinner spin", class: "loading-icon") - -.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } } += render 'import/githubish_status', provider: 'github' diff --git a/app/views/projects/environments/_terminal_button.html.haml b/app/views/projects/environments/_terminal_button.html.haml new file mode 100644 index 00000000000..97de9c95de7 --- /dev/null +++ b/app/views/projects/environments/_terminal_button.html.haml @@ -0,0 +1,3 @@ +- if environment.has_terminals? && can?(current_user, :admin_environment, @project) + = link_to terminal_namespace_project_environment_path(@project.namespace, @project, environment), class: 'btn terminal-button' do + = icon('terminal') diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a65a630f2d0..0c6f696f5b9 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -4,10 +4,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag("environments/environments_bundle.js") -.commit-icon-svg.hidden - = custom_icon("icon_commit") -.play-icon-svg.hidden - = custom_icon("icon_play") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, @@ -19,4 +15,5 @@ "help-page-path" => help_page_path("ci/environments"), "css-class" => container_class, "commit-icon-svg" => custom_icon("icon_commit"), + "terminal-icon-svg" => custom_icon("icon_terminal"), "play-icon-svg" => custom_icon("icon_play")}} diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index bcac73d3698..6e0d9456900 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -8,6 +8,7 @@ %h3.page-title= @environment.name.capitalize .col-md-3 .nav-controls + = render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/external_url', environment: @environment - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml new file mode 100644 index 00000000000..a6726e509e0 --- /dev/null +++ b/app/views/projects/environments/terminal.html.haml @@ -0,0 +1,22 @@ +- @no_container = true +- page_title "Terminal for environment", @environment.name += render "projects/pipelines/head" + +- content_for :page_specific_javascripts do + = stylesheet_link_tag "xterm/xterm" + = page_specific_javascript_tag("terminal/terminal_bundle.js") + +%div{class: container_class} + .top-area + .row + .col-sm-6 + %h3.page-title + Terminal for environment + = @environment.name + + .col-sm-6 + .nav-controls + = render 'projects/deployments/actions', deployment: @environment.last_deployment + +.terminal-container{class: container_class} + #terminal{data:{project_path: "#{terminal_namespace_project_environment_path(@project.namespace, @project, @environment)}.ws"}} diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 0788924d44a..866b278ce57 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -69,6 +69,11 @@ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do = icon('bug', text: 'Fogbugz') %div + - if gitea_import_enabled? + = link_to new_import_gitea_url, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do = icon('git', text: 'Repo by URL') diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index e939278bc07..07d4927b6c9 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -8,7 +8,7 @@ = render 'shared/empty_states/icons/issues.svg' .col-xs-12{ class: "#{'col-sm-6' if has_button}" } .text-content - - if has_button + - if has_button && current_user %h4 The Issue Tracker is a good place to add things that need to be improved or solved in a project! %p diff --git a/app/views/shared/icons/_go_logo.svg.erb b/app/views/shared/icons/_go_logo.svg.erb new file mode 100644 index 00000000000..5052651c110 --- /dev/null +++ b/app/views/shared/icons/_go_logo.svg.erb @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16"><g fill-rule="evenodd" transform="translate(0 1)"><path d="m14 15.01h1v-8.02c0-3.862-3.134-6.991-7-6.991-3.858 0-7 3.13-7 6.991v8.02h1v-8.02c0-3.306 2.691-5.991 6-5.991 3.314 0 6 2.682 6 5.991v8.02m-10.52-13.354c-.366-.402-.894-.655-1.48-.655-1.105 0-2 .895-2 2 0 .868.552 1.606 1.325 1.883.102-.321.226-.631.371-.93-.403-.129-.695-.507-.695-.953 0-.552.448-1 1-1 .306 0 .58.138.764.354.222-.25.461-.483.717-.699m9.04-.002c.366-.401.893-.653 1.479-.653 1.105 0 2 .895 2 2 0 .867-.552 1.606-1.324 1.883-.101-.321-.225-.632-.37-.931.403-.129.694-.507.694-.952 0-.552-.448-1-1-1-.305 0-.579.137-.762.353-.222-.25-.461-.483-.717-.699"/><path d="m5.726 7.04h1.557v.124c0 .283-.033.534-.1.752-.065.202-.175.391-.33.566-.35.394-.795.591-1.335.591-.527 0-.979-.19-1.355-.571-.376-.382-.564-.841-.564-1.377 0-.547.191-1.01.574-1.391.382-.382.848-.574 1.396-.574.295 0 .57.06.825.181.244.12.484.316.72.586l-.405.388c-.309-.412-.686-.618-1.13-.618-.399 0-.733.138-1 .413-.27.27-.405.609-.405 1.015 0 .42.151.766.452 1.037.282.252.587.378.915.378.28 0 .531-.094.754-.283.223-.19.347-.418.373-.683h-.94v-.535m2.884.061c0-.53.194-.986.583-1.367.387-.381.853-.571 1.396-.571.537 0 .998.192 1.382.576.386.384.578.845.578 1.384 0 .542-.194 1-.581 1.379-.389.379-.858.569-1.408.569-.487 0-.923-.168-1.311-.505-.426-.373-.64-.861-.64-1.465m.574.007c0 .417.14.759.42 1.028.278.269.6.403.964.403.395 0 .729-.137 1-.41.272-.277.408-.613.408-1.01 0-.402-.134-.739-.403-1.01-.267-.273-.597-.41-.991-.41-.392 0-.723.137-.993.41-.27.27-.405.604-.405 1m-.184 3.918c.525.026.812.063.812.063.271.025.324-.096.116-.273 0 0-.775-.813-1.933-.813-1.159 0-1.923.813-1.923.813-.211.174-.153.3.12.273 0 0 .286-.037.81-.063v.477c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.252.25c0 .268.224.5.5.5.268 0 .5-.223.5-.498v-.478m-1-1.023c.552 0 1-.224 1-.5 0-.276-.448-.5-1-.5-.552 0-1 .224-1 .5 0 .276.448.5 1 .5"/></g></svg> diff --git a/app/views/shared/icons/_icon_terminal.svg b/app/views/shared/icons/_icon_terminal.svg new file mode 100644 index 00000000000..c80f44c3edf --- /dev/null +++ b/app/views/shared/icons/_icon_terminal.svg @@ -0,0 +1 @@ +<svg width="19" height="14" viewBox="0 0 19 14" xmlns="http://www.w3.org/2000/svg"><rect fill="#848484" x="7.2" y="9.25" width="6.46" height="1.5" rx=".5"/><path d="M5.851 7.016L3.81 9.103a.503.503 0 0 0 .017.709l.35.334c.207.198.524.191.717-.006l2.687-2.748a.493.493 0 0 0 .137-.376.493.493 0 0 0-.137-.376L4.894 3.892a.507.507 0 0 0-.717-.006l-.35.334a.503.503 0 0 0-.017.709L5.85 7.016z"/><path d="M1.25 11.497c0 .691.562 1.253 1.253 1.253h13.994c.694 0 1.253-.56 1.253-1.253V2.503c0-.691-.562-1.253-1.253-1.253H2.503c-.694 0-1.253.56-1.253 1.253v8.994zM2.503 0h13.994A2.504 2.504 0 0 1 19 2.503v8.994A2.501 2.501 0 0 1 16.497 14H2.503A2.504 2.504 0 0 1 0 11.497V2.503A2.501 2.501 0 0 1 2.503 0z"/></svg> diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index fccddb70d18..2badd0680fb 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -2,8 +2,6 @@ class AuthorizedProjectsWorker include Sidekiq::Worker include DedicatedSidekiqQueue - LEASE_TIMEOUT = 1.minute.to_i - def self.bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end @@ -11,24 +9,6 @@ class AuthorizedProjectsWorker def perform(user_id) user = User.find_by(id: user_id) - refresh(user) if user - end - - def refresh(user) - lease_key = "refresh_authorized_projects:#{user.id}" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - - until uuid = lease.try_obtain - # Keep trying until we obtain the lease. If we don't do so we may end up - # not updating the list of authorized projects properly. To prevent - # hammering Redis too much we'll wait for a bit between retries. - sleep(1) - end - - begin - user.refresh_authorized_projects - ensure - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - end + user.refresh_authorized_projects if user end end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb new file mode 100644 index 00000000000..9af9dae04f0 --- /dev/null +++ b/app/workers/reactive_caching_worker.rb @@ -0,0 +1,15 @@ +class ReactiveCachingWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(class_name, id) + klass = begin + Kernel.const_get(class_name) + rescue NameError + nil + end + return unless klass + + klass.find_by(id: id).try(:exclusively_update_reactive_cache!) + end +end |