Commit ec3a5335 authored by Clement Ho's avatar Clement Ho

Merge branch 'webpack-mirrors' of gitlab.com:gitlab-org/gitlab-ee into webpack-mirrors

parents a00bb743 3c6ad577
...@@ -37,10 +37,11 @@ export default class Clusters { ...@@ -37,10 +37,11 @@ export default class Clusters {
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
ingressHelpPath, ingressHelpPath,
ingressDnsHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset; } = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore(); this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath); this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath); this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus); this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason); this.store.updateStatusReason(clusterStatusReason);
...@@ -98,6 +99,7 @@ export default class Clusters { ...@@ -98,6 +99,7 @@ export default class Clusters {
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath, ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath, managePrometheusPath: this.state.managePrometheusPath,
ingressDnsHelpPath: this.state.ingressDnsHelpPath,
}, },
}); });
}, },
......
...@@ -36,10 +36,6 @@ ...@@ -36,10 +36,6 @@
type: String, type: String,
required: false, required: false,
}, },
description: {
type: String,
required: true,
},
status: { status: {
type: String, type: String,
required: false, required: false,
...@@ -148,7 +144,7 @@ ...@@ -148,7 +144,7 @@
class="table-section section-wrap" class="table-section section-wrap"
role="gridcell" role="gridcell"
> >
<div v-html="description"></div> <slot name="description"></slot>
</div> </div>
<div <div
class="table-section table-button-footer section-align-top" class="table-section table-button-footer section-align-top"
......
...@@ -2,10 +2,16 @@ ...@@ -2,10 +2,16 @@
import _ from 'underscore'; import _ from 'underscore';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import {
APPLICATION_INSTALLED,
INGRESS,
} from '../constants';
export default { export default {
components: { components: {
applicationRow, applicationRow,
clipboardButton,
}, },
props: { props: {
applications: { applications: {
...@@ -23,6 +29,11 @@ ...@@ -23,6 +29,11 @@
required: false, required: false,
default: '', default: '',
}, },
ingressDnsHelpPath: {
type: String,
required: false,
default: '',
},
managePrometheusPath: { managePrometheusPath: {
type: String, type: String,
required: false, required: false,
...@@ -43,19 +54,16 @@ ...@@ -43,19 +54,16 @@
false, false,
); );
}, },
helmTillerDescription() { ingressId() {
return _.escape(s__( return INGRESS;
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. },
Tiller runs inside of your Kubernetes Cluster, and manages ingressInstalled() {
releases of your charts.`, return this.applications.ingress.status === APPLICATION_INSTALLED;
)); },
ingressExternalIp() {
return this.applications.ingress.externalIp;
}, },
ingressDescription() { ingressDescription() {
const descriptionParagraph = _.escape(s__(
`ClusterIntegration|Ingress gives you a way to route requests to services based on the
request host or path, centralizing a number of services into a single entrypoint.`,
));
const extraCostParagraph = sprintf( const extraCostParagraph = sprintf(
_.escape(s__( _.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources `ClusterIntegration|%{boldNotice} This will add some extra resources
...@@ -83,9 +91,6 @@ ...@@ -83,9 +91,6 @@
); );
return ` return `
<p>
${descriptionParagraph}
</p>
<p> <p>
${extraCostParagraph} ${extraCostParagraph}
</p> </p>
...@@ -136,33 +141,121 @@ ...@@ -136,33 +141,121 @@
id="helm" id="helm"
:title="applications.helm.title" :title="applications.helm.title"
title-link="https://docs.helm.sh/" title-link="https://docs.helm.sh/"
:description="helmTillerDescription"
:status="applications.helm.status" :status="applications.helm.status"
:status-reason="applications.helm.statusReason" :status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus" :request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
/> >
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) }}
</div>
</application-row>
<application-row <application-row
id="ingress" :id="ingressId"
:title="applications.ingress.title" :title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
:description="ingressDescription"
:status="applications.ingress.status" :status="applications.ingress.status"
:status-reason="applications.ingress.statusReason" :status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus" :request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason" :request-reason="applications.ingress.requestReason"
/> >
<div slot="description">
<p>
{{ s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`) }}
</p>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }}
</label>
<div
v-if="ingressExternalIp"
class="input-group"
>
<input
type="text"
id="ingress-ip-address"
class="form-control js-ip-address"
:value="ingressExternalIp"
readonly
/>
<span class="input-group-btn">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
css-class="btn btn-default js-clipboard-btn"
/>
</span>
</div>
<input
v-else
type="text"
class="form-control js-ip-address"
readonly
value="?"
/>
</div>
<p
v-if="!ingressExternalIp"
class="settings-message js-no-ip-message"
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on GKE if it takes a long time.`) }}
<a
:href="ingressHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
<div
v-else
v-html="ingressDescription"
>
</div>
</div>
</application-row>
<application-row <application-row
id="prometheus" id="prometheus"
:title="applications.prometheus.title" :title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/" title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath" :manage-link="managePrometheusPath"
:description="prometheusDescription"
:status="applications.prometheus.status" :status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason" :status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus" :request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason" :request-reason="applications.prometheus.requestReason"
/> >
<div
slot="description"
v-html="prometheusDescription"
>
</div>
</application-row>
<!-- <!--
NOTE: Don't forget to update `clusters.scss` NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests min-height for this block and uncomment `application_spec` tests
......
...@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored'; ...@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored';
export const REQUEST_LOADING = 'request-loading'; export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure'; export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { INGRESS } from '../constants';
export default class ClusterStore { export default class ClusterStore {
constructor() { constructor() {
...@@ -21,6 +22,7 @@ export default class ClusterStore { ...@@ -21,6 +22,7 @@ export default class ClusterStore {
statusReason: null, statusReason: null,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
externalIp: null,
}, },
runner: { runner: {
title: s__('ClusterIntegration|GitLab Runner'), title: s__('ClusterIntegration|GitLab Runner'),
...@@ -40,9 +42,10 @@ export default class ClusterStore { ...@@ -40,9 +42,10 @@ export default class ClusterStore {
}; };
} }
setHelpPaths(helpPath, ingressHelpPath) { setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) {
this.state.helpPath = helpPath; this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath; this.state.ingressHelpPath = ingressHelpPath;
this.state.ingressDnsHelpPath = ingressDnsHelpPath;
} }
setManagePrometheusPath(managePrometheusPath) { setManagePrometheusPath(managePrometheusPath) {
...@@ -64,6 +67,7 @@ export default class ClusterStore { ...@@ -64,6 +67,7 @@ export default class ClusterStore {
updateStateFromServer(serverState = {}) { updateStateFromServer(serverState = {}) {
this.state.status = serverState.status; this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason; this.state.statusReason = serverState.status_reason;
serverState.applications.forEach((serverAppEntry) => { serverState.applications.forEach((serverAppEntry) => {
const { const {
name: appId, name: appId,
...@@ -76,6 +80,10 @@ export default class ClusterStore { ...@@ -76,6 +80,10 @@ export default class ClusterStore {
status, status,
statusReason, statusReason,
}; };
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
}
}); });
} }
} }
import initRegistryImages from '~/registry/index';
document.addEventListener('DOMContentLoaded', initRegistryImages);
...@@ -4,10 +4,14 @@ import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; ...@@ -4,10 +4,14 @@ import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys'; import initDeployKeys from '~/deploy_keys';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new ProtectedTagCreate(); new ProtectedTagCreate();
new ProtectedTagEditList(); new ProtectedTagEditList();
initDeployKeys(); initDeployKeys();
initSettingsPanels(); initSettingsPanels();
new ProtectedBranchCreate(); // eslint-disable-line no-new
new ProtectedBranchEditList(); // eslint-disable-line no-new
}); });
/* eslint-disable no-unused-vars */
import ProtectedBranchCreate from './protected_branch_create';
import ProtectedBranchEditList from './protected_branch_edit_list';
$(() => {
const protectedBranchCreate = new ProtectedBranchCreate();
const protectedBranchEditList = new ProtectedBranchEditList();
});
...@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate'; ...@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ export default () => new Vue({
el: '#js-vue-registry-images', el: '#js-vue-registry-images',
components: { components: {
registryApp, registryApp,
...@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); });
/* eslint-disable func-names, wrap-iife */
/* global u2f */
import _ from 'underscore'; import _ from 'underscore';
import isU2FSupported from './util'; import importU2FLibrary from './util';
import U2FError from './error'; import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with. // Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
...@@ -10,6 +8,7 @@ import U2FError from './error'; ...@@ -10,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup // State Flow #2: setup -> in_progress -> error -> setup
export default class U2FAuthenticate { export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) { constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
this.u2fUtils = null;
this.container = container; this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this); this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this); this.renderAuthenticated = this.renderAuthenticated.bind(this);
...@@ -50,22 +49,23 @@ export default class U2FAuthenticate { ...@@ -50,22 +49,23 @@ export default class U2FAuthenticate {
} }
start() { start() {
if (isU2FSupported()) { return importU2FLibrary()
return this.renderInProgress(); .then((utils) => {
} this.u2fUtils = utils;
return this.renderNotSupported(); this.renderInProgress();
})
.catch(() => this.renderNotSupported());
} }
authenticate() { authenticate() {
return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) { return this.u2fUtils.sign(this.appId, this.challenge, this.signRequests,
return function (response) { (response) => {
if (response.errorCode) { if (response.errorCode) {
const error = new U2FError(response.errorCode, 'authenticate'); const error = new U2FError(response.errorCode, 'authenticate');
return _this.renderError(error); return this.renderError(error);
} }
return _this.renderAuthenticated(JSON.stringify(response)); return this.renderAuthenticated(JSON.stringify(response));
}; }, 10);
})(this), 10);
} }
renderTemplate(name, params) { renderTemplate(name, params) {
......
/* eslint-disable func-names, wrap-iife */
/* global u2f */
import _ from 'underscore'; import _ from 'underscore';
import isU2FSupported from './util'; import importU2FLibrary from './util';
import U2FError from './error'; import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with. // Register U2F (universal 2nd factor) devices for users to authenticate with.
...@@ -11,6 +8,7 @@ import U2FError from './error'; ...@@ -11,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup // State Flow #2: setup -> in_progress -> error -> setup
export default class U2FRegister { export default class U2FRegister {
constructor(container, u2fParams) { constructor(container, u2fParams) {
this.u2fUtils = null;
this.container = container; this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this); this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderRegistered = this.renderRegistered.bind(this); this.renderRegistered = this.renderRegistered.bind(this);
...@@ -34,22 +32,23 @@ export default class U2FRegister { ...@@ -34,22 +32,23 @@ export default class U2FRegister {
} }
start() { start() {
if (isU2FSupported()) { return importU2FLibrary()
return this.renderSetup(); .then((utils) => {
} this.u2fUtils = utils;
return this.renderNotSupported(); this.renderSetup();
})
.catch(() => this.renderNotSupported());
} }
register() { register() {
return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) { return this.u2fUtils.register(this.appId, this.registerRequests, this.signRequests,
return function (response) { (response) => {
if (response.errorCode) { if (response.errorCode) {
const error = new U2FError(response.errorCode, 'register'); const error = new U2FError(response.errorCode, 'register');
return _this.renderError(error); return this.renderError(error);
} }
return _this.renderRegistered(JSON.stringify(response)); return this.renderRegistered(JSON.stringify(response));
}; }, 10);
})(this), 10);
} }
renderTemplate(name, params) { renderTemplate(name, params) {
......
export default function isU2FSupported() { function isOpera(userAgent) {
return window.u2f; return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0;
}
function getOperaVersion(userAgent) {
const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/);
return match ? parseInt(match[1], 10) : false;
}
function isChrome(userAgent) {
return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent);
}
function getChromeVersion(userAgent) {
const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
return match ? parseInt(match[1], 10) : false;
}
export function canInjectU2fApi(userAgent) {
const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41;
const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40;
const isMobile = (
userAgent.indexOf('droid') >= 0 ||
userAgent.indexOf('CriOS') >= 0 ||
/\b(iPad|iPhone|iPod)(?=;)/.test(userAgent)
);
return (isSupportedChrome || isSupportedOpera) && !isMobile;
}
export default function importU2FLibrary() {
if (window.u2f) {
return Promise.resolve(window.u2f);
}
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) {
return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f);
}
return Promise.reject();
} }
...@@ -28,6 +28,11 @@ ...@@ -28,6 +28,11 @@
required: false, required: false,
default: false, default: false,
}, },
cssClass: {
type: String,
required: false,
default: 'btn btn-default btn-transparent btn-clipboard',
},
}, },
}; };
</script> </script>
...@@ -35,7 +40,7 @@ ...@@ -35,7 +40,7 @@
<template> <template>
<button <button
type="button" type="button"
class="btn btn-transparent btn-clipboard" :class="cssClass"
:title="title" :title="title"
:data-clipboard-text="text" :data-clipboard-text="text"
v-tooltip v-tooltip
......
...@@ -17,7 +17,7 @@ module IssuableCollections ...@@ -17,7 +17,7 @@ module IssuableCollections
set_pagination set_pagination
return if redirect_out_of_range(@total_pages) return if redirect_out_of_range(@total_pages)
if params[:label_name].present? if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] } labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute @labels = LabelsFinder.new(current_user, labels_params).execute
end end
......
...@@ -14,12 +14,13 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -14,12 +14,13 @@ class Groups::LabelsController < Groups::ApplicationController
end end
format.json do format.json do
available_labels = available_labels = LabelsFinder.new(
if params[:only_group_labels] current_user,
group.labels group_id: @group.id,
else only_group_labels: params[:only_group_labels],
LabelsFinder.new(current_user, group_id: @group.id).execute include_ancestor_groups: params[:include_ancestor_groups],
end include_descendant_groups: params[:include_descendant_groups]
).execute
render json: LabelSerializer.new.represent_appearance(available_labels) render json: LabelSerializer.new.represent_appearance(available_labels)
end end
......
...@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController
before_action :authorize_create_cluster!, only: [:new] before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy] before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status]
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
...@@ -101,4 +102,8 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -101,4 +102,8 @@ class Projects::ClustersController < Projects::ApplicationController
def authorize_admin_cluster! def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster) access_denied! unless can?(current_user, :admin_cluster, cluster)
end end
def update_applications_status
@cluster.applications.each(&:schedule_status_update)
end
end end
...@@ -61,12 +61,20 @@ class LabelsFinder < UnionFinder ...@@ -61,12 +61,20 @@ class LabelsFinder < UnionFinder
def group_ids def group_ids
strong_memoize(:group_ids) do strong_memoize(:group_ids) do
group = Group.find(params[:group_id]) groups_user_can_read_labels(groups_to_include).map(&:id)
groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group]
groups_user_can_read_labels(groups).map(&:id)
end end
end end
def groups_to_include
group = Group.find(params[:group_id])
groups = [group]
groups += group.ancestors if params[:include_ancestor_groups].present?
groups += group.descendants if params[:include_descendant_groups].present?
groups
end
def group? def group?
params[:group_id].present? params[:group_id].present?
end end
......
module U2fHelper
def inject_u2f_api?
((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile?
end
end
...@@ -5,6 +5,7 @@ module Clusters ...@@ -5,6 +5,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus include ::Clusters::Concerns::ApplicationStatus
include AfterCommitQueue
default_value_for :ingress_type, :nginx default_value_for :ingress_type, :nginx
default_value_for :version, :nginx default_value_for :version, :nginx
...@@ -13,6 +14,17 @@ module Clusters ...@@ -13,6 +14,17 @@ module Clusters
nginx: 1 nginx: 1
} }
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
before_transition any => [:installed] do |application|
application.run_after_commit do
ClusterWaitForIngressIpAddressWorker.perform_in(
FETCH_IP_ADDRESS_DELAY, application.name, application.id)
end
end
end
def chart def chart
'stable/nginx-ingress' 'stable/nginx-ingress'
end end
...@@ -24,6 +36,13 @@ module Clusters ...@@ -24,6 +36,13 @@ module Clusters
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
end end
def schedule_status_update
return unless installed?
return if external_ip
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
end end
end end
end end
...@@ -23,6 +23,11 @@ module Clusters ...@@ -23,6 +23,11 @@ module Clusters
def name def name
self.class.application_name self.class.application_name
end end
def schedule_status_update
# Override if you need extra data synchronized
# from K8s after installation
end
end end
end end
end end
......
...@@ -6,6 +6,12 @@ class CycleAnalytics ...@@ -6,6 +6,12 @@ class CycleAnalytics
@options = options @options = options
end end
def all_medians_per_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].median
end
end
def summary def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
from: @options[:from], from: @options[:from],
......
...@@ -597,15 +597,7 @@ class Repository ...@@ -597,15 +597,7 @@ class Repository
def license_key def license_key
return unless exists? return unless exists?
# The licensee gem creates a Rugged object from the path: raw_repository.license_short_name
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
begin
Licensee.license(path).try(:key)
# Normally we would rescue Rugged::Error, but that is banned by lint-rugged
# and we need to migrate this endpoint to Gitaly:
# https://gitlab.com/gitlab-org/gitaly/issues/1026
rescue
end
end end
cache_method :license_key cache_method :license_key
......
...@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :description expose :description
expose :median, as: :value do |stage| expose :median, as: :value do |stage|
stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil # median returns a BatchLoader instance which we first have to unwrap by using to_i
!stage.median.to_i.zero? ? distance_of_time_in_words(stage.median) : nil
end end
end end
...@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity ...@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :name expose :name
expose :status_name, as: :status expose :status_name, as: :status
expose :status_reason expose :status_reason
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
end end
module Clusters
module Applications
class CheckIngressIpAddressService < BaseHelmService
include Gitlab::Utils::StrongMemoize
Error = Class.new(StandardError)
LEASE_TIMEOUT = 15.seconds.to_i
def execute
return if app.external_ip
return unless try_obtain_lease
app.update!(external_ip: ingress_ip) if ingress_ip
end
private
def try_obtain_lease
Gitlab::ExclusiveLease
.new("check_ingress_ip_address_service:#{app.id}", timeout: LEASE_TIMEOUT)
.try_obtain
end
def ingress_ip
service.status.loadBalancer.ingress&.first&.ip
end
def service
strong_memoize(:ingress_service) do
kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
end
end
end
end
end
- if inject_u2f_api?
- content_for :page_specific_javascripts do
= webpack_bundle_tag('u2f')
%div %div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
.login-box .login-box
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'issues'
- if group_issues_count(state: 'all').zero? - if group_issues_count(state: 'all').zero?
= render 'shared/empty_states/issues', project_select_button: true = render 'shared/empty_states/issues', project_select_button: true
......
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
- if inject_u2f_api?
= webpack_bundle_tag('u2f')
= webpack_bundle_tag('two_factor_auth') = webpack_bundle_tag('two_factor_auth')
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path } .js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
......
...@@ -18,7 +18,14 @@ ...@@ -18,7 +18,14 @@
.email-modal-input-group.input-group .email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true = text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn .input-group-btn
= clipboard_button(target: '#issuable_email') = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs')
= mail_to email, class: 'btn btn-clipboard btn-transparent',
subject: _("Enter the #{name} title"),
body: _("Enter the #{name} description"),
title: _('Send email'),
data: { toggle: 'tooltip', placement: 'bottom' } do
= sprite_icon('mail')
%p %p
= render 'by_email_description' = render 'by_email_description'
%p %p
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'), help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'), ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } } manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice .js-cluster-application-notice
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'issues'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
......
...@@ -7,9 +7,6 @@ ...@@ -7,9 +7,6 @@
- can_update_issue = can?(current_user, :update_issue, @issue) - can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_report_spam = @issue.submittable_as_spam_by?(current_user)
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('issuable')
.detail-page-header .detail-page-header
.detail-page-header-body .detail-page-header-body
......
...@@ -5,14 +5,8 @@ ...@@ -5,14 +5,8 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description - page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes - page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do - if has_vue_discussions_cookie?
= webpack_bundle_tag('common_vue') - content_for :page_specific_javascripts do
= webpack_bundle_tag('issuable')
- if has_vue_discussions_cookie?
= webpack_bundle_tag('mr_notes')
- if has_vue_discussions_cookie?
= webpack_bundle_tag('mr_notes') = webpack_bundle_tag('mr_notes')
.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('protected_branches')
- content_for :create_protected_branch do - content_for :create_protected_branch do
= render 'projects/protected_branches/create_protected_branch' = render 'projects/protected_branches/create_protected_branch'
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
= webpack_bundle_tag('common_vue') = webpack_bundle_tag('common_vue')
= webpack_bundle_tag('registry_list')
.row.prepend-top-10 .row.prepend-top-10
.col-lg-12 .col-lg-12
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
- gcp_cluster:cluster_wait_for_app_installation - gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation - gcp_cluster:wait_for_cluster_creation
- gcp_cluster:check_gcp_project_billing - gcp_cluster:check_gcp_project_billing
- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage - github_import_advance_stage
- github_importer:github_import_import_diff_note - github_importer:github_import_import_diff_note
......
class ClusterWaitForIngressIpAddressWorker
include ApplicationWorker
include ClusterQueue
include ClusterApplications
def perform(app_name, app_id)
find_application(app_name, app_id) do |app|
Clusters::Applications::CheckIngressIpAddressService.new(app).execute
end
end
end
---
title: Add email button to new issue by email
merge_request: 10942
author: Islam Wazery
---
title: Include cycle time in usage ping data
merge_request: 16973
author:
type: added
---
title: Display ingress IP address in the Kubernetes page
merge_request: 17052
author:
type: added
...@@ -50,8 +50,6 @@ function generateEntries() { ...@@ -50,8 +50,6 @@ function generateEntries() {
const manualEntries = { const manualEntries = {
monitoring: './monitoring/monitoring_bundle.js', monitoring: './monitoring/monitoring_bundle.js',
mr_notes: './mr_notes/index.js', mr_notes: './mr_notes/index.js',
protected_branches: './protected_branches',
registry_list: './registry/index.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
...@@ -62,21 +60,14 @@ function generateEntries() { ...@@ -62,21 +60,14 @@ function generateEntries() {
ide: './ide/index.js', ide: './ide/index.js',
raven: './raven/index.js', raven: './raven/index.js',
test: './test.js', test: './test.js',
u2f: ['vendor/u2f'],
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
// EE-only // EE-only
add_gitlab_slack_application: 'ee/add_gitlab_slack_application/index.js', add_gitlab_slack_application: 'ee/add_gitlab_slack_application/index.js',
burndown_chart: 'ee/burndown_chart/index.js', burndown_chart: 'ee/burndown_chart/index.js',
epic_show: 'ee/epics/epic_show/epic_show_bundle.js',
new_epic: 'ee/epics/new_epic/new_epic_bundle.js',
geo_nodes: 'ee/geo_nodes', geo_nodes: 'ee/geo_nodes',
issuable: 'ee/issuable/issuable_bundle.js',
issues: 'ee/issues/issues_bundle.js',
ldap_group_links: 'ee/groups/ldap_group_links.js', ldap_group_links: 'ee/groups/ldap_group_links.js',
ee_protected_branches: 'ee/protected_branches',
service_desk: 'ee/projects/settings_service_desk/service_desk_bundle.js', service_desk: 'ee/projects/settings_service_desk/service_desk_bundle.js',
roadmap: 'ee/roadmap',
}; };
return Object.assign(manualEntries, autoEntries); return Object.assign(manualEntries, autoEntries);
......
class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :clusters_applications_ingress, :external_ip, :string
end
end
...@@ -665,6 +665,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do ...@@ -665,6 +665,7 @@ ActiveRecord::Schema.define(version: 20180301084653) do
t.string "version", null: false t.string "version", null: false
t.string "cluster_ip" t.string "cluster_ip"
t.text "status_reason" t.text "status_reason"
t.string "external_ip"
end end
create_table "clusters_applications_prometheus", force: :cascade do |t| create_table "clusters_applications_prometheus", force: :cascade do |t|
......
...@@ -454,9 +454,85 @@ wal_keep_segments = 10 ...@@ -454,9 +454,85 @@ wal_keep_segments = 10
hot_standby = on hot_standby = on
``` ```
#### Tracking Database for the Secondary nodes
NOTE: **Note**:
You only need to follow the steps below if you are not using the managed
PostgreSQL from a Omnibus GitLab package.
Geo secondary nodes use a tracking database to keep track of replication Geo secondary nodes use a tracking database to keep track of replication
status and recover automatically from some replication issues. Follow the status and recover automatically from some replication issues.
instructions for [enabling tracking database on the secondary server][tracking].
This is a separate PostgreSQL installation that can be configured to use
FDW to connect with the secondary database for improved performance.
To enable an external PostgreSQL instance as tracking database, follow
the instructions below:
1. Edit `/etc/gitlab/gitlab.rb` with the connection params and credentials
```ruby
# note this is shared between both databases,
# make sure you define the same password in both
gitlab_rails['db_password'] = 'mypassword'
geo_secondary['db_host'] = '2.3.4.5' # change to the correct public IP
geo_secondary['db_port'] = 5431 # change to the correct port
geo_secondary['db_fdw'] = true # enable FDW
geo_postgresql['enable'] = false # don't use internal managed instance
```
1. Reconfigure GitLab for the changes to take effect:
```bash
gitlab-ctl reconfigure
```
1. Run the tracking database migrations:
```bash
gitlab-rake geo:db:migrate
```
1. Configure the [PostgreSQL FDW][FDW] connection and credentials:
Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
params to match your environment. Execute it to setup the FDW connection.
```bash
#!/bin/bash
# Secondary Database connection params:
DB_HOST="5.6.7.8" # change to the public IP or VPC private IP
DB_NAME="gitlabhq_production"
DB_USER="gitlab"
DB_PORT="5432"
# Tracking Database connection params:
GEO_DB_HOST="2.3.4.5" # change to the public IP or VPC private IP
GEO_DB_NAME="gitlabhq_geo_production"
GEO_DB_USER="gitlab_geo"
GEO_DB_PORT="5432"
query_exec () {
gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
}
query_exec "CREATE EXTENSION postgres_fdw;"
query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}');"
query_exec "CREATE SCHEMA gitlab_secondary;"
query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
```
NOTE: **Note:** The script template above uses `gitlab-psql` as it's intended to be executed from the Geo machine,
but you can change it to `psql` and run it from any machine that has access to the database.
1. Restart GitLab
```bash
gitlab-ctl restart
```
## MySQL replication ## MySQL replication
......
...@@ -262,28 +262,32 @@ node. ...@@ -262,28 +262,32 @@ node.
1. Configure the [PostgreSQL FDW][FDW] connection and credentials: 1. Configure the [PostgreSQL FDW][FDW] connection and credentials:
Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
params to match your environment. params to match your environment. Execute it to setup the FDW connection.
```bash ```bash
#!/bin/bash #!/bin/bash
# Secondary Database connection params: # Secondary Database connection params:
DB_HOST="/var/opt/gitlab/postgresql" DB_HOST="/var/opt/gitlab/postgresql" # change to the public IP or VPC private IP if its an external server
DB_NAME="gitlabhq_production" DB_NAME="gitlabhq_production"
DB_USER="gitlab" DB_USER="gitlab"
DB_PORT="5432" DB_PORT="5432"
# Tracking Database connection params: # Tracking Database connection params:
GEO_DB_HOST="/var/opt/gitlab/geo-postgresql" GEO_DB_HOST="/var/opt/gitlab/geo-postgresql" # change to the public IP or VPC private IP if its an external server
GEO_DB_NAME="gitlabhq_geo_production" GEO_DB_NAME="gitlabhq_geo_production"
GEO_DB_USER="gitlab_geo" GEO_DB_USER="gitlab_geo"
GEO_DB_PORT="5432" GEO_DB_PORT="5432"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE EXTENSION postgres_fdw;" query_exec () {
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$(DB_HOST)', dbname '$(DB_NAME)', port '$(DB_PORT)' );" gitlab-psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "${1}"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE USER MAPPING FOR $(GEO_DB_USER) SERVER gitlab_secondary OPTIONS (user '$(DB_USER)');" }
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "CREATE SCHEMA gitlab_secondary;"
sudo -u postgres psql -h $GEO_DB_HOST -d $GEO_DB_NAME -p $GEO_DB_PORT -c "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO $(GEO_DB_USER);" query_exec "CREATE EXTENSION postgres_fdw;"
query_exec "CREATE SERVER gitlab_secondary FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '${DB_HOST}', dbname '${DB_NAME}', port '${DB_PORT}');"
query_exec "CREATE USER MAPPING FOR ${GEO_DB_USER} SERVER gitlab_secondary OPTIONS (user '${DB_USER}');"
query_exec "CREATE SCHEMA gitlab_secondary;"
query_exec "GRANT USAGE ON FOREIGN SERVER gitlab_secondary TO ${GEO_DB_USER};"
``` ```
And edit the content of `database_geo.yml` and to add `fdw: true` to And edit the content of `database_geo.yml` and to add `fdw: true` to
......
...@@ -508,7 +508,7 @@ import Vue from 'vue'; ...@@ -508,7 +508,7 @@ import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import * as mutations from './mutations'; import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -527,7 +527,7 @@ _Note:_ If the state of the application is too complex, an individual file for t ...@@ -527,7 +527,7 @@ _Note:_ If the state of the application is too complex, an individual file for t
An action commits a mutatation. In this file, we will write the actions that will call the respective mutation: An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
```javascript ```javascript
import * as types from './mutation-types' import * as types from './mutation_types';
export const addUser = ({ commit }, user) => { export const addUser = ({ commit }, user) => {
commit(types.ADD_USER, user); commit(types.ADD_USER, user);
...@@ -577,7 +577,8 @@ import { mapGetters } from 'vuex'; ...@@ -577,7 +577,8 @@ import { mapGetters } from 'vuex';
The only way to actually change state in a Vuex store is by committing a mutation. The only way to actually change state in a Vuex store is by committing a mutation.
```javascript ```javascript
import * as types from './mutation-types' import * as types from './mutation_types';
export default { export default {
[types.ADD_USER](state, user) { [types.ADD_USER](state, user) {
state.users.push(user); state.users.push(user);
...@@ -686,4 +687,3 @@ describe('component', () => { ...@@ -686,4 +687,3 @@ describe('component', () => {
[vuex-testing]: https://vuex.vuejs.org/en/testing.html [vuex-testing]: https://vuex.vuejs.org/en/testing.html
[axios]: https://github.com/axios/axios [axios]: https://github.com/axios/axios
[axios-interceptors]: https://github.com/axios/axios#interceptors [axios-interceptors]: https://github.com/axios/axios#interceptors
<script> <script>
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import relatedIssuesRoot from 'ee/issuable/related_issues/components/related_issues_root.vue'; import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import issuableAppEventHub from '~/issue_show/event_hub'; import issuableAppEventHub from '~/issue_show/event_hub';
import epicHeader from './epic_header.vue'; import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue'; import epicSidebar from '../../sidebar/components/sidebar_app.vue';
......
import Vue from 'vue'; import Vue from 'vue';
import EpicShowApp from './components/epic_show_app.vue'; import EpicShowApp from './components/epic_show_app.vue';
document.addEventListener('DOMContentLoaded', () => { export default () => {
const el = document.querySelector('#epic-show-app'); const el = document.querySelector('#epic-show-app');
const metaData = JSON.parse(el.dataset.meta); const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial); const initialData = JSON.parse(el.dataset.initial);
...@@ -21,4 +21,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -21,4 +21,4 @@ document.addEventListener('DOMContentLoaded', () => {
props, props,
}), }),
}); });
}); };
import Vue from 'vue'; import Vue from 'vue';
import NewEpicApp from './components/new_epic.vue'; import NewEpicApp from './components/new_epic.vue';
document.addEventListener('DOMContentLoaded', () => { export default () => {
const el = document.querySelector('#new-epic-app'); const el = document.querySelector('#new-epic-app');
const props = el.dataset;
return new Vue({ if (el) {
el, const props = el.dataset;
components: {
'new-epic-app': NewEpicApp, new Vue({ // eslint-disable-line no-new
}, el,
render: createElement => createElement('new-epic-app', { components: {
props, 'new-epic-app': NewEpicApp,
}), },
}); render: createElement => createElement('new-epic-app', {
}); props,
}),
});
}
};
/* eslint-disable no-new */
$(() => {
class ExportCSVModal {
constructor() {
this.$modal = $('.issues-export-modal');
this.$downloadBtn = $('.csv_download_link');
this.$closeBtn = $('.modal-header .close');
this.init();
}
init() {
this.$modal.modal({ show: false });
this.$downloadBtn.on('click', () => this.$modal.modal('show'));
this.$closeBtn.on('click', () => this.$modal.modal('hide'));
}
}
new ExportCSVModal();
});
import initFilteredSearch from '~/pages/search/init_filtered_search'; import initFilteredSearch from '~/pages/search/init_filtered_search';
import FilteredSearchTokenKeysEpics from 'ee/filtered_search/filtered_search_token_keys_epics'; import FilteredSearchTokenKeysEpics from 'ee/filtered_search/filtered_search_token_keys_epics';
import initNewEpic from 'ee/epics/new_epic/new_epic_bundle';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({ initFilteredSearch({
...@@ -7,4 +8,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -7,4 +8,5 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: FilteredSearchTokenKeysEpics, filteredSearchTokenKeys: FilteredSearchTokenKeysEpics,
stateFiltersSelector: '.epics-state-filters', stateFiltersSelector: '.epics-state-filters',
}); });
initNewEpic();
}); });
import ZenMode from '~/zen_mode'; import ZenMode from '~/zen_mode';
import initEpicShow from 'ee/epics/epic_show/epic_show_bundle';
document.addEventListener('DOMContentLoaded', () => new ZenMode()); document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
initEpicShow();
});
import initNewEpic from 'ee/epics/new_epic/new_epic_bundle';
import initRoadmap from 'ee/roadmap/index';
document.addEventListener('DOMContentLoaded', () => {
initNewEpic();
initRoadmap();
});
export default function initExportCSVModal() {
const $modal = $('.issues-export-modal');
const $downloadBtn = $('.csv_download_link');
const $closeBtn = $('.modal-header .close');
$modal.modal({ show: false });
$downloadBtn.on('click', () => $modal.modal('show'));
$closeBtn.on('click', () => $modal.modal('hide'));
}
import '~/pages/projects/issues/index/index';
import initExportCSVModal from './export_csv_modal';
document.addEventListener('DOMContentLoaded', initExportCSVModal);
import initShow from '~/pages/projects/issues/show'; import initShow from '~/pages/projects/issues/show';
import initSidebarBundle from 'ee/sidebar/sidebar_bundle'; import initSidebarBundle from 'ee/sidebar/sidebar_bundle';
import initRelatedIssues from 'ee/related_issues';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initShow(); initShow();
initSidebarBundle(); initSidebarBundle();
initRelatedIssues();
}); });
...@@ -3,24 +3,37 @@ import UsersSelect from '~/users_select'; ...@@ -3,24 +3,37 @@ import UsersSelect from '~/users_select';
import UserCallout from '~/user_callout'; import UserCallout from '~/user_callout';
import initSettingsPanels from '~/settings_panels'; import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys'; import initDeployKeys from '~/deploy_keys';
import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create'; import CEProtectedBranchCreate from '~/protected_branches/protected_branch_create';
import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list'; import CEProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
import CEProtectedTagCreate from '~/protected_tags/protected_tag_create'; import CEProtectedTagCreate from '~/protected_tags/protected_tag_create';
import CEProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import CEProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import MirrorPull from 'ee/mirrors/mirror_pull'; import MirrorPull from 'ee/mirrors/mirror_pull';
import ProtectedBranchCreate from 'ee/protected_branches/protected_branch_create';
import ProtectedBranchEditList from 'ee/protected_branches/protected_branch_edit_list';
import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create';
import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); new UsersSelect();
new UserCallout(); new UserCallout();
initDeployKeys();
initSettingsPanels();
if (document.querySelector('.js-protected-refs-for-users')) { if (document.querySelector('.js-protected-refs-for-users')) {
new ProtectedBranchCreate();
new ProtectedBranchEditList();
new ProtectedTagCreate(); new ProtectedTagCreate();
new ProtectedTagEditList(); new ProtectedTagEditList();
} else { } else {
new CEProtectedBranchCreate();
new CEProtectedBranchEditList();
new CEProtectedTagCreate(); new CEProtectedTagCreate();
new CEProtectedTagEditList(); new CEProtectedTagEditList();
} }
initDeployKeys();
initSettingsPanels();
const mirrorPull = new MirrorPull('.js-project-mirror-push-form'); const mirrorPull = new MirrorPull('.js-project-mirror-push-form');
......
import Vue from 'vue'; import Vue from 'vue';
import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import RelatedIssuesRoot from './related_issues/components/related_issues_root.vue'; import RelatedIssuesRoot from './components/related_issues_root.vue';
document.addEventListener('DOMContentLoaded', () => { export default function initRelatedIssues() {
const relatedIssuesRootElement = document.querySelector('.js-related-issues-root'); const relatedIssuesRootElement = document.querySelector('.js-related-issues-root');
if (relatedIssuesRootElement) { if (relatedIssuesRootElement) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
...@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => {
}), }),
}); });
} }
}); }
...@@ -13,7 +13,7 @@ import roadmapApp from './components/app.vue'; ...@@ -13,7 +13,7 @@ import roadmapApp from './components/app.vue';
Vue.use(Translate); Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { export default () => {
const el = document.getElementById('js-roadmap'); const el = document.getElementById('js-roadmap');
if (!el) { if (!el) {
...@@ -57,4 +57,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -57,4 +57,4 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
}, },
}); });
}); };
...@@ -25,7 +25,10 @@ function mountWeightComponent(mediator) { ...@@ -25,7 +25,10 @@ function mountWeightComponent(mediator) {
function mountEpic() { function mountEpic() {
const el = document.querySelector('#js-vue-sidebar-item-epic'); const el = document.querySelector('#js-vue-sidebar-item-epic');
return new Vue({ if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el, el,
components: { components: {
SidebarItemEpic, SidebarItemEpic,
......
...@@ -4,8 +4,6 @@ ...@@ -4,8 +4,6 @@
= render 'shared/issuable/epic_nav', type: :epics = render 'shared/issuable/epic_nav', type: :epics
.nav-controls .nav-controls
- if can?(current_user, :create_epic, @group) - if can?(current_user, :create_epic, @group)
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'new_epic'
#new-epic-app{ data: { endpoint: request.url, 'align-right' => true } } #new-epic-app{ data: { endpoint: request.url, 'align-right' => true } }
= render 'shared/epic/search_bar', type: :epics = render 'shared/epic/search_bar', type: :epics
......
...@@ -14,6 +14,5 @@ ...@@ -14,6 +14,5 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'epic_show'
#epic-show-app{ data: { initial: issuable_initial_data(@epic).to_json, meta: epic_meta_data } } #epic-show-app{ data: { initial: issuable_initial_data(@epic).to_json, meta: epic_meta_data } }
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
- if @epics_count != 0 - if @epics_count != 0
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'roadmap'
#js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, empty_state_illustration: image_path('illustrations/epics/roadmap.svg') } } #js-roadmap{ data: { epics_path: group_epics_path(@group, format: :json), group_id: @group.id, empty_state_illustration: image_path('illustrations/epics/roadmap.svg') } }
- else - else
= render 'shared/empty_states/roadmap' = render 'shared/empty_states/roadmap'
...@@ -3,10 +3,6 @@ ...@@ -3,10 +3,6 @@
- page_title "Service Desk" - page_title "Service Desk"
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'issues'
- content_for :breadcrumbs_extra do - content_for :breadcrumbs_extra do
= render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false = render "projects/issues/nav_btns", show_export_button: false, show_rss_button: false
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'ee_protected_branches'
- content_for :create_protected_branch do - content_for :create_protected_branch do
= render 'projects/protected_branches/ee/create_protected_branch' = render 'projects/protected_branches/ee/create_protected_branch'
......
...@@ -11,5 +11,4 @@ ...@@ -11,5 +11,4 @@
- if can?(current_user, :create_epic, @group) - if can?(current_user, :create_epic, @group)
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'new_epic'
#new-epic-app{ data: { endpoint: request.url } } #new-epic-app{ data: { endpoint: request.url } }
...@@ -11,7 +11,6 @@ ...@@ -11,7 +11,6 @@
- if can?(current_user, :create_epic, @group) - if can?(current_user, :create_epic, @group)
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'new_epic'
#new-epic-app{ data: { endpoint: request.url } } #new-epic-app{ data: { endpoint: request.url } }
= link_to group_epics_path(@group), title: 'List', class: 'btn' do = link_to group_epics_path(@group), title: 'List', class: 'btn' do
%span= _('View epics list') %span= _('View epics list')
...@@ -113,6 +113,18 @@ describe Groups::EpicsController do ...@@ -113,6 +113,18 @@ describe Groups::EpicsController do
expect(item['end_date']).to eq(epic.end_date) expect(item['end_date']).to eq(epic.end_date)
expect(item['web_url']).to eq(group_epic_path(group, epic)) expect(item['web_url']).to eq(group_epic_path(group, epic))
end end
context 'using label_name filter' do
let(:label) { create(:label) }
let!(:labeled_epic) { create(:labeled_epic, group: group, labels: [label]) }
it 'returns all epics with given label' do
get :index, group_id: group, label_name: label.title, format: :json
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(labeled_epic.id)
end
end
end end
end end
......
...@@ -8,13 +8,14 @@ module Gitlab ...@@ -8,13 +8,14 @@ module Gitlab
private private
def base_query def base_query
@base_query ||= stage_query @base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
def stage_query def stage_query(project_ids)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables .project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests # Load merge_requests
......
...@@ -21,17 +21,28 @@ module Gitlab ...@@ -21,17 +21,28 @@ module Gitlab
end end
def median def median
cte_table = Arel::Table.new("cte_table_for_#{name}") BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
cte_table = Arel::Table.new("cte_table_for_#{name}")
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current # We compute the (end_time - start_time) interval, and give it an alias based on the current
# cycle analytics stage. # cycle analytics stage.
interval_query = Arel::Nodes::As.new( interval_query = Arel::Nodes::As.new(cte_table,
cte_table, subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
median_datetime(cte_table, interval_query, name) if project_ids.one?
loader.call(@project.id, median_datetime(cte_table, interval_query, name))
else
begin
median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
end
rescue NotSupportedError
{}
end
end
end
end end
def name def name
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
module ProductionHelper module ProductionHelper
def stage_query def stage_query(project_ids)
super super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at] .where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables .gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
......
...@@ -25,11 +25,11 @@ module Gitlab ...@@ -25,11 +25,11 @@ module Gitlab
_("Total test time for all commits/merges") _("Total test time for all commits/merges")
end end
def stage_query def stage_query(project_ids)
if @options[:branch] if @options[:branch]
super.where(build_table[:ref].eq(@options[:branch])) super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else else
super super(project_ids)
end end
end end
end end
......
module Gitlab
module CycleAnalytics
class UsageData
PROJECTS_LIMIT = 10
attr_reader :projects, :options
def initialize
@projects = Project.sorted_by_activity.limit(PROJECTS_LIMIT)
@options = { from: 7.days.ago }
end
def to_json
total = 0
values =
medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
calculations = stage_values(medians)
total += calculations.values.compact.sum
hsh[stage_name] = calculations
end
values[:total] = total
{ avg_cycle_analytics: values }
end
private
def medians_per_stage
projects.each_with_object({}) do |project, hsh|
::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end
end
end
def stage_values(medians)
medians = medians.map(&:presence).compact
average = calc_average(medians)
{
average: average,
sd: standard_deviation(medians, average),
missing: projects.length - medians.length
}
end
def calc_average(values)
return if values.empty?
(values.sum / values.length).to_i
end
def standard_deviation(values, average)
Math.sqrt(sample_variance(values, average)).to_i
end
def sample_variance(values, average)
return 0 if values.length <= 1
sum = values.inject(0) do |acc, val|
acc + (val - average)**2
end
sum / (values.length - 1)
end
end
end
end
...@@ -2,18 +2,14 @@ ...@@ -2,18 +2,14 @@
module Gitlab module Gitlab
module Database module Database
module Median module Median
NotSupportedError = Class.new(StandardError)
def median_datetime(arel_table, query_so_far, column_sym) def median_datetime(arel_table, query_so_far, column_sym)
median_queries = extract_median(execute_queries(arel_table, query_so_far, column_sym)).presence
if Gitlab::Database.postgresql? end
pg_median_datetime_sql(arel_table, query_so_far, column_sym)
elsif Gitlab::Database.mysql? def median_datetimes(arel_table, query_so_far, column_sym, partition_column)
mysql_median_datetime_sql(arel_table, query_so_far, column_sym) extract_medians(execute_queries(arel_table, query_so_far, column_sym, partition_column)).presence
end
results = Array.wrap(median_queries).map do |query|
ActiveRecord::Base.connection.execute(query)
end
extract_median(results).presence
end end
def extract_median(results) def extract_median(results)
...@@ -21,13 +17,21 @@ module Gitlab ...@@ -21,13 +17,21 @@ module Gitlab
if Gitlab::Database.postgresql? if Gitlab::Database.postgresql?
result = result.first.presence result = result.first.presence
median = result['median'] if result
median.to_f if median result['median']&.to_f if result
elsif Gitlab::Database.mysql? elsif Gitlab::Database.mysql?
result.to_a.flatten.first result.to_a.flatten.first
end end
end end
def extract_medians(results)
median_values = results.compact.first.values
median_values.each_with_object({}) do |(id, median), hash|
hash[id.to_i] = median&.to_f
end
end
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym) def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
query = arel_table query = arel_table
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)) .from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
...@@ -53,7 +57,7 @@ module Gitlab ...@@ -53,7 +57,7 @@ module Gitlab
] ]
end end
def pg_median_datetime_sql(arel_table, query_so_far, column_sym) def pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column = nil)
# Create a CTE with the column we're operating on, row number (after sorting by the column # Create a CTE with the column we're operating on, row number (after sorting by the column
# we're operating on), and count of the table we're operating on (duplicated across) all rows # we're operating on), and count of the table we're operating on (duplicated across) all rows
# of the CTE. For example, if we're looking to find the median of the `projects.star_count` # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
...@@ -64,41 +68,107 @@ module Gitlab ...@@ -64,41 +68,107 @@ module Gitlab
# 5 | 1 | 3 # 5 | 1 | 3
# 9 | 2 | 3 # 9 | 2 | 3
# 15 | 3 | 3 # 15 | 3 | 3
#
# If a partition column is used we will do the same operation but for separate partitions,
# when that happens the CTE might look like this:
#
# project_id | star_count | row_id | ct
# ------------+------------+--------+----
# 1 | 5 | 1 | 2
# 1 | 9 | 2 | 2
# 2 | 10 | 1 | 3
# 2 | 15 | 2 | 3
# 2 | 20 | 3 | 3
cte_table = Arel::Table.new("ordered_records") cte_table = Arel::Table.new("ordered_records")
cte = Arel::Nodes::As.new( cte = Arel::Nodes::As.new(
cte_table, cte_table,
arel_table arel_table.project(*rank_rows(arel_table, column_sym, partition_column)).
.project(
arel_table[column_sym].as(column_sym.to_s),
Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
arel_table.project("COUNT(1)").as('ct')).
# Disallow negative values # Disallow negative values
where(arel_table[column_sym].gteq(zero_interval))) where(arel_table[column_sym].gteq(zero_interval)))
# From the CTE, select either the middle row or the middle two rows (this is accomplished # From the CTE, select either the middle row or the middle two rows (this is accomplished
# by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
# selected rows, and this is the median value. # selected rows, and this is the median value.
cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")) result =
.where( cte_table
Arel::Nodes::Between.new( .project(*median_projections(cte_table, column_sym, partition_column))
cte_table[:row_id], .where(
Arel::Nodes::And.new( Arel::Nodes::Between.new(
[(cte_table[:ct] / Arel.sql('2.0')), cte_table[:row_id],
(cte_table[:ct] / Arel.sql('2.0') + 1)] Arel::Nodes::And.new(
[(cte_table[:ct] / Arel.sql('2.0')),
(cte_table[:ct] / Arel.sql('2.0') + 1)]
)
) )
) )
) .with(query_so_far, cte)
.with(query_so_far, cte)
.to_sql result.group(cte_table[partition_column]).order(cte_table[partition_column]) if partition_column
result.to_sql
end end
private private
def median_queries(arel_table, query_so_far, column_sym, partition_column = nil)
if Gitlab::Database.postgresql?
pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
elsif Gitlab::Database.mysql?
raise NotSupportedError, "partition_column is not supported for MySQL" if partition_column
mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
end
end
def execute_queries(arel_table, query_so_far, column_sym, partition_column = nil)
queries = median_queries(arel_table, query_so_far, column_sym, partition_column)
Array.wrap(queries).map { |query| ActiveRecord::Base.connection.execute(query) }
end
def average(args, as) def average(args, as)
Arel::Nodes::NamedFunction.new("AVG", args, as) Arel::Nodes::NamedFunction.new("AVG", args, as)
end end
def rank_rows(arel_table, column_sym, partition_column)
column_row = arel_table[column_sym].as(column_sym.to_s)
if partition_column
partition_row = arel_table[partition_column]
row_id =
Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('rank', []),
Arel::Nodes::Window.new.partition(arel_table[partition_column])
.order(arel_table[column_sym])
).as('row_id')
count = arel_table.from(arel_table.alias)
.project('COUNT(*)')
.where(arel_table[partition_column].eq(arel_table.alias[partition_column]))
.as('ct')
[partition_row, column_row, row_id, count]
else
row_id =
Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('row_number', []),
Arel::Nodes::Window.new.order(arel_table[column_sym])
).as('row_id')
count = arel_table.project("COUNT(1)").as('ct')
[column_row, row_id, count]
end
end
def median_projections(table, column_sym, partition_column)
projections = []
projections << table[partition_column] if partition_column
projections << average([extract_epoch(table[column_sym])], "median")
projections
end
def extract_epoch(arel_attribute) def extract_epoch(arel_attribute)
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end end
......
...@@ -1042,6 +1042,21 @@ module Gitlab ...@@ -1042,6 +1042,21 @@ module Gitlab
end end
end end
def license_short_name
gitaly_migrate(:license_short_name) do |is_enabled|
if is_enabled
gitaly_repository_client.license_short_name
else
begin
# The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
Licensee.license(path).try(:key)
rescue Rugged::Error
end
end
end
end
def with_repo_branch_commit(start_repository, start_branch_name) def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository) Gitlab::Git.check_namespace!(start_repository)
start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository) start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
......
...@@ -249,6 +249,14 @@ module Gitlab ...@@ -249,6 +249,14 @@ module Gitlab
raise Gitlab::Git::OSError.new(response.error) unless response.error.empty? raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
end end
def license_short_name
request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)
response = GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
response.license_short_name.presence
end
end end
end end
end end
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.test_env = Rails.env.test?
if current_user if current_user
gon.current_user_id = current_user.id gon.current_user_id = current_user.id
......
...@@ -9,6 +9,7 @@ module Gitlab ...@@ -9,6 +9,7 @@ module Gitlab
license_usage_data.merge(system_usage_data) license_usage_data.merge(system_usage_data)
.merge(features_usage_data) .merge(features_usage_data)
.merge(components_usage_data) .merge(components_usage_data)
.merge(cycle_analytics_usage_data)
end end
def to_json(force_refresh: false) def to_json(force_refresh: false)
...@@ -108,6 +109,10 @@ module Gitlab ...@@ -108,6 +109,10 @@ module Gitlab
} }
end end
def cycle_analytics_usage_data
Gitlab::CycleAnalytics::UsageData.new.to_json
end
def features_usage_data def features_usage_data
features_usage_data_ce.merge(features_usage_data_ee) features_usage_data_ce.merge(features_usage_data_ee)
end end
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-02-23 13:23+0100\n" "POT-Creation-Date: 2018-03-01 22:35+0100\n"
"PO-Revision-Date: 2018-02-23 13:23+0100\n" "PO-Revision-Date: 2018-03-01 22:35+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -51,6 +51,9 @@ msgid_plural "%s additional commits have been omitted to prevent performance iss ...@@ -51,6 +51,9 @@ msgid_plural "%s additional commits have been omitted to prevent performance iss
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}" msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr "" msgstr ""
...@@ -71,6 +74,9 @@ msgstr "" ...@@ -71,6 +74,9 @@ msgstr ""
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
msgstr "" msgstr ""
msgid "%{openOrClose} %{noteable}"
msgstr ""
msgid "%{storage_name}: failed storage access attempt on host:" msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] "" msgstr[0] ""
...@@ -758,6 +764,9 @@ msgstr "" ...@@ -758,6 +764,9 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api" msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr "" msgstr ""
msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
msgstr ""
msgid "Click to expand text" msgid "Click to expand text"
msgstr "" msgstr ""
...@@ -911,9 +920,6 @@ msgstr "" ...@@ -911,9 +920,6 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about Kubernetes"
msgstr ""
msgid "ClusterIntegration|Learn more about environments" msgid "ClusterIntegration|Learn more about environments"
msgstr "" msgstr ""
...@@ -1049,6 +1055,12 @@ msgstr "" ...@@ -1049,6 +1055,12 @@ msgstr ""
msgid "Collapse" msgid "Collapse"
msgstr "" msgstr ""
msgid "Comment and resolve discussion"
msgstr ""
msgid "Comment and unresolve discussion"
msgstr ""
msgid "Comments" msgid "Comments"
msgstr "" msgstr ""
...@@ -1074,6 +1086,9 @@ msgstr "" ...@@ -1074,6 +1086,9 @@ msgstr ""
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}" msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr "" msgstr ""
msgid "Commit to %{branchName} branch"
msgstr ""
msgid "CommitBoxTitle|Commit" msgid "CommitBoxTitle|Commit"
msgstr "" msgstr ""
...@@ -1230,6 +1245,12 @@ msgstr "" ...@@ -1230,6 +1245,12 @@ msgstr ""
msgid "Create New Directory" msgid "Create New Directory"
msgstr "" msgstr ""
msgid "Create a new branch"
msgstr ""
msgid "Create a new branch and merge request"
msgstr ""
msgid "Create a personal access token on your account to pull or push via %{protocol}." msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "" msgstr ""
...@@ -1281,6 +1302,12 @@ msgstr "" ...@@ -1281,6 +1302,12 @@ msgstr ""
msgid "CreateTokenToCloneLink|create a personal access token" msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "" msgstr ""
msgid "Creates a new branch from %{branchName}"
msgstr ""
msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
msgstr ""
msgid "Creating epic" msgid "Creating epic"
msgstr "" msgstr ""
...@@ -1370,6 +1397,9 @@ msgstr "" ...@@ -1370,6 +1397,9 @@ msgstr ""
msgid "Disable" msgid "Disable"
msgstr "" msgstr ""
msgid "Discard draft"
msgstr ""
msgid "Discover GitLab Geo." msgid "Discover GitLab Geo."
msgstr "" msgstr ""
...@@ -1490,6 +1520,12 @@ msgstr "" ...@@ -1490,6 +1520,12 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort" msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "" msgstr ""
msgid "Error checking branch data. Please try again."
msgstr ""
msgid "Error committing changes. Please try again."
msgstr ""
msgid "Error creating epic" msgid "Error creating epic"
msgstr "" msgstr ""
...@@ -1639,6 +1675,9 @@ msgstr "" ...@@ -1639,6 +1675,9 @@ msgstr ""
msgid "From merge request merge until deploy to production" msgid "From merge request merge until deploy to production"
msgstr "" msgstr ""
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr ""
msgid "GPG Keys" msgid "GPG Keys"
msgstr "" msgstr ""
...@@ -1917,6 +1956,9 @@ msgstr "" ...@@ -1917,6 +1956,9 @@ msgstr ""
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition." msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr "" msgstr ""
msgid "Install Runner on Kubernetes"
msgstr ""
msgid "Install a Runner compatible with GitLab CI" msgid "Install a Runner compatible with GitLab CI"
msgstr "" msgstr ""
...@@ -2050,6 +2092,9 @@ msgstr "" ...@@ -2050,6 +2092,9 @@ msgstr ""
msgid "Learn more" msgid "Learn more"
msgstr "" msgstr ""
msgid "Learn more about Kubernetes"
msgstr ""
msgid "Learn more about protected branches" msgid "Learn more about protected branches"
msgstr "" msgstr ""
...@@ -2128,9 +2173,6 @@ msgstr "" ...@@ -2128,9 +2173,6 @@ msgstr ""
msgid "Members" msgid "Members"
msgstr "" msgstr ""
msgid "Merge Request"
msgstr ""
msgid "Merge Requests" msgid "Merge Requests"
msgstr "" msgstr ""
...@@ -2877,6 +2919,9 @@ msgstr "" ...@@ -2877,6 +2919,9 @@ msgstr ""
msgid "Reset runners registration token" msgid "Reset runners registration token"
msgstr "" msgstr ""
msgid "Resolve discussion"
msgstr ""
msgid "Reveal value" msgid "Reveal value"
msgid_plural "Reveal values" msgid_plural "Reveal values"
msgstr[0] "" msgstr[0] ""
...@@ -2945,6 +2990,9 @@ msgstr "" ...@@ -2945,6 +2990,9 @@ msgstr ""
msgid "Select a timezone" msgid "Select a timezone"
msgstr "" msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
msgid "Select assignee" msgid "Select assignee"
msgstr "" msgstr ""
...@@ -2987,6 +3035,9 @@ msgstr "" ...@@ -2987,6 +3035,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "Setup a specific Runner automatically"
msgstr ""
msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero." msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
msgstr "" msgstr ""
...@@ -3040,7 +3091,7 @@ msgstr "" ...@@ -3040,7 +3091,7 @@ msgstr ""
msgid "Something went wrong when toggling the button" msgid "Something went wrong when toggling the button"
msgstr "" msgstr ""
msgid "Something went wrong while closing the issue. Please try again later" msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr "" msgstr ""
msgid "Something went wrong while fetching SAST." msgid "Something went wrong while fetching SAST."
...@@ -3052,7 +3103,10 @@ msgstr "" ...@@ -3052,7 +3103,10 @@ msgstr ""
msgid "Something went wrong while fetching the registry list." msgid "Something went wrong while fetching the registry list."
msgstr "" msgstr ""
msgid "Something went wrong while reopening the issue. Please try again later" msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while resolving this discussion. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong. Please try again." msgid "Something went wrong. Please try again."
...@@ -3670,6 +3724,9 @@ msgstr "" ...@@ -3670,6 +3724,9 @@ msgstr ""
msgid "Unlocked" msgid "Unlocked"
msgstr "" msgstr ""
msgid "Unresolve discussion"
msgstr ""
msgid "Unstar" msgid "Unstar"
msgstr "" msgstr ""
...@@ -3754,6 +3811,9 @@ msgstr "" ...@@ -3754,6 +3811,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot." msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "" msgstr ""
msgid "Web IDE"
msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group." msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr "" msgstr ""
...@@ -3874,6 +3934,9 @@ msgstr "" ...@@ -3874,6 +3934,9 @@ msgstr ""
msgid "Withdraw Access Request" msgid "Withdraw Access Request"
msgstr "" msgstr ""
msgid "Write a commit message..."
msgstr ""
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?" msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "" msgstr ""
...@@ -3892,6 +3955,9 @@ msgstr "" ...@@ -3892,6 +3955,9 @@ msgstr ""
msgid "You can also star a label to make it a priority label." msgid "You can also star a label to make it a priority label."
msgstr "" msgstr ""
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr ""
msgid "You can move around the graph by using the arrow keys." msgid "You can move around the graph by using the arrow keys."
msgstr "" msgstr ""
...@@ -3958,6 +4024,9 @@ msgstr "" ...@@ -3958,6 +4024,9 @@ msgstr ""
msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure" msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
msgstr "" msgstr ""
msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
msgstr ""
msgid "Your comment will not be visible to the public." msgid "Your comment will not be visible to the public."
msgstr "" msgstr ""
...@@ -4073,6 +4142,9 @@ msgstr[1] "" ...@@ -4073,6 +4142,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
msgid "mrWidget|Add approval"
msgstr ""
msgid "mrWidget|An error occured while removing your approval." msgid "mrWidget|An error occured while removing your approval."
msgstr "" msgstr ""
...@@ -4085,9 +4157,6 @@ msgstr "" ...@@ -4085,9 +4157,6 @@ msgstr ""
msgid "mrWidget|Approve" msgid "mrWidget|Approve"
msgstr "" msgstr ""
msgid "mrWidget|Add approval"
msgstr ""
msgid "mrWidget|Approved by" msgid "mrWidget|Approved by"
msgstr "" msgstr ""
...@@ -4266,3 +4335,6 @@ msgstr "" ...@@ -4266,3 +4335,6 @@ msgstr ""
msgid "uses Kubernetes clusters to deploy your code!" msgid "uses Kubernetes clusters to deploy your code!"
msgstr "" msgstr ""
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
require 'spec_helper' require 'spec_helper'
describe Groups::LabelsController do describe Groups::LabelsController do
let(:group) { create(:group) } set(:group) { create(:group) }
let(:user) { create(:user) } set(:user) { create(:user) }
set(:project) { create(:project, namespace: group) }
before do before do
group.add_owner(user) group.add_owner(user)
...@@ -10,6 +11,34 @@ describe Groups::LabelsController do ...@@ -10,6 +11,34 @@ describe Groups::LabelsController do
sign_in(user) sign_in(user)
end end
describe 'GET #index' do
set(:label_1) { create(:label, project: project, title: 'label_1') }
set(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') }
it 'returns group and project labels by default' do
get :index, group_id: group, format: :json
label_ids = json_response.map {|label| label['title']}
expect(label_ids).to match_array([label_1.title, group_label_1.title])
end
context 'with ancestor group', :nested_groups do
set(:subgroup) { create(:group, parent: group) }
set(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') }
before do
subgroup.add_owner(user)
end
it 'returns ancestor group labels', :nested_groups do
get :index, group_id: subgroup, include_ancestor_groups: true, only_group_labels: true, format: :json
label_ids = json_response.map {|label| label['title']}
expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
end
end
end
describe 'POST #toggle_subscription' do describe 'POST #toggle_subscription' do
it 'allows user to toggle subscription on group labels' do it 'allows user to toggle subscription on group labels' do
label = create(:group_label, group: group) label = create(:group_label, group: group)
......
...@@ -91,6 +91,12 @@ describe Projects::ClustersController do ...@@ -91,6 +91,12 @@ describe Projects::ClustersController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('cluster_status') expect(response).to match_response_schema('cluster_status')
end end
it 'invokes schedule_status_update on each application' do
expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
go
end
end end
describe 'security' do describe 'security' do
......
...@@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do ...@@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do
milestone = create(:milestone, project: project, created_at: 5.days.ago) milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone) issue.update(milestone: milestone)
create_merge_request_closing_issue(issue) create_merge_request_closing_issue(user, project, issue)
end end
it 'is false' do it 'is false' do
......
...@@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do ...@@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") } let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
context 'as an allowed user' do context 'as an allowed user' do
...@@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do ...@@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
project.add_master(user) project.add_master(user)
create_cycle @build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master deploy_master(user, project)
sign_in(user) sign_in(user)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
...@@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do ...@@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do
project.add_guest(guest) project.add_guest(guest)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master deploy_master(user, project)
sign_in(guest) sign_in(guest)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
...@@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do ...@@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do
expect(find('.stage-events')).to have_content("!#{mr.iid}") expect(find('.stage-events')).to have_content("!#{mr.iid}")
end end
def create_cycle
issue.update(milestone: milestone)
pipeline.run
@build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
def click_stage(stage_name) def click_stage(stage_name)
find('.stage-nav li', text: stage_name).click find('.stage-nav li', text: stage_name).click
wait_for_requests wait_for_requests
......
...@@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do ...@@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do
scenario 'user is unable to install applications' do scenario 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end end
end end
end end
...@@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do ...@@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do
scenario 'user can install applications' do scenario 'user can install applications' do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end end
end end
context 'when user installs Helm' do context 'when user installs Helm' do
before do before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) allow(ClusterInstallAppWorker).to receive(:perform_async)
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click page.find(:css, '.js-cluster-application-install-button').click
...@@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do ...@@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do
page.within('.js-cluster-application-row-helm') do page.within('.js-cluster-application-row-helm') do
# FE sends request and gets the response, then the buttons is "Install" # FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_helm.make_installing! Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing" # FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
Clusters::Cluster.last.application_helm.make_installed! Clusters::Cluster.last.application_helm.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
end end
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster') expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
...@@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do ...@@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do
context 'when user installs Ingress' do context 'when user installs Ingress' do
context 'when user installs application: Ingress' do context 'when user installs application: Ingress' do
before do before do
allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil) allow(ClusterInstallAppWorker).to receive(:perform_async)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
create(:clusters_applications_helm, :installed, cluster: cluster) create(:clusters_applications_helm, :installed, cluster: cluster)
page.within('.js-cluster-application-row-ingress') do page.within('.js-cluster-application-row-ingress') do
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
page.find(:css, '.js-cluster-application-install-button').click page.find(:css, '.js-cluster-application-install-button').click
end end
end end
...@@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do ...@@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do
it 'he sees status transition' do it 'he sees status transition' do
page.within('.js-cluster-application-row-ingress') do page.within('.js-cluster-application-row-ingress') do
# FE sends request and gets the response, then the buttons is "Install" # FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_ingress.make_installing! Clusters::Cluster.last.application_ingress.make_installing!
# FE starts polling and update the buttons to "Installing" # FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing') expect(page).to have_css('.js-cluster-application-install-button[disabled]')
# The application becomes installed but we keep waiting for external IP address
Clusters::Cluster.last.application_ingress.make_installed! Clusters::Cluster.last.application_ingress.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed') expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page).to have_selector('.js-no-ip-message')
expect(page.find('.js-ip-address').value).to eq('?')
# We receive the external IP address and display
Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
expect(page).not_to have_selector('.js-no-ip-message')
expect(page.find('.js-ip-address').value).to eq('192.168.1.100')
end end
expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster') expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
......
require 'spec_helper' require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
before do
allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true)
end
def manage_two_factor_authentication def manage_two_factor_authentication
click_on 'Manage two-factor authentication' click_on 'Manage two-factor authentication'
expect(page).to have_content("Setup new U2F device") expect(page).to have_content("Setup new U2F device")
......
...@@ -89,6 +89,25 @@ describe LabelsFinder do ...@@ -89,6 +89,25 @@ describe LabelsFinder do
expect(finder.execute).to eq [private_subgroup_label_1] expect(finder.execute).to eq [private_subgroup_label_1]
end end
end end
context 'when including labels from group descendants', :nested_groups do
it 'returns labels from group and its descendants' do
private_group_1.add_developer(user)
private_subgroup_1.add_developer(user)
finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true)
expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1]
end
it 'ignores labels from groups which user can not read' do
private_subgroup_1.add_developer(user)
finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true)
expect(finder.execute).to eq [private_subgroup_label_1]
end
end
end end
context 'filtering by project_id' do context 'filtering by project_id' do
......
...@@ -30,7 +30,8 @@ ...@@ -30,7 +30,8 @@
] ]
} }
}, },
"status_reason": { "type": ["string", "null"] } "status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] }
}, },
"required" : [ "name", "status" ] "required" : [ "name", "status" ]
} }
......
require 'spec_helper'
describe U2fHelper do
describe 'when not on mobile' do
it 'does not inject u2f on chrome 40' do
device = double(mobile?: false)
browser = double(chrome?: true, opera?: false, version: 40, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
it 'injects u2f on chrome 41' do
device = double(mobile?: false)
browser = double(chrome?: true, opera?: false, version: 41, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq true
end
it 'does not inject u2f on opera 39' do
device = double(mobile?: false)
browser = double(chrome?: false, opera?: true, version: 39, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
it 'injects u2f on opera 40' do
device = double(mobile?: false)
browser = double(chrome?: false, opera?: true, version: 40, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq true
end
end
describe 'when on mobile' do
it 'does not inject u2f on chrome 41' do
device = double(mobile?: true)
browser = double(chrome?: true, opera?: false, version: 41, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
it 'does not inject u2f on opera 40' do
device = double(mobile?: true)
browser = double(chrome?: false, opera?: true, version: 40, device: device)
allow(helper).to receive(:browser).and_return(browser)
expect(helper.inject_u2f_api?).to eq false
end
end
end
...@@ -44,4 +44,71 @@ describe('Applications', () => { ...@@ -44,4 +44,71 @@ describe('Applications', () => {
}); });
/* */ /* */
}); });
describe('Ingress application', () => {
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '0.0.0.0',
},
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
).toEqual('0.0.0.0');
});
});
describe('without ip address', () => {
it('renders an input text with a question mark and an alert text', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
},
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
});
});
describe('before installing', () => {
it('does not render the IP address', () => {
vm = mountComponent(Applications, {
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
},
});
expect(vm.$el.textContent).not.toContain('Ingress IP Address');
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
});
}); });
...@@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = { ...@@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'ingress', name: 'ingress',
status: APPLICATION_ERROR, status: APPLICATION_ERROR,
status_reason: 'Cannot connect', status_reason: 'Cannot connect',
external_ip: null,
}, { }, {
name: 'runner', name: 'runner',
status: APPLICATION_INSTALLING, status: APPLICATION_INSTALLING,
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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