Commit c4e46877 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'master' into pawel/prometheus-business-metrics-ee-2273

parents bc9ca388 f290e2ea
...@@ -426,7 +426,7 @@ group :ed25519 do ...@@ -426,7 +426,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.85.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.87.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -309,7 +309,7 @@ GEM ...@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.85.0) gitaly-proto (0.87.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -1091,7 +1091,7 @@ DEPENDENCIES ...@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.85.0) gitaly-proto (~> 0.87.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
......
...@@ -5,12 +5,12 @@ import Vue from 'vue'; ...@@ -5,12 +5,12 @@ import Vue from 'vue';
import Flash from '~/flash'; import Flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import '~/vue_shared/models/label';
import FilteredSearchBoards from './filtered_search_boards'; import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub'; import eventHub from './eventhub';
import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue'; import './models/issue';
import './models/label';
import './models/list'; import './models/list';
import './models/milestone'; import './models/milestone';
import './models/assignee'; import './models/assignee';
......
...@@ -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>
...@@ -94,12 +99,6 @@ ...@@ -94,12 +99,6 @@
</p> </p>
`; `;
}, },
gitlabRunnerDescription() {
return _.escape(s__(
`ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
and send the results back to GitLab.`,
));
},
prometheusDescription() { prometheusDescription() {
return sprintf( return sprintf(
_.escape(s__( _.escape(s__(
...@@ -136,33 +135,137 @@ ...@@ -136,33 +135,137 @@
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>
<application-row
id="runner"
:title="applications.runner.title"
title-link="https://docs.gitlab.com/runner/"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
>
<div slot="description">
{{ s__(`ClusterIntegration|GitLab Runner connects to this
project's repository and executes CI/CD jobs,
pushing results back and deploying,
applications to production.`) }}
</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;
}
}); });
} }
} }
...@@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager { ...@@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager {
page, page,
isGroup, isGroup,
isGroupAncestor, isGroupAncestor,
isGroupDecendent,
filteredSearchTokenKeys, filteredSearchTokenKeys,
}) { }) {
this.container = FilteredSearchContainer.container; this.container = FilteredSearchContainer.container;
...@@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager { ...@@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager {
this.page = page; this.page = page;
this.groupsOnly = isGroup; this.groupsOnly = isGroup;
this.groupAncestor = isGroupAncestor; this.groupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
this.setupMapping(); this.setupMapping();
......
...@@ -22,11 +22,13 @@ export default class FilteredSearchManager { ...@@ -22,11 +22,13 @@ export default class FilteredSearchManager {
page, page,
isGroup = false, isGroup = false,
isGroupAncestor = false, isGroupAncestor = false,
isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys, filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters', stateFiltersSelector = '.issues-state-filters',
}) { }) {
this.isGroup = isGroup; this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor; this.isGroupAncestor = isGroupAncestor;
this.isGroupDecendent = isGroupDecendent;
this.states = ['opened', 'closed', 'merged', 'all']; this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page; this.page = page;
......
import _ from 'underscore'; import _ from 'underscore';
import AjaxCache from '../lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import { objectToQueryString } from '~/lib/utils/common_utils';
import Flash from '../flash'; import Flash from '../flash';
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache'; import UsersCache from '../lib/utils/users_cache';
...@@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens { ...@@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens {
}; };
} }
/**
* Returns a computed API endpoint
* and query string composed of values from endpointQueryParams
* @param {String} endpoint
* @param {String} endpointQueryParams
*/
static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
if (!endpointQueryParams) {
return endpoint;
}
const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
return `${endpoint}?${queryString}`;
}
static unselectTokens() { static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected')); [].forEach.call(otherTokens, t => t.classList.remove('selected'));
...@@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens { ...@@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) { static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
const labelsEndpoint = `${baseEndpoint}/labels.json`; const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
`${baseEndpoint}/labels.json`,
filteredSearchInput.dataset.endpointQueryParams,
);
return AjaxCache.retrieve(labelsEndpoint) return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
......
...@@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => { ...@@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => {
}, {}); }, {});
}; };
/**
* Converts object with key-value pairs
* into query-param string
*
* @param {Object} params
*/
export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname); export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/** /**
......
...@@ -39,8 +39,11 @@ import 'ee/main'; ...@@ -39,8 +39,11 @@ import 'ee/main';
import initDispatcher from './dispatcher'; import initDispatcher from './dispatcher';
// eslint-disable-next-line global-require, import/no-commonjs // inject test utilities if necessary
if (process.env.NODE_ENV !== 'production') require('./test_utils/'); if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
$.fx.off = true;
import(/* webpackMode: "eager" */ './test_utils/');
}
svg4everybody(); svg4everybody();
......
...@@ -3,7 +3,7 @@ import notesApp from '../notes/components/notes_app.vue'; ...@@ -3,7 +3,7 @@ import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue'; import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores'; import store from '../notes/stores';
document.addEventListener('DOMContentLoaded', () => { export default function initMrNotes() {
new Vue({ // eslint-disable-line new Vue({ // eslint-disable-line
el: '#js-vue-mr-discussions', el: '#js-vue-mr-discussions',
components: { components: {
...@@ -38,4 +38,4 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -38,4 +38,4 @@ document.addEventListener('DOMContentLoaded', () => {
return createElement('discussion-counter'); return createElement('discussion-counter');
}, },
}); });
}); }
import U2FRegister from './u2f/register'; import U2FRegister from '~/u2f/register';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth'); const twoFactorNode = document.querySelector('.js-two-factor-auth');
......
import initTerminal from '~/terminal/';
document.addEventListener('DOMContentLoaded', initTerminal);
import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
import initMrNotes from '~/mr_notes';
import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../init_merge_request_show'; import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initShow(); initShow();
initSidebarBundle(); initSidebarBundle();
if (hasVueMRDiscussionsCookie()) {
initMrNotes();
}
}); });
...@@ -5,6 +5,7 @@ export default ({ ...@@ -5,6 +5,7 @@ export default ({
filteredSearchTokenKeys, filteredSearchTokenKeys,
isGroup, isGroup,
isGroupAncestor, isGroupAncestor,
isGroupDecendent,
stateFiltersSelector, stateFiltersSelector,
}) => { }) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
...@@ -13,6 +14,7 @@ export default ({ ...@@ -13,6 +14,7 @@ export default ({
page, page,
isGroup, isGroup,
isGroupAncestor, isGroupAncestor,
isGroupDecendent,
filteredSearchTokenKeys, filteredSearchTokenKeys,
stateFiltersSelector, stateFiltersSelector,
}); });
......
...@@ -6,4 +6,4 @@ import './terminal'; ...@@ -6,4 +6,4 @@ import './terminal';
window.Terminal = Terminal; window.Terminal = Terminal;
$(() => new gl.Terminal({ selector: '#terminal' })); export default () => new gl.Terminal({ selector: '#terminal' });
/* 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
......
<script>
import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue';
import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
export default {
components: {
LoadingIcon,
DropdownTitle,
DropdownValue,
DropdownValueCollapsed,
DropdownButton,
DropdownHiddenInput,
DropdownHeader,
DropdownSearchInput,
DropdownFooter,
DropdownCreateLabel,
},
props: {
showCreate: {
type: Boolean,
required: false,
default: false,
},
abilityName: {
type: String,
required: true,
},
context: {
type: Object,
required: true,
},
namespace: {
type: String,
required: false,
default: '',
},
updatePath: {
type: String,
required: false,
default: '',
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: false,
default: '',
},
labelFilterBasePath: {
type: String,
required: false,
default: '',
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
hiddenInputName() {
return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
},
},
mounted() {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
},
};
</script>
<template>
<div class="block labels">
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
/>
<dropdown-title
:can-edit="canEdit"
/>
<dropdown-value
:labels="context.labels"
:label-filter-base-path="labelFilterBasePath"
>
<slot></slot>
</dropdown-value>
<div
v-if="canEdit"
class="selectbox"
style="display: none;"
>
<dropdown-hidden-input
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
:label="label"
/>
<div class="dropdown">
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
:update-path="updatePath"
:labels-path="labelsPath"
:namespace="namespace"
:labels="context.labels"
:show-extra-options="!showCreate"
/>
<div
class="dropdown-menu dropdown-select dropdown-menu-paging
dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-page-one">
<dropdown-header v-if="showCreate" />
<dropdown-search-input/>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
<loading-icon />
</div>
<dropdown-footer
v-if="showCreate"
:labels-web-url="labelsWebUrl"
/>
</div>
<dropdown-create-label
v-if="showCreate"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { __, s__, sprintf } from '~/locale';
export default {
props: {
abilityName: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
updatePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
namespace: {
type: String,
required: true,
},
labels: {
type: Array,
required: true,
},
showExtraOptions: {
type: Boolean,
required: true,
},
},
computed: {
dropdownToggleText() {
if (this.labels.length === 0) {
return __('Label');
}
if (this.labels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.labels[0].title,
remainingLabelCount: this.labels.length - 1,
});
}
return this.labels[0].title;
},
},
};
</script>
<template>
<button
type="button"
ref="dropdownButton"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
data-toggle="dropdown"
:class="{ 'js-extra-options': showExtraOptions }"
:data-ability-name="abilityName"
:data-field-name="fieldName"
:data-issue-update="updatePath"
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
>
<span class="dropdown-toggle-text">
{{ dropdownToggleText }}
</span>
<i
aria-hidden="true"
class="fa fa-chevron-down"
data-hidden="true"
>
</i>
</button>
</template>
<script>
export default {
created() {
this.suggestedColors = gon.suggested_label_colors;
},
};
</script>
<template>
<div class="dropdown-page-two dropdown-new-label">
<div class="dropdown-title">
<button
type="button"
class="dropdown-title-button dropdown-menu-back"
:aria-label="__('Go back')"
>
<i
aria-hidden="true"
class="fa fa-arrow-left"
data-hidden="true"
>
</i>
</button>
{{ __('Create new label') }}
<button
type="button"
class="dropdown-title-button dropdown-menu-close"
:aria-label="__('Close')"
>
<i
aria-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
data-hidden="true"
>
</i>
</button>
</div>
<div class="dropdown-content">
<div class="dropdown-labels-error js-label-error"></div>
<input
id="new_label_name"
type="text"
class="default-dropdown-input"
:placeholder="__('Name new label')"
/>
<div class="suggest-colors suggest-colors-dropdown">
<a
v-for="(color, index) in suggestedColors"
href="#"
:key="index"
:data-color="color"
:style="{
backgroundColor: color,
}"
>
&nbsp;
</a>
</div>
<div class="dropdown-label-color-input">
<div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
<input
id="new_label_color"
type="text"
class="default-dropdown-input"
:placeholder="__('Assign custom color like #FF0000')"
/>
</div>
<div class="clearfix">
<button
type="button"
class="btn btn-primary pull-left js-new-label-btn disabled"
>
{{ __('Create') }}
</button>
<button
type="button"
class="btn btn-default pull-right js-cancel-label-btn"
>
{{ __('Cancel') }}
</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
labelsWebUrl: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<a
href="#"
class="dropdown-toggle-page"
>
{{ __('Create new label') }}
</a>
</li>
<li>
<a
data-is-link="true"
class="dropdown-external-link"
:href="labelsWebUrl"
>
{{ __('Manage labels') }}
</a>
</li>
</ul>
</div>
</template>
<script>
export default {};
</script>
<template>
<div class="dropdown-title">
<span>{{ __('Assign labels') }}</span>
<button
type="button"
class="dropdown-title-button dropdown-menu-close"
:aria-label="__('Close')"
>
<i
aria-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
data-hidden="true"
>
</i>
</button>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true,
},
label: {
type: Object,
required: true,
},
},
};
</script>
<template>
<input
type="hidden"
:name="name"
:value="label.id"
/>
</template>
<script>
export default {};
</script>
<template>
<div class="dropdown-input">
<input
autocomplete="off"
class="dropdown-input-field"
type="search"
:placeholder="__('Search')"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
data-hidden="true"
>
</i>
<i
aria-hidden="true"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
data-hidden="true"
role="button"
>
</i>
</div>
</template>
<script>
export default {
props: {
canEdit: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div class="title hide-collapsed append-bottom-10">
{{ __('Labels') }}
<template v-if="canEdit">
<i
aria-hidden="true"
class="fa fa-spinner fa-spin block-loading"
data-hidden="true"
>
</i>
<button
type="button"
class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
>
{{ __('Edit') }}
</button>
</template>
</div>
</template>
<script>
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
labels: {
type: Array,
required: true,
},
labelFilterBasePath: {
type: String,
required: true,
},
},
computed: {
isEmpty() {
return this.labels.length === 0;
},
},
methods: {
labelFilterUrl(label) {
return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
},
labelStyle(label) {
return {
color: label.textColor,
backgroundColor: label.color,
};
},
},
};
</script>
<template>
<div class="hide-collapsed value issuable-show-labels">
<span
v-if="isEmpty"
class="text-secondary"
>
<slot>{{ __('None') }}</slot>
</span>
<a
v-else
v-for="label in labels"
:key="label.id"
:href="labelFilterUrl(label)"
>
<span
v-tooltip
class="label color-label"
data-placement="bottom"
data-container="body"
:style="labelStyle(label)"
:title="label.description"
>
{{ label.title }}
</span>
</a>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
props: {
labels: {
type: Array,
required: true,
},
},
computed: {
labelsList() {
const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
if (this.labels.length > 5) {
return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
labelsString,
remainingLabelCount: this.labels.length - 5,
});
}
return labelsString;
},
},
};
</script>
<template>
<div
v-tooltip
class="sidebar-collapsed-icon"
data-placement="left"
data-container="body"
:title="labelsList"
>
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-tags"
>
</i>
<span>{{ labels.length }}</span>
</div>
</template>
/* eslint-disable no-unused-vars, space-before-function-paren */
class ListLabel { class ListLabel {
constructor (obj) { constructor(obj) {
this.id = obj.id; this.id = obj.id;
this.title = obj.title; this.title = obj.title;
this.type = obj.type; this.type = obj.type;
......
...@@ -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
......
...@@ -62,19 +62,27 @@ module UploadsActions ...@@ -62,19 +62,27 @@ module UploadsActions
end end
def build_uploader_from_upload def build_uploader_from_upload
return nil unless params[:secret] && params[:filename] return unless uploader = build_uploader
upload_path = uploader_class.upload_path(params[:secret], params[:filename]) upload_paths = uploader.upload_paths(params[:filename])
upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_path) upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_paths)
upload&.build_uploader upload&.build_uploader
end end
def build_uploader_from_params def build_uploader_from_params
return unless uploader = build_uploader
uploader.retrieve_from_store!(params[:filename])
uploader
end
def build_uploader
return unless params[:secret] && params[:filename]
uploader = uploader_class.new(model, secret: params[:secret]) uploader = uploader_class.new(model, secret: params[:secret])
return nil unless uploader.model_valid? return unless uploader.model_valid?
uploader.retrieve_from_store!(params[:filename])
uploader uploader
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
...@@ -14,37 +14,31 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -14,37 +14,31 @@ class Projects::CommitsController < Projects::ApplicationController
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
# https://gitlab.com/gitlab-org/gitaly/issues/931 respond_to do |format|
Gitlab::GitalyClient.allow_n_plus_1_calls do format.html
respond_to do |format| format.atom { render layout: 'xml.atom' }
format.html
format.atom { render layout: 'xml.atom' }
format.json do format.json do
pager_json( pager_json(
'projects/commits/_commits', 'projects/commits/_commits',
@commits.size, @commits.size,
project: @project, project: @project,
ref: @ref) ref: @ref)
end
end end
end end
end end
def signatures def signatures
# https://gitlab.com/gitlab-org/gitaly/issues/931 respond_to do |format|
Gitlab::GitalyClient.allow_n_plus_1_calls do format.json do
respond_to do |format| render json: {
format.json do signatures: @commits.select(&:has_signature?).map do |commit|
render json: { {
signatures: @commits.select(&:has_signature?).map do |commit| commit_sha: commit.sha,
{ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
commit_sha: commit.sha, }
html: view_to_html_string('projects/commit/_signature', signature: commit.signature) end
} }
end
}
end
end end
end 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
......
...@@ -48,11 +48,23 @@ class NotesFinder ...@@ -48,11 +48,23 @@ class NotesFinder
def init_collection def init_collection
if target if target
notes_on_target notes_on_target
elsif target_type
notes_of_target_type
else else
notes_of_any_type notes_of_any_type
end end
end end
def notes_of_target_type
notes = notes_for_type(target_type)
search(notes)
end
def target_type
@params[:target_type]
end
def notes_of_any_type def notes_of_any_type
types = %w(commit issue merge_request snippet) types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) } note_relations = types.map { |t| notes_for_type(t) }
......
...@@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder ...@@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder
.public_or_visible_to_user(current_user) .public_or_visible_to_user(current_user)
end end
# Returns a collection of projects that is either public or visible to the
# logged in user.
#
# A caller must pass in a block to modify individual parts of
# the query, e.g. to apply .with_feature_available_for_user on top of it.
# This is useful for performance as we can stick those additional filters
# at the bottom of e.g. the UNION.
def projects_for_user
return yield(Project.public_to_user) unless current_user
# If the current_user is allowed to see all projects,
# we can shortcut and just return.
return yield(Project.all) if current_user.full_private_access?
authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
visible_projects = yield(Project.where(visibility_level: levels))
# We use a UNION here instead of OR clauses since this results in better
# performance.
union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
Project.from("(#{union.to_sql}) AS #{Project.table_name}")
end
def feature_available_projects def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project # Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user) part.with_feature_available_for_user(:snippets, current_user)
end.select(:id) end.select(:id)
......
...@@ -324,10 +324,6 @@ module ApplicationHelper ...@@ -324,10 +324,6 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true" cookies["sidebar_collapsed"] == "true"
end end
def show_new_ide?(project)
cookies["new_repo"] == "true" && project.feature_available?(:ide)
end
def locale_path def locale_path
asset_path("locale/#{Gitlab::I18n.locale}/app.js") asset_path("locale/#{Gitlab::I18n.locale}/app.js")
end end
......
...@@ -33,20 +33,6 @@ module BlobHelper ...@@ -33,20 +33,6 @@ module BlobHelper
ref) ref)
end end
def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
return unless show_new_ide?(project)
return unless blob = readable_blob(options, path, project, ref)
common_classes = "btn js-edit-ide #{options[:extra_class]}"
edit_button_tag(blob,
common_classes,
_('Web IDE'),
ide_edit_path(project, ref, path, options),
project,
ref)
end
def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:) def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user return unless current_user
......
module LabelsHelper module LabelsHelper
extend self
include ActionView::Helpers::TagHelper include ActionView::Helpers::TagHelper
def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil)
......
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
class Badge < ActiveRecord::Base
# This structure sets the placeholders that the urls
# can have. This hash also sets which action to ask when
# the placeholder is found.
PLACEHOLDERS = {
'project_path' => :full_path,
'project_id' => :id,
'default_branch' => :default_branch,
'commit_sha' => ->(project) { project.commit&.sha }
}.freeze
# This regex is built dynamically using the keys from the PLACEHOLDER struct.
# So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
# This regex will build the new PLACEHOLDER_REGEX with the new information
PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze
default_scope { order_created_at_asc }
scope :order_created_at_asc, -> { reorder(created_at: :asc) }
validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX }
validates :type, presence: true
def rendered_link_url(project = nil)
build_rendered_url(link_url, project)
end
def rendered_image_url(project = nil)
build_rendered_url(image_url, project)
end
private
def build_rendered_url(url, project = nil)
return url unless valid? && project
Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg|
replace_placeholder_action(PLACEHOLDERS[arg], project)
end
end
# The action param represents the :symbol or Proc to call in order
# to retrieve the return value from the project.
# This method checks if it is a Proc and use the call method, and if it is
# a symbol just send the action
def replace_placeholder_action(action, project)
return unless project
action.is_a?(Proc) ? action.call(project) : project.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
end
end
class GroupBadge < Badge
belongs_to :group
validates :group, presence: true
end
class ProjectBadge < Badge
belongs_to :project
validates :project, presence: true
def rendered_link_url(project = nil)
project ||= self.project
super
end
def rendered_image_url(project = nil)
project ||= self.project
super
end
end
...@@ -15,7 +15,7 @@ module Clusters ...@@ -15,7 +15,7 @@ module Clusters
end end
def install_command def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(name, install_helm: true) Gitlab::Kubernetes::Helm::InitCommand.new(name)
end end
end end
end end
......
...@@ -5,6 +5,8 @@ module Clusters ...@@ -5,6 +5,8 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationData
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,16 +15,34 @@ module Clusters ...@@ -13,16 +15,34 @@ 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
def chart_values_file def install_command
"#{Rails.root}/vendor/#{name}/values.yaml" Gitlab::Kubernetes::Helm::InstallCommand.new(
name,
chart: chart,
values: values
)
end end
def install_command def schedule_status_update
Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) return unless installed?
return if external_ip
ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end end
end end
end end
......
...@@ -7,6 +7,7 @@ module Clusters ...@@ -7,6 +7,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationData
default_value_for :version, VERSION default_value_for :version, VERSION
...@@ -30,12 +31,12 @@ module Clusters ...@@ -30,12 +31,12 @@ module Clusters
80 80
end end
def chart_values_file
"#{Rails.root}/vendor/#{name}/values.yaml"
end
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,
values: values
)
end end
def proxy_client def proxy_client
......
module Clusters
module Applications
class Runner < ActiveRecord::Base
VERSION = '0.1.13'.freeze
self.table_name = 'clusters_applications_runners'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationData
belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id
delegate :project, to: :cluster
default_value_for :version, VERSION
def chart
"#{name}/gitlab-runner"
end
def repository
'https://charts.gitlab.io'
end
def values
content_values.to_yaml
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name,
chart: chart,
values: values,
repository: repository
)
end
private
def ensure_runner
runner || create_and_assign_runner
end
def create_and_assign_runner
transaction do
project.runners.create!(name: 'kubernetes-cluster', tag_list: %w(kubernetes cluster)).tap do |runner|
update!(runner_id: runner.id)
end
end
end
def gitlab_url
Gitlab::Routing.url_helpers.root_url(only_path: false)
end
def specification
{
"gitlabUrl" => gitlab_url,
"runnerToken" => ensure_runner.token
}
end
def content_values
specification.merge(YAML.load_file(chart_values_file))
end
end
end
end
...@@ -8,7 +8,8 @@ module Clusters ...@@ -8,7 +8,8 @@ module Clusters
APPLICATIONS = { APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm, Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress, Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner
}.freeze }.freeze
belongs_to :user belongs_to :user
...@@ -24,6 +25,7 @@ module Clusters ...@@ -24,6 +25,7 @@ module Clusters
has_one :application_helm, class_name: 'Clusters::Applications::Helm' has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true
...@@ -70,7 +72,8 @@ module Clusters ...@@ -70,7 +72,8 @@ module Clusters
[ [
application_helm || build_application_helm, application_helm || build_application_helm,
application_ingress || build_application_ingress, application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus application_prometheus || build_application_prometheus,
application_runner || build_application_runner
] ]
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
......
module Clusters
module Concerns
module ApplicationData
extend ActiveSupport::Concern
included do
def repository
nil
end
def values
File.read(chart_values_file)
end
private
def chart_values_file
"#{Rails.root}/vendor/#{name}/values.yaml"
end
end
end
end
end
...@@ -19,6 +19,7 @@ class Commit ...@@ -19,6 +19,7 @@ class Commit
attr_accessor :project, :author attr_accessor :project, :author
attr_accessor :redacted_description_html attr_accessor :redacted_description_html
attr_accessor :redacted_title_html attr_accessor :redacted_title_html
attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
...@@ -110,6 +111,7 @@ class Commit ...@@ -110,6 +111,7 @@ class Commit
@raw = raw_commit @raw = raw_commit
@project = project @project = project
@statuses = {} @statuses = {}
@gpg_commit = Gitlab::Gpg::Commit.new(self) if project
end end
def id def id
...@@ -452,8 +454,4 @@ class Commit ...@@ -452,8 +454,4 @@ class Commit
def merged_merge_request_no_cache(user) def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end end
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
end end
...@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end end
def group_name def group_name
name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip
end end
def failed_but_allowed? def failed_but_allowed?
......
...@@ -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],
......
...@@ -39,6 +39,7 @@ class Group < Namespace ...@@ -39,6 +39,7 @@ class Group < Namespace
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy` # We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`. # here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id' has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id'
has_many :badges, class_name: 'GroupBadge'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
......
...@@ -18,4 +18,8 @@ class LfsObject < ActiveRecord::Base ...@@ -18,4 +18,8 @@ class LfsObject < ActiveRecord::Base
.where(lfs_objects_projects: { id: nil }) .where(lfs_objects_projects: { id: nil })
.destroy_all .destroy_all
end end
def self.calculate_oid(path)
Digest::SHA256.file(path).hexdigest
end
end end
...@@ -228,6 +228,8 @@ class Project < ActiveRecord::Base ...@@ -228,6 +228,8 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops' has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data accepts_nested_attributes_for :import_data
...@@ -324,42 +326,13 @@ class Project < ActiveRecord::Base ...@@ -324,42 +326,13 @@ class Project < ActiveRecord::Base
# Returns a collection of projects that is either public or visible to the # Returns a collection of projects that is either public or visible to the
# logged in user. # logged in user.
# def self.public_or_visible_to_user(user = nil)
# A caller may pass in a block to modify individual parts of if user
# the query, e.g. to apply .with_feature_available_for_user on top of it. where('EXISTS (?) OR projects.visibility_level IN (?)',
# This is useful for performance as we can stick those additional filters user.authorizations_for_projects,
# at the bottom of e.g. the UNION. Gitlab::VisibilityLevel.levels_for_user(user))
#
# Optionally, turning `use_where_in` off leads to returning a
# relation using #from instead of #where. This can perform much better
# but leads to trouble when used in conjunction with AR's #merge method.
def self.public_or_visible_to_user(user = nil, use_where_in: true, &block)
# If we don't get a block passed, use identity to avoid if/else repetitions
block = ->(part) { part } unless block_given?
return block.call(public_to_user) unless user
# If the user is allowed to see all projects,
# we can shortcut and just return.
return block.call(all) if user.full_private_access?
authorized = user
.project_authorizations
.select(1)
.where('project_authorizations.project_id = projects.id')
authorized_projects = block.call(where('EXISTS (?)', authorized))
levels = Gitlab::VisibilityLevel.levels_for_user(user)
visible_projects = block.call(where(visibility_level: levels))
# We use a UNION here instead of OR clauses since this results in better
# performance.
union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
if use_where_in
where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
else else
from("(#{union.to_sql}) AS #{table_name}") public_to_user
end end
end end
...@@ -378,14 +351,11 @@ class Project < ActiveRecord::Base ...@@ -378,14 +351,11 @@ class Project < ActiveRecord::Base
elsif user elsif user
column = ProjectFeature.quoted_access_level_column(feature) column = ProjectFeature.quoted_access_level_column(feature)
authorized = user.project_authorizations.select(1)
.where('project_authorizations.project_id = projects.id')
with_project_feature with_project_feature
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
visible, visible,
ProjectFeature::PRIVATE, ProjectFeature::PRIVATE,
authorized) user.authorizations_for_projects)
else else
with_feature_access_level(feature, visible) with_feature_access_level(feature, visible)
end end
...@@ -1806,6 +1776,17 @@ class Project < ActiveRecord::Base ...@@ -1806,6 +1776,17 @@ class Project < ActiveRecord::Base
.set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION) .set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
end end
def badges
return project_badges unless group
group_badges_rel = GroupBadge.where(group: group.self_and_ancestors)
union = Gitlab::SQL::Union.new([project_badges.select(:id),
group_badges_rel.select(:id)])
Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
private private
def storage def storage
......
...@@ -99,7 +99,7 @@ class ChatNotificationService < Service ...@@ -99,7 +99,7 @@ class ChatNotificationService < Service
def get_message(object_kind, data) def get_message(object_kind, data)
case object_kind case object_kind
when "push", "tag_push" when "push", "tag_push"
ChatMessage::PushMessage.new(data) ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue" when "issue"
ChatMessage::IssueMessage.new(data) unless update?(data) ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request" when "merge_request"
...@@ -145,10 +145,16 @@ class ChatNotificationService < Service ...@@ -145,10 +145,16 @@ class ChatNotificationService < Service
end end
def notify_for_ref?(data) def notify_for_ref?(data)
return true if data[:object_attributes][:tag] return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch? return true unless notify_only_default_branch?
data[:object_attributes][:ref] == project.default_branch ref = if data[:ref]
Gitlab::Git.ref_name(data[:ref])
else
data.dig(:object_attributes, :ref)
end
ref == project.default_branch
end end
def notify_for_pipeline?(data) def notify_for_pipeline?(data)
......
...@@ -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
......
...@@ -619,6 +619,15 @@ class User < ActiveRecord::Base ...@@ -619,6 +619,15 @@ class User < ActiveRecord::Base
authorized_projects(min_access_level).exists?({ id: project.id }) authorized_projects(min_access_level).exists?({ id: project.id })
end end
# Typically used in conjunction with projects table to get projects
# a user has been given access to.
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
def authorizations_for_projects
project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
end
# Returns the projects this user has reporter (or greater) access to, limited # Returns the projects this user has reporter (or greater) access to, limited
# to at most the given projects. # to at most the given projects.
# #
......
...@@ -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 Badges
class BaseService
protected
attr_accessor :params
def initialize(params = {})
@params = params.dup
end
end
end
module Badges
class BuildService < Badges::BaseService
# returns the created badge
def execute(source)
if source.is_a?(Group)
GroupBadge.new(params.merge(group: source))
else
ProjectBadge.new(params.merge(project: source))
end
end
end
end
module Badges
class CreateService < Badges::BaseService
# returns the created badge
def execute(source)
badge = Badges::BuildService.new(params).execute(source)
badge.tap { |b| b.save }
end
end
end
module Badges
class UpdateService < Badges::BaseService
# returns the updated badge
def execute(badge)
if params.present?
badge.update_attributes(params)
end
badge
end
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
...@@ -111,6 +111,10 @@ class IssuableBaseService < BaseService ...@@ -111,6 +111,10 @@ class IssuableBaseService < BaseService
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end end
def handle_quick_actions_on_create(issuable)
merge_quick_actions_into_params!(issuable)
end
def merge_quick_actions_into_params!(issuable) def merge_quick_actions_into_params!(issuable)
original_description = params.fetch(:description, issuable.description) original_description = params.fetch(:description, issuable.description)
...@@ -133,7 +137,7 @@ class IssuableBaseService < BaseService ...@@ -133,7 +137,7 @@ class IssuableBaseService < BaseService
end end
def create(issuable) def create(issuable)
merge_quick_actions_into_params!(issuable) handle_quick_actions_on_create(issuable)
filter_params(issuable) filter_params(issuable)
params.delete(:state_event) params.delete(:state_event)
......
...@@ -26,6 +26,17 @@ module MergeRequests ...@@ -26,6 +26,17 @@ module MergeRequests
private private
def handle_wip_event(merge_request)
if wip_event = params.delete(:wip_event)
# We update the title that is provided in the params or we use the mr title
title = params[:title] || merge_request.title
params[:title] = case wip_event
when 'wip' then MergeRequest.wip_title(title)
when 'unwip' then MergeRequest.wipless_title(title)
end
end
end
def merge_request_metrics_service(merge_request) def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics) MergeRequestMetricsService.new(merge_request.metrics)
end end
......
...@@ -34,6 +34,12 @@ module MergeRequests ...@@ -34,6 +34,12 @@ module MergeRequests
super super
end end
# Override from IssuableBaseService
def handle_quick_actions_on_create(merge_request)
super
handle_wip_event(merge_request)
end
private private
def update_merge_requests_head_pipeline(merge_request) def update_merge_requests_head_pipeline(merge_request)
......
...@@ -119,17 +119,6 @@ module MergeRequests ...@@ -119,17 +119,6 @@ module MergeRequests
end end
end end
def handle_wip_event(merge_request)
if wip_event = params.delete(:wip_event)
# We update the title that is provided in the params or we use the mr title
title = params[:title] || merge_request.title
params[:title] = case wip_event
when 'wip' then MergeRequest.wip_title(title)
when 'unwip' then MergeRequest.wipless_title(title)
end
end
end
def create_branch_change_note(issuable, branch_type, old_branch, new_branch) def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch( SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type, issuable, issuable.project, current_user, branch_type,
......
...@@ -348,9 +348,9 @@ module QuickActions ...@@ -348,9 +348,9 @@ module QuickActions
"#{verb} this #{noun} as Work In Progress." "#{verb} this #{noun} as Work In Progress."
end end
condition do condition do
issuable.persisted? && issuable.respond_to?(:work_in_progress?) &&
issuable.respond_to?(:work_in_progress?) && # Allow it to mark as WIP on MR creation page _or_ through MR notes.
current_user.can?(:"update_#{issuable.to_ability_name}", issuable) (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable))
end end
command :wip do command :wip do
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
......
...@@ -32,8 +32,11 @@ class FileUploader < GitlabUploader ...@@ -32,8 +32,11 @@ class FileUploader < GitlabUploader
) )
end end
def self.base_dir(model) def self.base_dir(model, store = Store::LOCAL)
model_path_segment(model) decorated_model = model
decorated_model = Storage::HashedProject.new(model) if store == Store::REMOTE
model_path_segment(decorated_model)
end end
# used in migrations and import/exports # used in migrations and import/exports
...@@ -51,21 +54,24 @@ class FileUploader < GitlabUploader ...@@ -51,21 +54,24 @@ class FileUploader < GitlabUploader
# #
# Returns a String without a trailing slash # Returns a String without a trailing slash
def self.model_path_segment(model) def self.model_path_segment(model)
if model.hashed_storage?(:attachments) case model
model.disk_path when Storage::HashedProject then model.disk_path
else else
model.full_path model.hashed_storage?(:attachments) ? model.disk_path : model.full_path
end end
end end
def self.upload_path(secret, identifier)
File.join(secret, identifier)
end
def self.generate_secret def self.generate_secret
SecureRandom.hex SecureRandom.hex
end end
def upload_paths(filename)
[
File.join(secret, filename),
File.join(base_dir(Store::REMOTE), secret, filename)
]
end
attr_accessor :model attr_accessor :model
def initialize(model, mounted_as = nil, **uploader_context) def initialize(model, mounted_as = nil, **uploader_context)
...@@ -75,8 +81,10 @@ class FileUploader < GitlabUploader ...@@ -75,8 +81,10 @@ class FileUploader < GitlabUploader
apply_context!(uploader_context) apply_context!(uploader_context)
end end
def base_dir # enforce the usage of Hashed storage when storing to
self.class.base_dir(@model) # remote store as the FileMover doesn't support OS
def base_dir(store = nil)
self.class.base_dir(@model, store || object_store)
end end
# we don't need to know the actual path, an uploader instance should be # we don't need to know the actual path, an uploader instance should be
...@@ -86,15 +94,19 @@ class FileUploader < GitlabUploader ...@@ -86,15 +94,19 @@ class FileUploader < GitlabUploader
end end
def upload_path def upload_path
self.class.upload_path(dynamic_segment, identifier) if file_storage?
end # Legacy path relative to project.full_path
File.join(dynamic_segment, identifier)
def model_path_segment else
self.class.model_path_segment(@model) File.join(store_dir, identifier)
end
end end
def store_dir def store_dirs
File.join(base_dir, dynamic_segment) {
Store::LOCAL => File.join(base_dir, dynamic_segment),
Store::REMOTE => File.join(base_dir(ObjectStorage::Store::REMOTE), dynamic_segment)
}
end end
def markdown_link def markdown_link
......
...@@ -4,7 +4,7 @@ class NamespaceFileUploader < FileUploader ...@@ -4,7 +4,7 @@ class NamespaceFileUploader < FileUploader
options.storage_path options.storage_path
end end
def self.base_dir(model) def self.base_dir(model, _store = nil)
File.join(options.base_dir, 'namespace', model_path_segment(model)) File.join(options.base_dir, 'namespace', model_path_segment(model))
end end
...@@ -20,7 +20,7 @@ class NamespaceFileUploader < FileUploader ...@@ -20,7 +20,7 @@ class NamespaceFileUploader < FileUploader
def store_dirs def store_dirs
{ {
Store::LOCAL => File.join(base_dir, dynamic_segment), Store::LOCAL => File.join(base_dir, dynamic_segment),
Store::REMOTE => File.join('namespace', model_path_segment, dynamic_segment) Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment)
} }
end end
end end
...@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader ...@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
options.storage_path options.storage_path
end end
def self.base_dir(model) def self.base_dir(model, _store = nil)
File.join(options.base_dir, model_path_segment(model)) File.join(options.base_dir, model_path_segment(model))
end end
...@@ -34,7 +34,7 @@ class PersonalFileUploader < FileUploader ...@@ -34,7 +34,7 @@ class PersonalFileUploader < FileUploader
def store_dirs def store_dirs
{ {
Store::LOCAL => File.join(base_dir, dynamic_segment), Store::LOCAL => File.join(base_dir, dynamic_segment),
Store::REMOTE => File.join(model_path_segment, dynamic_segment) Store::REMOTE => File.join(self.class.model_path_segment(model), dynamic_segment)
} }
end end
......
# UrlValidator
#
# Custom validator for URLs.
#
# By default, only URLs for the HTTP(S) protocols will be considered valid.
# Provide a `:protocols` option to configure accepted protocols.
#
# Also, this validator can help you validate urls with placeholders inside.
# Usually, if you have a url like 'http://www.example.com/%{project_path}' the
# URI parser will reject that URL format. Provide a `:placeholder_regex` option
# to configure accepted placeholders.
#
# Example:
#
# class User < ActiveRecord::Base
# validates :personal_url, url: true
#
# validates :ftp_url, url: { protocols: %w(ftp) }
#
# validates :git_url, url: { protocols: %w(http https ssh git) }
#
# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ }
# end
#
class UrlPlaceholderValidator < UrlValidator
def validate_each(record, attribute, value)
placeholder_regex = self.options[:placeholder_regex]
value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value
super(record, attribute, value)
end
end
...@@ -706,15 +706,15 @@ ...@@ -706,15 +706,15 @@
.checkbox .checkbox
= f.label :usage_ping_enabled do = f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured = f.check_box :usage_ping_enabled, disabled: !can_be_configured
Usage ping enabled Enable usage ping
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
.help-block .help-block
- if can_be_configured - if can_be_configured
Every week GitLab will report license usage back to GitLab, Inc. To help improve GitLab and its user experience, GitLab will
Disable this option if you do not want this to occur. To see the periodically collect usage information.
JSON payload that will be sent, visit the = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
= succeed '.' do about what information is shared with GitLab Inc. Visit
= link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping') = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
to see the JSON payload sent.
- else - else
The usage ping is disabled, and cannot be configured through this The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on form. For more information, see the documentation on
......
- 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
......
...@@ -42,7 +42,6 @@ ...@@ -42,7 +42,6 @@
= webpack_bundle_tag "common" = webpack_bundle_tag "common"
= webpack_bundle_tag "main" = webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
= webpack_bundle_tag "test" if Rails.env.test?
- if content_for?(:page_specific_javascripts) - if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts = yield :page_specific_javascripts
......
...@@ -2,12 +2,6 @@ ...@@ -2,12 +2,6 @@
- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path) - add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- content_for :page_specific_javascripts do
- if inject_u2f_api?
= webpack_bundle_tag('u2f')
= 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 }
.row.prepend-top-default .row.prepend-top-default
.col-lg-4 .col-lg-4
......
...@@ -30,6 +30,12 @@ ...@@ -30,6 +30,12 @@
%br %br
= render "shared/mirror_status" = render "shared/mirror_status"
.project-badges
- @project.badges.each do |badge|
- badge_link_url = badge.rendered_link_url(@project)
%a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' }
%img{ src: badge.rendered_image_url(@project), alt: badge_link_url }
.project-repo-buttons .project-repo-buttons
.count-buttons .count-buttons
= render 'projects/buttons/star' = render 'projects/buttons/star'
......
...@@ -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
......
...@@ -14,7 +14,9 @@ ...@@ -14,7 +14,9 @@
= lock_file_link(html_options: { class: 'btn btn-sm path-lock' }) = lock_file_link(html_options: { class: 'btn btn-sm path-lock' })
= edit_blob_button = edit_blob_button
-# EE-specific
= ide_edit_button = ide_edit_button
-# EE-specific
- if current_user - if current_user
= replace_blob_link = replace_blob_link
= delete_blob_link = delete_blob_link
......
...@@ -10,11 +10,13 @@ ...@@ -10,11 +10,13 @@
install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner),
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name, cluster_status: @cluster.status_name,
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
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,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('service_desk')
.project-edit-container .project-edit-container
%section.settings.general-settings.no-animate{ class: ('expanded' if expanded) } %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) }
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm" = stylesheet_link_tag "xterm/xterm"
= webpack_bundle_tag("terminal")
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
......
...@@ -5,9 +5,6 @@ ...@@ -5,9 +5,6 @@
- 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
- if has_vue_discussions_cookie?
- content_for :page_specific_javascripts do
= 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) } }
= render "projects/merge_requests/mr_title" = render "projects/merge_requests/mr_title"
......
...@@ -78,9 +78,11 @@ ...@@ -78,9 +78,11 @@
= render 'projects/find_file_link' = render 'projects/find_file_link'
-## EE-specific
- if show_new_ide?(@project) - if show_new_ide?(@project)
= succeed " " do = succeed " " do
= link_to ide_edit_path(@project, @id), class: 'btn btn-default' do = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
= _('Web IDE') = _('Web IDE')
-## EE-specific
= render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/download', project: @project, ref: @ref
...@@ -14,25 +14,25 @@ ...@@ -14,25 +14,25 @@
= link_to search_filter_path(scope: 'issues') do = link_to search_filter_path(scope: 'issues') do
Issues Issues
%span.badge %span.badge
= @search_results.issues_count = limited_count(@search_results.limited_issues_count)
- if project_search_tabs?(:merge_requests) - if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') } %li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do = link_to search_filter_path(scope: 'merge_requests') do
Merge requests Merge requests
%span.badge %span.badge
= @search_results.merge_requests_count = limited_count(@search_results.limited_merge_requests_count)
- if project_search_tabs?(:milestones) - if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') } %li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do = link_to search_filter_path(scope: 'milestones') do
Milestones Milestones
%span.badge %span.badge
= @search_results.milestones_count = limited_count(@search_results.limited_milestones_count)
- if project_search_tabs?(:notes) - if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') } %li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do = link_to search_filter_path(scope: 'notes') do
Comments Comments
%span.badge %span.badge
= @search_results.notes_count = limited_count(@search_results.limited_notes_count)
- if project_search_tabs?(:wiki) - if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') } %li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do = link_to search_filter_path(scope: 'wiki_blobs') do
......
- show_create = local_assigns.fetch(:show_create, false) - show_create = local_assigns.fetch(:show_create, false)
- show_new_branch_form = show_new_ide?(@project) && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch - dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination = hidden_field_tag :destination, destination
...@@ -16,14 +15,3 @@ ...@@ -16,14 +15,3 @@
= dropdown_filter _("Search branches and tags") = dropdown_filter _("Search branches and tags")
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
- if show_new_branch_form
= dropdown_footer do
%ul.dropdown-footer-list
%li
%a.dropdown-toggle-page{ href: "#" }
Create new branch
- if show_new_branch_form
.dropdown-page-two
= dropdown_title("Create new branch", options: { back: true })
= dropdown_content do
.js-new-branch-dropdown
...@@ -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
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.
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.
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.
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.
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.
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.
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.
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.
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