Commit 3eee0641 authored by leiminghuan's avatar leiminghuan

Merge branch 'master' into 202271-migrate-fa-spinner-for-notifications_dropdown.js

parents 72fa1130 92c62ed1
......@@ -203,6 +203,8 @@
- name: postgres:9.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg10:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
......@@ -210,6 +212,8 @@
- name: postgres:10.9
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg9-ee:
services:
......@@ -217,6 +221,8 @@
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
- name: elasticsearch:6.4.2
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.use-pg10-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
......@@ -225,6 +231,8 @@
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
- name: elasticsearch:6.4.2
variables:
POSTGRES_HOST_AUTH_METHOD: trust
.only-ee:
only:
......
......@@ -33,7 +33,7 @@ gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.3'
gem 'omniauth-github', '~> 1.4'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.6.0'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
......@@ -87,7 +87,7 @@ gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.6', require: 'rack/cors'
# GraphQL API
gem 'graphql', '~> 1.9.11'
gem 'graphql', '~> 1.9.12'
# NOTE: graphiql-rails v1.5+ doesn't work: https://gitlab.com/gitlab-org/gitlab/issues/31771
# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released:
# https://gitlab.com/gitlab-org/gitlab/issues/31747
......
......@@ -455,7 +455,7 @@ GEM
graphiql-rails (1.4.10)
railties
sprockets-rails
graphql (1.9.11)
graphql (1.9.12)
graphql-docs (1.6.0)
commonmarker (~> 0.16)
escape_utils (~> 1.2)
......@@ -688,7 +688,7 @@ GEM
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
omniauth-github (1.3.0)
omniauth-github (1.4.0)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.3)
......@@ -1252,7 +1252,7 @@ DEPENDENCIES
grape-path-helpers (~> 1.2)
grape_logging (~> 1.7)
graphiql-rails (~> 1.4.10)
graphql (~> 1.9.11)
graphql (~> 1.9.12)
graphql-docs (~> 1.6.0)
grpc (~> 1.24.0)
gssapi
......@@ -1304,7 +1304,7 @@ DEPENDENCIES
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.3)
omniauth-github (~> 1.4)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.6.0)
omniauth-kerberos (~> 0.3.0)
......
......@@ -24,6 +24,7 @@ const Api = {
projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
......@@ -220,6 +221,22 @@ const Api = {
return axios.get(url, config);
},
projectProtectedBranches(id, query = '') {
const url = Api.buildUrl(Api.projectProtectedBranchesPath).replace(
':id',
encodeURIComponent(id),
);
return axios
.get(url, {
params: {
search: query,
per_page: DEFAULT_PER_PAGE,
},
})
.then(({ data }) => data);
},
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import BlobContentError from './blob_content_error.vue';
export default {
components: {
GlLoadingIcon,
BlobContentError,
},
props: {
content: {
type: String,
default: '',
required: false,
},
loading: {
type: Boolean,
default: true,
required: false,
},
activeViewer: {
type: Object,
required: true,
},
},
computed: {
viewer() {
switch (this.activeViewer.type) {
case 'rich':
return RichViewer;
default:
return SimpleViewer;
}
},
viewerError() {
return this.activeViewer.renderError;
},
},
};
</script>
<template>
<div class="blob-viewer" :data-type="activeViewer.type">
<gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" />
<template v-else>
<blob-content-error v-if="viewerError" :viewer-error="viewerError" />
<component :is="viewer" v-else ref="contentViewer" :content="content" />
</template>
</div>
</template>
<script>
export default {
props: {
viewerError: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="file-content code">
<div class="text-center py-4" v-html="viewerError"></div>
</div>
</template>
......@@ -36,11 +36,6 @@ export default {
return this.activeViewer === RICH_BLOB_VIEWER;
},
},
methods: {
requestCopyContents() {
this.$emit('copy');
},
},
BTN_COPY_CONTENTS_TITLE,
BTN_DOWNLOAD_TITLE,
BTN_RAW_TITLE,
......@@ -53,7 +48,7 @@ export default {
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
@click="requestCopyContents"
data-clipboard-target="#blob-code-content"
>
<gl-icon name="copy-to-clipboard" :size="14" />
</gl-button>
......
......@@ -4,6 +4,6 @@ import 'jquery';
import 'jquery-ujs';
import 'vendor/jquery.endless-scroll';
import 'jquery.caret'; // must be imported before at.js
import 'at.js';
import '@gitlab/at.js';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
import $ from 'jquery';
import Vue from 'vue';
import Cookies from 'js-cookie';
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins';
import Flash from '../flash';
import { __ } from '~/locale';
......@@ -28,6 +28,7 @@ export default () => {
name: 'CycleAnalytics',
components: {
GlEmptyState,
GlLoadingIcon,
banner,
'stage-issue-component': stageComponent,
'stage-plan-component': stageComponent,
......
import $ from 'jquery';
import 'at.js';
import '@gitlab/at.js';
import _ from 'underscore';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp';
......
fragment BlobViewer on SnippetBlobViewer {
collapsed
loadingPartialName
renderError
tooLarge
type
fileType
}
......@@ -70,7 +70,7 @@ export default {
:title="$options.currentBranchPermissionsTooltip"
>
<span
class="ide-radio-label"
class="ide-option-label"
data-qa-selector="commit_to_current_branch_radio"
v-html="commitToCurrentBranchText"
></span>
......
<script>
import { createNamespacedHelpers } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
const {
mapState: mapCommitState,
mapActions: mapCommitActions,
mapGetters: mapCommitGetters,
} = createNamespacedHelpers('commit');
const { mapActions: mapCommitActions, mapGetters: mapCommitGetters } = createNamespacedHelpers(
'commit',
);
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
computed: {
...mapCommitState(['shouldCreateMR']),
...mapCommitGetters(['shouldHideNewMrOption']),
...mapCommitGetters(['shouldHideNewMrOption', 'shouldDisableNewMrOption', 'shouldCreateMR']),
tooltipText() {
if (this.shouldDisableNewMrOption) {
return s__(
'IDE|This option is disabled because you are not allowed to create merge requests in this project.',
);
}
return '';
},
},
methods: {
...mapCommitActions(['toggleShouldCreateMR']),
......@@ -21,14 +32,19 @@ export default {
<template>
<fieldset v-if="!shouldHideNewMrOption">
<hr class="my-2" />
<label class="mb-0 js-ide-commit-new-mr">
<label
v-gl-tooltip="tooltipText"
class="mb-0 js-ide-commit-new-mr"
:class="{ 'is-disabled': shouldDisableNewMrOption }"
>
<input
:disabled="shouldDisableNewMrOption"
:checked="shouldCreateMR"
type="checkbox"
data-qa-selector="start_new_mr_checkbox"
@change="toggleShouldCreateMR"
/>
<span class="prepend-left-10">
<span class="prepend-left-10 ide-option-label">
{{ __('Start a new merge request') }}
</span>
</label>
......
......@@ -67,7 +67,7 @@ export default {
@change="updateCommitAction($event.target.value)"
/>
<span class="prepend-left-10">
<span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot>
<span v-if="label" class="ide-option-label"> {{ label }} </span> <slot v-else></slot>
</span>
</label>
<div v-if="commitAction === value && showInput" class="ide-commit-new-branch">
......
<script>
import $ from 'jquery';
import { mapGetters } from 'vuex';
import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
......@@ -13,6 +14,9 @@ export default {
isVisibleDropdown: false,
};
},
computed: {
...mapGetters(['canReadMergeRequests']),
},
mounted() {
this.addDropdownListeners();
},
......@@ -42,7 +46,9 @@ export default {
<template>
<div ref="dropdown" class="btn-group ide-nav-dropdown dropdown">
<nav-dropdown-button />
<div class="dropdown-menu dropdown-menu-left p-0"><nav-form v-if="isVisibleDropdown" /></div>
<nav-dropdown-button :show-merge-requests="canReadMergeRequests" />
<div class="dropdown-menu dropdown-menu-left p-0">
<nav-form v-if="isVisibleDropdown" :show-merge-requests="canReadMergeRequests" />
</div>
</div>
</template>
......@@ -10,6 +10,13 @@ export default {
Icon,
DropdownButton,
},
props: {
showMergeRequests: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() {
......@@ -25,10 +32,10 @@ export default {
<template>
<dropdown-button>
<span class="row">
<span class="col-7 text-truncate">
<span class="col-auto text-truncate" :class="{ 'col-7': showMergeRequests }">
<icon :size="16" :aria-label="__('Current Branch')" name="branch" /> {{ branchLabel }}
</span>
<span class="col-5 pl-0 text-truncate">
<span v-if="showMergeRequests" class="col-5 pl-0 text-truncate">
<icon :size="16" :aria-label="__('Merge Request')" name="merge-request" />
{{ mergeRequestLabel }}
</span>
......
......@@ -11,12 +11,19 @@ export default {
BranchesSearchList,
MergeRequestSearchList,
},
props: {
showMergeRequests: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
<template>
<div class="ide-nav-form p-0">
<tabs stop-propagation>
<tabs v-if="showMergeRequests" stop-propagation>
<tab active>
<template slot="title">
{{ __('Branches') }}
......@@ -30,5 +37,6 @@ export default {
<merge-request-search-list />
</tab>
</tabs>
<branches-search-list v-else />
</div>
</template>
......@@ -8,6 +8,9 @@ export const MAX_BODY_LENGTH = 72;
export const FILE_VIEW_MODE_EDITOR = 'editor';
export const FILE_VIEW_MODE_PREVIEW = 'preview';
export const PERMISSION_CREATE_MR = 'createMergeRequestIn';
export const PERMISSION_READ_MR = 'readMergeRequest';
export const activityBarViews = {
edit: 'ide-tree',
commit: 'commit-section',
......
query getUserPermissions($projectPath: ID!) {
project(fullPath: $projectPath) {
userPermissions {
createMergeRequestIn,
readMergeRequest
}
}
}
import createGqClient, { fetchPolicies } from '~/lib/graphql';
export default createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
import Api from '~/api';
import getUserPermissions from '../queries/getUserPermissions.query.graphql';
import gqClient from './gql';
const fetchApiProjectData = projectPath => Api.project(projectPath).then(({ data }) => data);
const fetchGqlProjectData = projectPath =>
gqClient
.query({
query: getUserPermissions,
variables: { projectPath },
})
.then(({ data }) => data.project);
export default {
getFileData(endpoint) {
......@@ -47,7 +59,16 @@ export default {
.then(({ data }) => data);
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
const projectPath = `${namespace}/${project}`;
return Promise.all([fetchApiProjectData(projectPath), fetchGqlProjectData(projectPath)]).then(
([apiProjectData, gqlProjectData]) => ({
data: {
...apiProjectData,
...gqlProjectData,
},
}),
);
},
getProjectMergeRequests(projectId, params = {}) {
return Api.projectMergeRequests(projectId, params);
......
......@@ -2,10 +2,17 @@ import flash from '~/flash';
import { __ } from '~/locale';
import service from '../../services';
import * as types from '../mutation_types';
import { activityBarViews } from '../../constants';
import { activityBarViews, PERMISSION_READ_MR } from '../../constants';
export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branchId } = {}) =>
service
export const getMergeRequestsForBranch = (
{ commit, state, getters },
{ projectId, branchId } = {},
) => {
if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) {
return Promise.resolve();
}
return service
.getProjectMergeRequests(`${projectId}`, {
source_branch: branchId,
source_project_id: state.projects[projectId].id,
......@@ -36,6 +43,7 @@ export const getMergeRequestsForBranch = ({ commit, state }, { projectId, branch
);
throw e;
});
};
export const getMergeRequestData = (
{ commit, dispatch, state },
......
import { getChangesCountForFiles, filePathMatches } from './utils';
import { activityBarViews, packageJsonPath } from '../constants';
import {
activityBarViews,
packageJsonPath,
PERMISSION_READ_MR,
PERMISSION_CREATE_MR,
} from '../constants';
export const activeFile = state => state.openFiles.find(file => file.active) || null;
......@@ -141,5 +146,14 @@ export const getDiffInfo = (state, getters) => path => {
};
};
export const findProjectPermissions = (state, getters) => projectId =>
getters.findProject(projectId)?.userPermissions || {};
export const canReadMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]);
export const canCreateMergeRequests = (state, getters) =>
Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -158,7 +158,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
if (state.shouldCreateMR) {
if (getters.shouldCreateMR) {
const { currentProject } = rootGetters;
const targetBranch = getters.isCreatingNewBranch
? rootState.currentBranchId
......
......@@ -54,5 +54,11 @@ export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters)
(!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) &&
rootGetters.canPushToBranch;
export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) =>
!rootGetters.canCreateMergeRequests;
export const shouldCreateMR = (state, getters) =>
state.shouldCreateMR && !getters.shouldDisableNewMrOption;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -6,7 +6,7 @@ import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils';
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { PROMETHEUS_TIMEOUT } from '../constants';
......@@ -52,6 +52,8 @@ export const requestMetricsDashboard = ({ commit }) => {
export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard);
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(response.metrics_data));
return dispatch('fetchPrometheusMetrics', params);
};
export const receiveMetricsDashboardFailure = ({ commit }, error) => {
......
import Vue from 'vue';
import pick from 'lodash/pick';
import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils';
......@@ -174,15 +175,19 @@ export default {
state: emptyStateFromError(error),
});
},
[types.SET_ENDPOINTS](state, endpoints) {
state.metricsEndpoint = endpoints.metricsEndpoint;
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
state.dashboardEndpoint = endpoints.dashboardEndpoint;
state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
state.logsPath = endpoints.logsPath || state.logsPath;
[types.SET_ENDPOINTS](state, endpoints = {}) {
const endpointKeys = [
'metricsEndpoint',
'deploymentsEndpoint',
'dashboardEndpoint',
'dashboardsEndpoint',
'currentDashboard',
'projectPath',
'logsPath',
];
Object.entries(pick(endpoints, endpointKeys)).forEach(([key, value]) => {
state[key] = value;
});
},
[types.SET_TIME_RANGE](state, timeRange) {
state.timeRange = timeRange;
......
......@@ -15,7 +15,7 @@ import { escape, uniqueId } from 'lodash';
import Cookies from 'js-cookie';
import Autosize from 'autosize';
import 'jquery.caret'; // required by at.js
import 'at.js';
import '@gitlab/at.js';
import Vue from 'vue';
import { GlSkeletonLoading } from '@gitlab/ui';
import AjaxCache from '~/lib/utils/ajax_cache';
......
......@@ -26,7 +26,7 @@ export default class NotificationsForm {
.addClass('is-loading')
.find('.custom-notification-event-loading')
.removeClass('fa-check')
.addClass('fa-spin fa-spinner')
.addClass('spinner align-middle')
.removeClass('is-done');
}
......@@ -41,12 +41,12 @@ export default class NotificationsForm {
if (data.saved) {
$parent
.find('.custom-notification-event-loading')
.toggleClass('fa-spin fa-spinner fa-check is-done');
.toggleClass('spinner fa-check is-done align-middle');
setTimeout(() => {
$parent
.removeClass('is-loading')
.find('.custom-notification-event-loading')
.toggleClass('fa-spin fa-spinner fa-check is-done');
.toggleClass('spinner fa-check is-done align-middle');
}, 2000);
}
})
......
......@@ -3,9 +3,7 @@ import projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor';
document.addEventListener('DOMContentLoaded', () => {
if (gon.features && gon.features.selfMonitoringProject) {
selfMonitor();
}
selfMonitor();
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
......
import ZenMode from '~/zen_mode';
import initEditRelease from '~/releases/detail';
import initEditRelease from '~/releases/mount_edit';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
......
import initReleases from '~/releases/list';
import initReleases from '~/releases/mount_index';
document.addEventListener('DOMContentLoaded', initReleases);
......@@ -7,7 +7,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
export default {
name: 'ReleaseDetailApp',
name: 'ReleaseEditApp',
components: {
GlFormInput,
GlFormGroup,
......@@ -18,7 +18,7 @@ export default {
autofocusonshow,
},
computed: {
...mapState([
...mapState('detail', [
'isFetchingRelease',
'fetchError',
'markdownDocsPath',
......@@ -42,7 +42,7 @@ export default {
);
},
tagName() {
return this.$store.state.release.tagName;
return this.$store.state.detail.release.tagName;
},
tagNameHintText() {
return sprintf(
......@@ -60,7 +60,7 @@ export default {
},
releaseTitle: {
get() {
return this.$store.state.release.name;
return this.$store.state.detail.release.name;
},
set(title) {
this.updateReleaseTitle(title);
......@@ -68,7 +68,7 @@ export default {
},
releaseNotes: {
get() {
return this.$store.state.release.description;
return this.$store.state.detail.release.description;
},
set(notes) {
this.updateReleaseNotes(notes);
......@@ -79,7 +79,7 @@ export default {
this.fetchRelease();
},
methods: {
...mapActions([
...mapActions('detail', [
'fetchRelease',
'updateRelease',
'updateReleaseTitle',
......
......@@ -32,7 +32,7 @@ export default {
},
},
computed: {
...mapState(['isLoading', 'releases', 'hasError', 'pageInfo']),
...mapState('list', ['isLoading', 'releases', 'hasError', 'pageInfo']),
shouldRenderEmptyState() {
return !this.releases.length && !this.hasError && !this.isLoading;
},
......@@ -47,7 +47,7 @@ export default {
});
},
methods: {
...mapActions(['fetchReleases']),
...mapActions('list', ['fetchReleases']),
onChangePage(page) {
historyPushState(buildUrlWithCurrentLocation(`?page=${page}`));
this.fetchReleases({ page, projectId: this.projectId });
......
import Vue from 'vue';
import App from './components/app.vue';
import createStore from './store';
export default () => {
const element = document.getElementById('js-releases-page');
return new Vue({
el: element,
store: createStore(),
components: {
App,
},
render(createElement) {
return createElement('app', {
props: {
projectId: element.dataset.projectId,
documentationLink: element.dataset.documentationPath,
illustrationPath: element.dataset.illustrationPath,
},
});
},
});
};
import Vue from 'vue';
import ReleaseDetailApp from './components/app.vue';
import createStore from './store';
import ReleaseEditApp from './components/app_edit.vue';
import createStore from './stores';
import detailModule from './stores/modules/detail';
export default () => {
const el = document.getElementById('js-edit-release-page');
const store = createStore();
const store = createStore({ detail: detailModule });
store.dispatch('setInitialState', el.dataset);
return new Vue({
el,
store,
components: { ReleaseDetailApp },
render(createElement) {
return createElement('release-detail-app');
},
render: h => h(ReleaseEditApp),
});
};
import Vue from 'vue';
import ReleaseListApp from './components/app_index.vue';
import createStore from './stores';
import listModule from './stores/modules/list';
export default () => {
const el = document.getElementById('js-releases-page');
return new Vue({
el,
store: createStore({ list: listModule }),
render: h =>
h(ReleaseListApp, {
props: {
projectId: el.dataset.projectId,
documentationLink: el.dataset.documentationPath,
illustrationPath: el.dataset.illustrationPath,
},
}),
});
};
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default modules => new Vuex.Store({ modules });
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
mutations,
state,
});
export default {
namespaced: true,
actions,
mutations,
state,
};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
mutations,
state: state(),
});
export default {
namespaced: true,
actions,
mutations,
state,
};
......@@ -71,7 +71,12 @@ export default {
<template>
<div class="tree-content-holder">
<div class="table-holder bordered-box">
<table :aria-label="tableCaption" class="table tree-table qa-file-tree" aria-live="polite">
<table
:aria-label="tableCaption"
class="table tree-table"
aria-live="polite"
data-qa-selector="file_tree_table"
>
<table-header v-once />
<tbody>
<parent-row
......
......@@ -139,7 +139,13 @@ export default {
class="d-inline-block align-text-bottom fa-fw"
/>
<i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
<component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
<component
:is="linkComponent"
:to="routerLinkTo"
:href="url"
class="str-truncated"
data-qa-selector="file_name_link"
>
{{ fullPath }}
</component>
<!-- eslint-disable-next-line @gitlab/vue-i18n/no-bare-strings -->
......
......@@ -2,13 +2,19 @@
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue';
import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
import BlobContent from '~/blob/components/blob_content.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
export default {
components: {
BlobEmbeddable,
BlobHeader,
BlobContent,
GlLoadingIcon,
},
apollo: {
......@@ -20,6 +26,23 @@ export default {
};
},
update: data => data.snippets.edges[0].node.blob,
result(res) {
const viewer = res.data.snippets.edges[0].node.blob.richViewer
? RICH_BLOB_VIEWER
: SIMPLE_BLOB_VIEWER;
this.switchViewer(viewer, true);
},
},
blobContent: {
query: GetBlobContent,
variables() {
return {
ids: this.snippet.id,
rich: this.activeViewerType === RICH_BLOB_VIEWER,
};
},
update: data =>
data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData,
},
},
props: {
......@@ -31,6 +54,8 @@ export default {
data() {
return {
blob: {},
blobContent: '',
activeViewerType: window.location.hash ? SIMPLE_BLOB_VIEWER : '',
};
},
computed: {
......@@ -40,6 +65,18 @@ export default {
isBlobLoading() {
return this.$apollo.queries.blob.loading;
},
isContentLoading() {
return this.$apollo.queries.blobContent.loading;
},
viewer() {
const { richViewer, simpleViewer } = this.blob;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
},
},
methods: {
switchViewer(newViewer, respectHash = false) {
this.activeViewerType = respectHash && window.location.hash ? SIMPLE_BLOB_VIEWER : newViewer;
},
},
};
</script>
......@@ -49,11 +86,12 @@ export default {
<gl-loading-icon
v-if="isBlobLoading"
:label="__('Loading blob')"
:size="2"
size="lg"
class="prepend-top-20 append-bottom-20"
/>
<article v-else class="file-holder snippet-file-content">
<blob-header :blob="blob" />
<blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer" />
<blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" />
</article>
</div>
</template>
query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
snippets(ids: $ids) {
edges {
node {
id
blob {
richData @include(if: $rich)
plainData @skip(if: $rich)
}
}
}
}
}
export const HIGHLIGHT_CLASS_NAME = 'hll';
export default {};
import RichViewer from './rich_viewer.vue';
import SimpleViewer from './simple_viewer.vue';
export { RichViewer, SimpleViewer };
export default {
props: {
content: {
type: String,
required: true,
},
},
};
<script>
import ViewerMixin from './mixins';
export default {
mixins: [ViewerMixin],
};
</script>
<template>
<div v-html="content"></div>
</template>
<script>
import ViewerMixin from './mixins';
import { GlIcon } from '@gitlab/ui';
import { HIGHLIGHT_CLASS_NAME } from './constants';
export default {
components: {
GlIcon,
},
mixins: [ViewerMixin],
data() {
return {
highlightedLine: null,
};
},
computed: {
lineNumbers() {
return this.content.split('\n').length;
},
},
mounted() {
const { hash } = window.location;
if (hash) this.scrollToLine(hash, true);
},
methods: {
scrollToLine(hash, scroll = false) {
const lineToHighlight = hash && this.$el.querySelector(hash);
const currentlyHighlighted = this.highlightedLine;
if (lineToHighlight) {
if (currentlyHighlighted) {
currentlyHighlighted.classList.remove(HIGHLIGHT_CLASS_NAME);
}
lineToHighlight.classList.add(HIGHLIGHT_CLASS_NAME);
this.highlightedLine = lineToHighlight;
if (scroll) {
lineToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
},
},
userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
<div
class="file-content code js-syntax-highlight qa-file-content"
:class="$options.userColorScheme"
>
<div class="line-numbers">
<a
v-for="line in lineNumbers"
:id="`L${line}`"
:key="line"
class="diff-line-num js-line-number"
:href="`#LC${line}`"
:data-line-number="line"
@click="scrollToLine(`#LC${line}`)"
>
<gl-icon :size="12" name="link" />
{{ line }}
</a>
</div>
<div class="blob-content">
<pre class="code highlight"><code id="blob-code-content" v-html="content"></code></pre>
</div>
</div>
</template>
......@@ -11,7 +11,7 @@
// like a table or typography then make changes in the framework/ directory.
// If you need to add unique style that should affect only one page - use pages/
// directory.
@import "at.js/dist/css/jquery.atwho";
@import "@gitlab/at.js/dist/css/jquery.atwho";
@import "dropzone/dist/basic";
@import "select2/select2";
......
......@@ -158,6 +158,27 @@
}
}
// Temporary hack until `gitlab-ui` issue is fixed.
// https://gitlab.com/gitlab-org/gitlab-ui/issues/164
.gl-dropdown .dropdown-menu-toggle {
.gl-dropdown-caret {
position: absolute;
right: $gl-padding-8;
top: $gl-padding-8;
}
// Add some child to the button so that the default height kicks in
// when there's no text (since the caret is now aboslute)
&::after {
border: 0;
content: ' ';
display: inline-block;
margin: 0;
padding: 0;
position: relative;
}
}
@mixin dropdown-item-hover {
background-color: $gray-darker;
color: $gl-text-color;
......
......@@ -30,7 +30,6 @@
.line {
display: block;
width: 100%;
min-height: 1.5em;
padding-left: 10px;
padding-right: 10px;
white-space: pre;
......@@ -48,10 +47,10 @@
font-family: $monospace-font;
display: block;
font-size: $code-font-size !important;
min-height: 1.5em;
white-space: nowrap;
i {
i,
svg {
float: left;
margin-top: 3px;
margin-right: 5px;
......@@ -62,12 +61,20 @@
&:focus {
outline: none;
i {
i,
svg {
visibility: visible;
}
}
}
}
pre .line,
.line-numbers a {
font-size: 0.8125rem;
line-height: 1.1875rem;
min-height: 1.1875rem;
}
}
// Vertically aligns <table> line numbers (eg. blame view)
......
......@@ -63,7 +63,8 @@
display: block;
}
.select2-choices {
.select2-choices,
.select2-choice {
border-color: $red-500;
}
}
......
......@@ -688,7 +688,7 @@ $ide-commit-header-height: 48px;
font-weight: normal;
&.is-disabled {
.ide-radio-label {
.ide-option-label {
text-decoration: line-through;
}
}
......
......@@ -109,14 +109,6 @@
top: $gl-padding-top;
}
.fa-spinner {
font-size: 28px;
position: relative;
margin-left: -20px;
left: 50%;
margin-top: 36px;
}
.stage-panel-body {
display: flex;
flex-wrap: wrap;
......
......@@ -11,16 +11,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data]
before_action :validate_self_monitoring_feature_flag_enabled, only: [
:create_self_monitoring_project,
:status_create_self_monitoring_project,
:delete_self_monitoring_project,
:status_delete_self_monitoring_project
]
before_action do
push_frontend_feature_flag(:self_monitoring_project)
end
VALID_SETTING_PANELS = %w(general integrations repository
ci_cd reporting metrics_and_profiling
......@@ -66,7 +56,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def clear_repository_check_states
RepositoryCheck::ClearWorker.perform_async
RepositoryCheck::ClearWorker.perform_async # rubocop:disable CodeReuse/Worker
redirect_to(
general_admin_application_settings_path,
......@@ -83,7 +73,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# Specs are in spec/requests/self_monitoring_project_spec.rb
def create_self_monitoring_project
job_id = SelfMonitoringProjectCreateWorker.perform_async
job_id = SelfMonitoringProjectCreateWorker.perform_async # rubocop:disable CodeReuse/Worker
render status: :accepted, json: {
job_id: job_id,
......@@ -102,7 +92,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
}
end
if SelfMonitoringProjectCreateWorker.in_progress?(job_id)
if SelfMonitoringProjectCreateWorker.in_progress?(job_id) # rubocop:disable CodeReuse/Worker
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
return render status: :accepted, json: {
......@@ -122,7 +112,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# Specs are in spec/requests/self_monitoring_project_spec.rb
def delete_self_monitoring_project
job_id = SelfMonitoringProjectDeleteWorker.perform_async
job_id = SelfMonitoringProjectDeleteWorker.perform_async # rubocop:disable CodeReuse/Worker
render status: :accepted, json: {
job_id: job_id,
......@@ -141,7 +131,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
}
end
if SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
if SelfMonitoringProjectDeleteWorker.in_progress?(job_id) # rubocop:disable CodeReuse/Worker
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
return render status: :accepted, json: {
......@@ -163,10 +153,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
private
def validate_self_monitoring_feature_flag_enabled
self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
end
def self_monitoring_data
{
project_id: @application_setting.self_monitoring_project_id,
......@@ -174,16 +160,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
}
end
def self_monitoring_project_not_implemented
render(
status: :not_implemented,
json: {
message: _('Self-monitoring is not enabled on this GitLab server, contact your administrator.'),
documentation_url: help_page_path('administration/monitoring/gitlab_self_monitoring_project/index')
}
)
end
def set_application_setting
@application_setting = ApplicationSetting.current_without_cache
end
......
......@@ -55,7 +55,7 @@ class Admin::ProjectsController < Admin::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def repository_check
RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id)
RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id) # rubocop:disable CodeReuse/Worker
redirect_to(
admin_project_path(@project),
......
......@@ -19,7 +19,7 @@ class Admin::ServicesController < Admin::ApplicationController
def update
if service.update(service_params[:service])
PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
PropagateServiceTemplateWorker.perform_async(service.id) if service.active? # rubocop:disable CodeReuse/Worker
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
......
......@@ -5,6 +5,7 @@
module MetricsDashboard
include RenderServiceResults
include ChecksCollaboration
include EnvironmentsHelper
extend ActiveSupport::Concern
......@@ -15,8 +16,9 @@ module MetricsDashboard
metrics_dashboard_params.to_h.symbolize_keys
)
if include_all_dashboards? && result
result[:all_dashboards] = all_dashboards
if result
result[:all_dashboards] = all_dashboards if include_all_dashboards?
result[:metrics_data] = metrics_data(project_for_dashboard, environment_for_dashboard) if project_for_dashboard && environment_for_dashboard
end
respond_to do |format|
......@@ -76,10 +78,14 @@ module MetricsDashboard
defined?(project) ? project : nil
end
def environment_for_dashboard
defined?(environment) ? environment : nil
end
def dashboard_success_response(result)
{
status: :ok,
json: result.slice(:all_dashboards, :dashboard, :status)
json: result.slice(:all_dashboards, :dashboard, :status, :metrics_data)
}
end
......
......@@ -11,7 +11,7 @@ module SpammableActions
end
def mark_as_spam
if Spam::MarkAsSpamService.new(target: spammable).execute
if Spam::MarkAsSpamService.new(spammable: spammable).execute
redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase }
else
redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
......@@ -42,7 +42,7 @@ module SpammableActions
end
format.json do
locals = { target: spammable, script: false, has_submit: false }
locals = { spammable: spammable, script: false, has_submit: false }
recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals)
render json: { recaptcha_html: recaptcha_html }
......
......@@ -188,7 +188,7 @@ class Projects::IssuesController < Projects::ApplicationController
def import_csv
if uploader = UploadService.new(project, params[:file]).execute
ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id)
ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id) # rubocop:disable CodeReuse/Worker
flash[:notice] = _("Your issues are being imported. Once finished, you'll get a confirmation email.")
else
......
......@@ -47,7 +47,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
end
def play
job_id = RunPipelineScheduleWorker.perform_async(schedule.id, current_user.id)
job_id = RunPipelineScheduleWorker.perform_async(schedule.id, current_user.id) # rubocop:disable CodeReuse/Worker
if job_id
pipelines_link_start = "<a href=\"#{project_pipelines_path(@project)}\">"
......
......@@ -32,7 +32,7 @@ module Projects
end
def destroy
DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id)
DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id) # rubocop:disable CodeReuse/Worker
track_event(:delete_repository)
respond_to do |format|
......
......@@ -69,7 +69,9 @@ module Projects
return
end
# rubocop:disable CodeReuse/Worker
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
# rubocop:enable CodeReuse/Worker
pipelines_link_start = '<a href="%{url}">'.html_safe % { url: project_pipelines_path(@project) }
flash[:toast] = _("A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details") % { pipelines_link_start: pipelines_link_start, pipelines_link_end: "</a>".html_safe }
......
......@@ -25,7 +25,7 @@ module Projects
result = Projects::UpdateService.new(project, current_user, cleanup_params).execute
if result[:status] == :success
RepositoryCleanupWorker.perform_async(project.id, current_user.id)
RepositoryCleanupWorker.perform_async(project.id, current_user.id) # rubocop:disable CodeReuse/Worker
flash[:notice] = _('Repository cleanup has started. You will receive an email once the cleanup operation is complete.')
else
flash[:alert] = _('Failed to upload object map file')
......
......@@ -80,7 +80,7 @@ module Repositories
return unless repo_type.project?
return unless project&.daily_statistics_enabled?
ProjectDailyStatisticsWorker.perform_async(project.id)
ProjectDailyStatisticsWorker.perform_async(project.id) # rubocop:disable CodeReuse/Worker
end
def access
......
......@@ -24,7 +24,7 @@ module Mutations
private
def mark_as_spam(snippet)
Spam::MarkAsSpamService.new(target: snippet).execute
Spam::MarkAsSpamService.new(spammable: snippet).execute
end
def authorized_resource?(snippet)
......
......@@ -31,7 +31,7 @@ module AnalyticsNavbarHelper
end
def cycle_analytics_navbar_link(project, current_user)
return unless Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, project)
return unless Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, project, default_enabled: true)
return unless project_nav_tab?(:cycle_analytics)
navbar_sub_item(
......@@ -43,7 +43,7 @@ module AnalyticsNavbarHelper
end
def repository_analytics_navbar_link(project, current_user)
return if Feature.disabled?(:analytics_pages_under_project_analytics_sidebar, project)
return if Feature.disabled?(:analytics_pages_under_project_analytics_sidebar, project, default_enabled: true)
return if project.empty_repo?
navbar_sub_item(
......@@ -55,7 +55,7 @@ module AnalyticsNavbarHelper
end
def ci_cd_analytics_navbar_link(project, current_user)
return unless Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, project)
return unless Feature.enabled?(:analytics_pages_under_project_analytics_sidebar, project, default_enabled: true)
return unless project_nav_tab?(:pipelines)
return unless project.feature_available?(:builds, current_user) || !project.empty_repo?
......
# frozen_string_literal: true
module EnvironmentsHelper
include ActionView::Helpers::AssetUrlHelper
prepend_if_ee('::EE::EnvironmentsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
def environments_list_data
......@@ -21,7 +22,7 @@ module EnvironmentsHelper
{
"settings-path" => edit_project_service_path(project, 'prometheus'),
"clusters-path" => project_clusters_path(project),
"current-environment-name": environment.name,
"current-environment-name" => environment.name,
"documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
......
......@@ -818,7 +818,7 @@ module Ci
depended_jobs = depends_on_builds
# find all jobs that are needed
if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists?
if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && scheduling_type_dag?
depended_jobs = depended_jobs.where(name: needs.artifacts.select(:name))
end
......
......@@ -12,6 +12,18 @@ module Ci
scope :preload_needs, -> { preload(:needs) }
scope :with_needs, -> (names = nil) do
needs = Ci::BuildNeed.scoped_build.select(1)
needs = needs.where(name: names) if names
where('EXISTS (?)', needs).preload(:needs)
end
scope :without_needs, -> (names = nil) do
needs = Ci::BuildNeed.scoped_build.select(1)
needs = needs.where(name: names) if names
where('NOT EXISTS (?)', needs)
end
def self.select_with_aggregated_needs(project)
return all unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
......@@ -26,6 +38,18 @@ module Ci
)
end
# Old processables may have scheduling_type as nil,
# so we need to ensure the data exists before using it.
def self.populate_scheduling_type!
needs = Ci::BuildNeed.scoped_build.select(1)
where(scheduling_type: nil).update_all(
"scheduling_type = CASE WHEN (EXISTS (#{needs.to_sql}))
THEN #{scheduling_types[:dag]}
ELSE #{scheduling_types[:stage]}
END"
)
end
validates :type, presence: true
validates :scheduling_type, presence: true, on: :create, if: :validate_scheduling_type?
......@@ -53,6 +77,11 @@ module Ci
raise NotImplementedError
end
# Overriding scheduling_type enum's method for nil `scheduling_type`s
def scheduling_type_dag?
super || find_legacy_scheduling_type == :dag
end
# scheduling_type column of previous builds/bridges have not been populated,
# so we calculate this value on runtime when we need it.
def find_legacy_scheduling_type
......
......@@ -62,18 +62,6 @@ class CommitStatus < ApplicationRecord
preload(project: :namespace)
end
scope :with_needs, -> (names = nil) do
needs = Ci::BuildNeed.scoped_build.select(1)
needs = needs.where(name: names) if names
where('EXISTS (?)', needs).preload(:needs)
end
scope :without_needs, -> (names = nil) do
needs = Ci::BuildNeed.scoped_build.select(1)
needs = needs.where(name: names) if names
where('NOT EXISTS (?)', needs)
end
scope :match_id_and_lock_version, -> (slice) do
# it expects that items are an array of attributes to match
# each hash needs to have `id` and `lock_version`
......
......@@ -42,6 +42,22 @@ class Label < ApplicationRecord
scope :order_name_desc, -> { reorder(title: :desc) }
scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) }
scope :top_labels_by_target, -> (target_relation) {
label_id_column = arel_table[:id]
# Window aggregation to count labels
count_by_id = Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('count', [label_id_column]),
Arel::Nodes::Window.new.partition(label_id_column)
).as('count_by_id')
select(arel_table[Arel.star], count_by_id)
.joins(:label_links)
.merge(LabelLink.where(target: target_relation))
.reorder(count_by_id: :desc)
.distinct
}
def self.prioritized(project)
joins(:priorities)
.where(label_priorities: { project_id: project })
......
......@@ -6,9 +6,9 @@ class YoutrackService < IssueTrackerService
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
def self.reference_pattern(only_long: false)
if only_long
/(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+)/
/(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)/
else
/(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+)|(#{Issue.reference_prefix}(?<issue>\d+))/
/(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)|(#{Issue.reference_prefix}(?<issue>\d+))/
end
end
......
......@@ -1226,7 +1226,8 @@ class User < ApplicationRecord
{
name: name,
username: username,
avatar_url: avatar_url(only_path: false)
avatar_url: avatar_url(only_path: false),
email: email
}
end
......
......@@ -61,7 +61,7 @@ module Ci
Ci::ProcessPipelineService
.new(pipeline)
.execute
.execute(nil, initial_process: true)
end
end
......
......@@ -93,9 +93,9 @@ module Ci
end
def processable_status(processable)
if needs_names = processable.aggregated_needs_names
if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && processable.scheduling_type_dag?
# Processable uses DAG, get status of all dependent needs
@collection.status_for_names(needs_names)
@collection.status_for_names(processable.aggregated_needs_names.to_a)
else
# Processable uses Stages, get status of prior stage
@collection.status_for_prior_stage_position(processable.stage_idx.to_i)
......
......@@ -11,12 +11,13 @@ module Ci
@pipeline = pipeline
end
def execute(trigger_build_ids = nil)
success = process_stages_without_needs
def execute(trigger_build_ids = nil, initial_process: false)
success = process_stages_for_stage_scheduling
# we evaluate dependent needs,
# only when the another job has finished
success = process_builds_with_needs(trigger_build_ids) || success
success = process_dag_builds_without_needs || success if initial_process
success = process_dag_builds_with_needs(trigger_build_ids) || success
@pipeline.update_legacy_status
......@@ -25,23 +26,31 @@ module Ci
private
def process_stages_without_needs
stage_indexes_of_created_processables_without_needs.flat_map do |index|
process_stage_without_needs(index)
def process_stages_for_stage_scheduling
stage_indexes_of_created_stage_scheduled_processables.flat_map do |index|
process_stage_for_stage_scheduling(index)
end.any?
end
def process_stage_without_needs(index)
def process_stage_for_stage_scheduling(index)
current_status = status_for_prior_stages(index)
return unless HasStatus::COMPLETED_STATUSES.include?(current_status)
created_processables_in_stage_without_needs(index).find_each.select do |build|
created_stage_scheduled_processables_in_stage(index).find_each.select do |build|
process_build(build, current_status)
end.any?
end
def process_builds_with_needs(trigger_build_ids)
def process_dag_builds_without_needs
return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
created_processables.scheduling_type_dag.without_needs.each do |build|
process_build(build, 'success')
end
end
def process_dag_builds_with_needs(trigger_build_ids)
return false unless trigger_build_ids.present?
return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true)
......@@ -56,14 +65,15 @@ module Ci
# Each found processable is guaranteed here to have completed status
created_processables
.scheduling_type_dag
.with_needs(trigger_build_names)
.without_needs(incomplete_build_names)
.find_each
.map(&method(:process_build_with_needs))
.map(&method(:process_dag_build_with_needs))
.any?
end
def process_build_with_needs(build)
def process_dag_build_with_needs(build)
current_status = status_for_build_needs(build.needs.map(&:name))
return unless HasStatus::COMPLETED_STATUSES.include?(current_status)
......@@ -87,23 +97,23 @@ module Ci
end
# rubocop: disable CodeReuse/ActiveRecord
def stage_indexes_of_created_processables_without_needs
created_processables_without_needs.order(:stage_idx)
def stage_indexes_of_created_stage_scheduled_processables
created_stage_scheduled_processables.order(:stage_idx)
.pluck(Arel.sql('DISTINCT stage_idx'))
end
# rubocop: enable CodeReuse/ActiveRecord
def created_processables_in_stage_without_needs(index)
created_processables_without_needs
def created_stage_scheduled_processables_in_stage(index)
created_stage_scheduled_processables
.with_preloads
.for_stage(index)
end
def created_processables_without_needs
def created_stage_scheduled_processables
if Feature.enabled?(:ci_dag_support, project, default_enabled: true)
pipeline.processables.created.without_needs
created_processables.scheduling_type_stage
else
pipeline.processables.created
created_processables
end
end
......
......@@ -3,7 +3,7 @@
module Ci
class ProcessBuildService < BaseService
def execute(build, current_status)
if valid_statuses_for_when(build.when).include?(current_status)
if valid_statuses_for_build(build).include?(current_status)
if build.schedulable?
build.schedule
elsif build.action?
......@@ -25,10 +25,10 @@ module Ci
build.enqueue
end
def valid_statuses_for_when(value)
case value
def valid_statuses_for_build(build)
case build.when
when 'on_success'
%w[success skipped]
build.scheduling_type_dag? ? %w[success] : %w[success skipped]
when 'on_failure'
%w[failed]
when 'always'
......
......@@ -8,8 +8,9 @@ module Ci
@pipeline = pipeline
end
def execute(trigger_build_ids = nil)
def execute(trigger_build_ids = nil, initial_process: false)
update_retried
ensure_scheduling_type_for_processables
if Feature.enabled?(:ci_atomic_processing, pipeline.project)
Ci::PipelineProcessing::AtomicProcessingService
......@@ -18,7 +19,7 @@ module Ci
else
Ci::PipelineProcessing::LegacyProcessingService
.new(pipeline)
.execute(trigger_build_ids)
.execute(trigger_build_ids, initial_process: initial_process)
end
end
......@@ -43,5 +44,17 @@ module Ci
.update_all(retried: true) if latest_statuses.any?
end
# rubocop: enable CodeReuse/ActiveRecord
# Set scheduling type of processables if they were created before scheduling_type
# data was deployed (https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22246).
# Given that this service runs multiple times during the pipeline
# life cycle we need to ensure we populate the data once.
# See more: https://gitlab.com/gitlab-org/gitlab/issues/205426
def ensure_scheduling_type_for_processables
lease = Gitlab::ExclusiveLease.new("set-scheduling-types:#{pipeline.id}", timeout: 1.hour.to_i)
return unless lease.try_obtain
pipeline.processables.populate_scheduling_type!
end
end
end
......@@ -36,7 +36,7 @@ module Ci
Ci::ProcessPipelineService
.new(pipeline)
.execute(completed_build_ids)
.execute(completed_build_ids, initial_process: true)
end
end
end
# frozen_string_literal: true
module AkismetMethods
def target_owner
@user ||= User.find(target.author_id)
def spammable_owner
@user ||= User.find(spammable.author_id)
end
def akismet
@akismet ||= Spam::AkismetService.new(
target_owner.name,
target_owner.email,
target.try(:spammable_text) || target&.text,
spammable_owner.name,
spammable_owner.email,
spammable.try(:spammable_text) || spammable&.text,
options
)
end
......
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.
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