Commit 92db820a authored by James Fargher's avatar James Fargher

Merge branch 'master' into '338949-remove-subtransactions-in-user'

# Conflicts:
#   app/services/users/migrate_to_ghost_user_service.rb
parents 0fdfb125 13646226
...@@ -201,7 +201,7 @@ Dangerfile @gl-quality/eng-prod ...@@ -201,7 +201,7 @@ Dangerfile @gl-quality/eng-prod
/lib/gitlab/auth/ldap/ @dblessing @mkozono /lib/gitlab/auth/ldap/ @dblessing @mkozono
[Templates] [Templates]
/lib/gitlab/ci/templates/ @nolith @shinya.maeda @matteeyah /lib/gitlab/ci/templates/ @gitlab-org/maintainers/cicd-templates
/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah /lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @DylanGriffith @mayra-cabrera @tkuah
/lib/gitlab/ci/templates/Security/ @gonzoyumo @twoodham @sethgitlab @thiagocsf /lib/gitlab/ci/templates/Security/ @gonzoyumo @twoodham @sethgitlab @thiagocsf
/lib/gitlab/ci/templates/Security/Container-Scanning.*.yml @gitlab-org/protect/container-security-backend /lib/gitlab/ci/templates/Security/Container-Scanning.*.yml @gitlab-org/protect/container-security-backend
......
<!--
Follow the documentation workflow https://docs.gitlab.com/ee/development/documentation/workflow.html
Additional information is located at https://docs.gitlab.com/ee/development/documentation/
To find the designated Tech Writer for the stage/group, see
https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
Mention "documentation" or "docs" in the MR title
For changing documentation location use the Change Documentation Location.md template
-->
## What does this MR do? ## What does this MR do?
<!-- Briefly describe what this MR is about. --> <!-- Briefly describe what this MR is about. -->
...@@ -18,12 +8,13 @@ ...@@ -18,12 +8,13 @@
## Author's checklist ## Author's checklist
- Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4) - [ ] Consider taking [the GitLab Technical Writing Fundamentals course](https://gitlab.edcast.com/pathways/ECL-02528ee2-c334-4e16-abf3-e9d8b8260de4)
- [ ] Follow the: - [ ] Follow the:
- [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/). - [Documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html).
- [Documentation guidelines](https://docs.gitlab.com/ee/development/documentation/).
- [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/). - [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide/).
- [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`. - [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#product-tier-badges) is added to topic's `h1`.
- [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on the: - [ ] [Request a review](https://docs.gitlab.com/ee/development/code_review.html#dogfooding-the-reviewers-feature) based on:
- The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata). - The documentation page's [metadata](https://docs.gitlab.com/ee/development/documentation/#metadata).
- The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments). - The [associated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
......
...@@ -712,3 +712,8 @@ QA/SelectorUsage: ...@@ -712,3 +712,8 @@ QA/SelectorUsage:
- 'ee/spec/**/*.rb' - 'ee/spec/**/*.rb'
Exclude: Exclude:
- 'spec/rubocop/**/*_spec.rb' - 'spec/rubocop/**/*_spec.rb'
Performance/ActiveRecordSubtransactions:
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 14.2.1 (2021-08-23)
### Fixed (1 change)
- [Drop un-used db/ci_migrate symlink](gitlab-org/gitlab@1154311625345e120407c0c397c7d4a27848a739) ([merge request](gitlab-org/gitlab!68723))
### Changed (2 changes)
- [Reorder vuln check criteria](gitlab-org/gitlab@9bbb20db46362a859632e7bb88deba985318ca2c) ([merge request](gitlab-org/gitlab!68723)) **GitLab Enterprise Edition**
- [Don't override vulnerability feedback UUID anymore](gitlab-org/gitlab@5f8372fb782c9416ae5ab582009a4399cb7d3750) ([merge request](gitlab-org/gitlab!68723)) **GitLab Enterprise Edition**
## 14.2.0 (2021-08-20) ## 14.2.0 (2021-08-20)
### Added (128 changes) ### Added (128 changes)
......
48d7984d9912c935a2c2abba3b55593cf0be2d8e bb2e3f4a916f031f38c9fb1c4fc955f50f0e4275
...@@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue'; ...@@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue';
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
}); });
export default () => { export default () => {
......
...@@ -22,6 +22,7 @@ export default { ...@@ -22,6 +22,7 @@ export default {
<img <img
data-testid="image" data-testid="image"
class="gl-max-w-full gl-h-auto" class="gl-max-w-full gl-h-auto"
:title="node.attrs.title"
:class="{ 'gl-opacity-5': node.attrs.uploading }" :class="{ 'gl-opacity-5': node.attrs.uploading }"
:src="node.attrs.src" :src="node.attrs.src"
/> />
......
...@@ -50,6 +50,16 @@ export default Image.extend({ ...@@ -50,6 +50,16 @@ export default Image.extend({
}; };
}, },
}, },
title: {
default: null,
parseHTML: (element) => {
const img = resolveImageEl(element);
return {
title: img.getAttribute('title'),
};
},
},
}; };
}, },
parseHTML() { parseHTML() {
......
...@@ -42,6 +42,14 @@ export default Link.extend({ ...@@ -42,6 +42,14 @@ export default Link.extend({
}; };
}, },
}, },
title: {
title: null,
parseHTML: (element) => {
return {
title: element.getAttribute('title'),
};
},
},
canonicalSrc: { canonicalSrc: {
default: null, default: null,
parseHTML: (element) => { parseHTML: (element) => {
......
...@@ -232,7 +232,7 @@ export function insertMarkdownText({ ...@@ -232,7 +232,7 @@ export function insertMarkdownText({
.join('\n'); .join('\n');
} }
} else if (tag.indexOf(textPlaceholder) > -1) { } else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, () => selected); textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
} else { } else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : ''); textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
} }
......
...@@ -417,43 +417,6 @@ export const urlParamsToArray = (path = '') => ...@@ -417,43 +417,6 @@ export const urlParamsToArray = (path = '') =>
export const getUrlParamsArray = () => urlParamsToArray(window.location.search); export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
/**
* Accepts encoding string which includes query params being
* sent to URL.
*
* @param {string} path Query param string
*
* @returns {object} Query params object containing key-value pairs
* with both key and values decoded into plain string.
*
* @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845
*/
export const urlParamsToObject = (path = '') =>
splitPath(path).reduce((dataParam, filterParam) => {
if (filterParam === '') {
return dataParam;
}
const data = dataParam;
let [key, value] = filterParam.split('=');
key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
const isArray = key.includes('[]');
key = key.replace('[]', '');
value = decodeURIComponent(value.replace(/\+/g, ' '));
if (isArray) {
if (!data[key]) {
data[key] = [];
}
data[key].push(value);
} else {
data[key] = value;
}
return data;
}, {});
/** /**
* Convert search query into an object * Convert search query into an object
* *
......
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
import storageCounter from '~/projects/storage_counter';
import initSearchSettings from '~/search_settings';
const initLinkedTabs = () => {
if (!document.querySelector('.js-usage-quota-tabs')) {
return false;
}
return new LinkedTabs({
defaultAction: '#storage-quota-tab',
parentEl: '.js-usage-quota-tabs',
hashedTabs: true,
});
};
const initVueApp = () => {
storageCounter('js-project-storage-count-app');
};
initVueApp();
initLinkedTabs();
initSearchSettings();
...@@ -56,7 +56,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { ...@@ -56,7 +56,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, { typeDefs, useGet: true }), defaultClient: createDefaultClient(resolvers, {
typeDefs,
useGet: true,
assumeImmutableResults: true,
}),
}); });
const { cache } = apolloProvider.clients.defaultClient; const { cache } = apolloProvider.clients.defaultClient;
......
...@@ -134,7 +134,7 @@ export default { ...@@ -134,7 +134,7 @@ export default {
update(data) { update(data) {
const { ciConfig } = data || {}; const { ciConfig } = data || {};
const stageNodes = ciConfig?.stages?.nodes || []; const stageNodes = ciConfig?.stages?.nodes || [];
const stages = unwrapStagesWithNeeds(stageNodes); const stages = unwrapStagesWithNeeds(JSON.parse(JSON.stringify(stageNodes)));
return { ...ciConfig, stages }; return { ...ciConfig, stages };
}, },
......
import { get } from 'lodash';
import { REST, GRAPHQL } from './constants';
const accessors = {
[REST]: {
detailsPath: 'details_path',
groupId: 'id',
hasDetails: 'has_details',
pipelineStatus: ['details', 'status'],
sourceJob: ['source_job', 'name'],
},
[GRAPHQL]: {
detailsPath: 'detailsPath',
groupId: 'name',
hasDetails: 'hasDetails',
pipelineStatus: 'status',
sourceJob: ['sourceJob', 'name'],
},
};
const accessValue = (dataMethod, prop, item) => {
return get(item, accessors[dataMethod][prop]);
};
export { accessors, accessValue };
...@@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream'; ...@@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream';
*/ */
export const ONE_COL_WIDTH = 180; export const ONE_COL_WIDTH = 180;
export const REST = 'rest';
export const GRAPHQL = 'graphql';
export const STAGE_VIEW = 'stage'; export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer'; export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type'; export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
......
...@@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue'; ...@@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue'; import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue'; import JobNameComponent from '../jobs_shared/job_name_component.vue';
import { accessValue } from './accessors'; import { SINGLE_JOB } from './constants';
import { REST, SINGLE_JOB } from './constants';
/** /**
* Renders the badge for the pipeline graph and the job's dropdown. * Renders the badge for the pipeline graph and the job's dropdown.
...@@ -47,11 +46,6 @@ export default { ...@@ -47,11 +46,6 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [delayedJobMixin], mixins: [delayedJobMixin],
inject: {
dataMethod: {
default: REST,
},
},
props: { props: {
job: { job: {
type: Object, type: Object,
...@@ -111,10 +105,10 @@ export default { ...@@ -111,10 +105,10 @@ export default {
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : ''; return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
}, },
detailsPath() { detailsPath() {
return accessValue(this.dataMethod, 'detailsPath', this.status); return this.status.detailsPath;
}, },
hasDetails() { hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status); return this.status.hasDetails;
}, },
isSingleItem() { isSingleItem() {
return this.type === SINGLE_JOB; return this.type === SINGLE_JOB;
......
...@@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; ...@@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue'; import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import { accessValue } from './accessors'; import { DOWNSTREAM, UPSTREAM } from './constants';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
export default { export default {
directives: { directives: {
...@@ -18,11 +17,6 @@ export default { ...@@ -18,11 +17,6 @@ export default {
GlLoadingIcon, GlLoadingIcon,
GlBadge, GlBadge,
}, },
inject: {
dataMethod: {
default: REST,
},
},
props: { props: {
columnTitle: { columnTitle: {
type: String, type: String,
...@@ -40,20 +34,9 @@ export default { ...@@ -40,20 +34,9 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
/*
The next two props will be removed or required
once the graph transition is done.
See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
*/
isLoading: { isLoading: {
type: Boolean, type: Boolean,
required: false, required: true,
default: false,
},
projectId: {
type: Number,
required: false,
default: -1,
}, },
}, },
computed: { computed: {
...@@ -65,7 +48,7 @@ export default { ...@@ -65,7 +48,7 @@ export default {
return `js-linked-pipeline-${this.pipeline.id}`; return `js-linked-pipeline-${this.pipeline.id}`;
}, },
pipelineStatus() { pipelineStatus() {
return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline); return this.pipeline.status;
}, },
projectName() { projectName() {
return this.pipeline.project.name; return this.pipeline.project.name;
...@@ -97,12 +80,10 @@ export default { ...@@ -97,12 +80,10 @@ export default {
return this.type === UPSTREAM; return this.type === UPSTREAM;
}, },
isSameProject() { isSameProject() {
return this.projectId > -1 return !this.pipeline.multiproject;
? this.projectId === this.pipeline.project.id
: !this.pipeline.multiproject;
}, },
sourceJobName() { sourceJobName() {
return accessValue(this.dataMethod, 'sourceJob', this.pipeline); return this.pipeline.sourceJob?.name ?? '';
}, },
sourceJobInfo() { sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : ''; return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
......
...@@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; ...@@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
import ActionComponent from '../jobs_shared/action_component.vue'; import ActionComponent from '../jobs_shared/action_component.vue';
import { accessValue } from './accessors';
import { GRAPHQL } from './constants';
import JobGroupDropdown from './job_group_dropdown.vue'; import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue'; import JobItem from './job_item.vue';
...@@ -97,7 +95,7 @@ export default { ...@@ -97,7 +95,7 @@ export default {
}, },
methods: { methods: {
getGroupId(group) { getGroupId(group) {
return accessValue(GRAPHQL, 'groupId', group); return group.name;
}, },
groupId(group) { groupId(group) {
return `ci-badge-${escape(group.name)}`; return `ci-badge-${escape(group.name)}`;
......
import Vue from 'vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
import TestReports from './components/test_reports/test_reports.vue';
import createDagApp from './pipeline_details_dag'; import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph'; import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header'; import { createPipelineHeaderApp } from './pipeline_details_header';
import { apolloProvider } from './pipeline_shared_client'; import { apolloProvider } from './pipeline_shared_client';
import createTestReportsStore from './stores/test_reports'; import { createTestDetails } from './pipeline_test_details';
Vue.use(Translate);
const SELECTORS = { const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue', PIPELINE_DETAILS: '.js-pipeline-details-vue',
...@@ -19,33 +13,6 @@ const SELECTORS = { ...@@ -19,33 +13,6 @@ const SELECTORS = {
PIPELINE_TESTS: '#js-pipeline-tests-detail', PIPELINE_TESTS: '#js-pipeline-tests-detail',
}; };
const createTestDetails = () => {
const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
suiteEndpoint,
});
// eslint-disable-next-line no-new
new Vue({
el,
components: {
TestReports,
},
provide: {
emptyStateImagePath,
hasTestReport: parseBoolean(hasTestReport),
},
store: testReportsStore,
render(createElement) {
return createElement('test-reports');
},
});
};
export default async function initPipelineDetailsBundle() { export default async function initPipelineDetailsBundle() {
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS); const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
...@@ -65,6 +32,27 @@ export default async function initPipelineDetailsBundle() { ...@@ -65,6 +32,27 @@ export default async function initPipelineDetailsBundle() {
}); });
} }
try {
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
});
}
try {
createDagApp(apolloProvider); createDagApp(apolloProvider);
createTestDetails(); } catch {
createFlash({
message: __('An error occurred while loading the Needs tab.'),
});
}
try {
createTestDetails(SELECTORS.PIPELINE_TESTS);
} catch {
createFlash({
message: __('An error occurred while loading the Test Reports tab.'),
});
}
} }
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GRAPHQL } from './components/graph/constants';
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue'; import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { reportToSentry } from './utils'; import { reportToSentry } from './utils';
...@@ -23,7 +22,6 @@ const createPipelinesDetailApp = ( ...@@ -23,7 +22,6 @@ const createPipelinesDetailApp = (
pipelineProjectPath, pipelineProjectPath,
pipelineIid, pipelineIid,
graphqlResourceEtag, graphqlResourceEtag,
dataMethod: GRAPHQL,
}, },
errorCaptured(err, _vm, info) { errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`); reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
......
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate';
import TestReports from './components/test_reports/test_reports.vue';
import createTestReportsStore from './stores/test_reports';
Vue.use(Translate);
export const createTestDetails = (selector) => {
const el = document.querySelector(selector);
const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
el?.dataset || {};
const testReportsStore = createTestReportsStore({
blobPath,
summaryEndpoint,
suiteEndpoint,
});
// eslint-disable-next-line no-new
new Vue({
el,
components: {
TestReports,
},
provide: {
emptyStateImagePath,
hasTestReport: parseBoolean(hasTestReport),
},
store: testReportsStore,
render(createElement) {
return createElement('test-reports');
},
});
};
...@@ -5,7 +5,12 @@ import createDefaultClient from '~/lib/graphql'; ...@@ -5,7 +5,12 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(
{},
{
assumeImmutableResults: true,
},
),
}); });
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => { export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
......
<script>
import { s__ } from '~/locale';
export default {
name: 'StorageCounterApp',
i18n: {
placeholder: s__('UsageQuota|Usage'),
},
};
</script>
<template>
<div>{{ $options.i18n.placeholder }}</div>
</template>
import Vue from 'vue';
import StorageCounterApp from './components/app.vue';
export default (containerId = 'js-project-storage-count-app') => {
const el = document.getElementById(containerId);
if (!el) {
return false;
}
return new Vue({
el,
render(createElement) {
return createElement(StorageCounterApp);
},
});
};
...@@ -2,12 +2,16 @@ ...@@ -2,12 +2,16 @@
import createFlash from '~/flash'; import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility'; import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, __ } from '~/locale';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue'; import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue'; import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue'; import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue'; import RunnerTypeHelp from '../components/runner_type_help.vue';
import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants'; import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql'; import getRunnersQuery from '../graphql/get_runners.query.graphql';
import { import {
fromUrlQueryToSearch, fromUrlQueryToSearch,
...@@ -78,6 +82,21 @@ export default { ...@@ -78,6 +82,21 @@ export default {
noRunnersFound() { noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length; return !this.runnersLoading && !this.runners.items.length;
}, },
activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), {
active_runners_count: formatNumber(this.activeRunnersCount),
});
},
searchTokens() {
return [
statusTokenConfig,
typeTokenConfig,
{
...tagTokenConfig,
recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
},
];
},
}, },
watch: { watch: {
search: { search: {
...@@ -99,6 +118,7 @@ export default { ...@@ -99,6 +118,7 @@ export default {
captureException({ error, component: this.$options.name }); captureException({ error, component: this.$options.name });
}, },
}, },
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE, INSTANCE_TYPE,
}; };
</script> </script>
...@@ -118,9 +138,13 @@ export default { ...@@ -118,9 +138,13 @@ export default {
<runner-filtered-search-bar <runner-filtered-search-bar
v-model="search" v-model="search"
namespace="admin_runners" :tokens="searchTokens"
:active-runners-count="activeRunnersCount" :namespace="$options.filteredSearchNamespace"
/> >
<template #runner-count>
{{ activeRunnersMessage }}
</template>
</runner-filtered-search-bar>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5"> <div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }} {{ __('No runners found') }}
......
<script> <script>
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { formatNumber, sprintf, __, s__ } from '~/locale'; import { __ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
CREATED_DESC,
CREATED_ASC,
CONTACTED_DESC,
CONTACTED_ASC,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
} from '../constants';
import TagToken from './search_tokens/tag_token.vue';
const sortOptions = [ const sortOptions = [
{ {
...@@ -58,10 +39,6 @@ export default { ...@@ -58,10 +39,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
activeRunnersCount: {
type: Number,
required: true,
},
}, },
data() { data() {
// filtered_search_bar_root.vue may mutate the inital // filtered_search_bar_root.vue may mutate the inital
...@@ -73,62 +50,6 @@ export default { ...@@ -73,62 +50,6 @@ export default {
initialSortBy: sort, initialSortBy: sort,
}; };
}, },
computed: {
searchTokens() {
return [
{
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
options: [
{ value: STATUS_ACTIVE, title: s__('Runners|Active') },
{ value: STATUS_PAUSED, title: s__('Runners|Paused') },
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
// Added extra quotes in this title to avoid splitting this value:
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
{ value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
],
// TODO In principle we could support more complex search rules,
// this can be added to a separate issue.
operators: OPERATOR_IS_ONLY,
},
{
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|instance') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|project') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
},
{
icon: 'tag',
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
operators: OPERATOR_IS_ONLY,
},
];
},
activeRunnersMessage() {
return sprintf(__('Runners currently online: %{active_runners_count}'), {
active_runners_count: formatNumber(this.activeRunnersCount),
});
},
},
methods: { methods: {
onFilter(filters) { onFilter(filters) {
const { sort } = this.value; const { sort } = this.value;
...@@ -161,12 +82,13 @@ export default { ...@@ -161,12 +82,13 @@ export default {
:sort-options="$options.sortOptions" :sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue" :initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy" :initial-sort-by="initialSortBy"
:tokens="searchTokens"
:search-input-placeholder="__('Search or filter results...')" :search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search" data-testid="runners-filtered-search"
@onFilter="onFilter" @onFilter="onFilter"
@onSort="onSort" @onSort="onSort"
/> />
<div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div> <div class="gl-text-right" data-testid="runner-count">
<slot name="runner-count"></slot>
</div>
</div> </div>
</template> </template>
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
STATUS_ONLINE,
STATUS_OFFLINE,
STATUS_NOT_CONNECTED,
PARAM_KEY_STATUS,
} from '../../constants';
export const statusTokenConfig = {
icon: 'status',
title: __('Status'),
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
options: [
{ value: STATUS_ACTIVE, title: s__('Runners|Active') },
{ value: STATUS_PAUSED, title: s__('Runners|Paused') },
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
// Added extra quotes in this title to avoid splitting this value:
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
{ value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
],
// TODO In principle we could support more complex search rules,
// this can be added to a separate issue.
operators: OPERATOR_IS_ONLY,
};
...@@ -33,6 +33,7 @@ export default { ...@@ -33,6 +33,7 @@ export default {
// The API should // The API should
// 1) scope to the rights of the user // 1) scope to the rights of the user
// 2) stay up to date to the removal of old tags // 2) stay up to date to the removal of old tags
// 3) consider the scope of search, like searching within the tags of a group
// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796 // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
return axios return axios
.get(TAG_SUGGESTIONS_PATH, { .get(TAG_SUGGESTIONS_PATH, {
......
import { s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import { PARAM_KEY_TAG } from '../../constants';
import TagToken from './tag_token.vue';
export const tagTokenConfig = {
icon: 'tag',
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
operators: OPERATOR_IS_ONLY,
};
import { __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
export const typeTokenConfig = {
icon: 'file-tree',
title: __('Type'),
type: PARAM_KEY_RUNNER_TYPE,
token: BaseToken,
unique: true,
options: [
{ value: INSTANCE_TYPE, title: s__('Runners|instance') },
{ value: GROUP_TYPE, title: s__('Runners|group') },
{ value: PROJECT_TYPE, title: s__('Runners|project') },
],
// TODO We should support more complex search rules,
// search for multiple states (OR) or have NOT operators
operators: OPERATOR_IS_ONLY,
};
...@@ -2,6 +2,7 @@ import { s__ } from '~/locale'; ...@@ -2,6 +2,7 @@ import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20; export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000; export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.'); export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}'); export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
...@@ -50,3 +51,8 @@ export const CONTACTED_DESC = 'CONTACTED_DESC'; // TODO Add this to the API ...@@ -50,3 +51,8 @@ export const CONTACTED_DESC = 'CONTACTED_DESC'; // TODO Add this to the API
export const CONTACTED_ASC = 'CONTACTED_ASC'; export const CONTACTED_ASC = 'CONTACTED_ASC';
export const DEFAULT_SORT = CREATED_DESC; export const DEFAULT_SORT = CREATED_DESC;
// Local storage namespaces
export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners';
export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners';
#import "~/runner/graphql/runner_node.fragment.graphql"
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getGroupRunners(
$groupFullPath: ID!
$before: String
$after: String
$first: Int
$last: Int
$status: CiRunnerStatus
$type: CiRunnerType
$search: String
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
runners(
membership: DESCENDANTS
before: $before
after: $after
first: $first
last: $last
status: $status
type: $type
search: $search
sort: $sort
) {
nodes {
...RunnerNode
}
pageInfo {
...PageInfo
}
}
}
}
<script> <script>
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue'; import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue'; import RunnerTypeHelp from '../components/runner_type_help.vue';
import { GROUP_TYPE } from '../constants'; import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { typeTokenConfig } from '../components/search_tokens/type_token_config';
import {
I18N_FETCH_ERROR,
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
GROUP_RUNNER_COUNT_LIMIT,
} from '../constants';
import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
export default { export default {
name: 'GroupRunnersApp',
components: { components: {
RunnerFilteredSearchBar,
RunnerList,
RunnerManualSetupHelp, RunnerManualSetupHelp,
RunnerTypeHelp, RunnerTypeHelp,
RunnerPagination,
}, },
props: { props: {
registrationToken: { registrationToken: {
type: String, type: String,
required: true, required: true,
}, },
groupFullPath: {
type: String,
required: true,
},
groupRunnersLimitedCount: {
type: Number,
required: true,
},
},
data() {
return {
search: fromUrlQueryToSearch(),
runners: {
items: [],
pageInfo: {},
},
};
},
apollo: {
runners: {
query: getGroupRunnersQuery,
// Runners can be updated by users directly in this list.
// A "cache and network" policy prevents outdated filtered
// results.
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
variables() {
return this.variables;
},
update(data) {
const { runners } = data?.group || {};
return {
items: runners?.nodes || [],
pageInfo: runners?.pageInfo || {},
};
},
error(error) {
createFlash({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
},
computed: {
variables() {
return {
...fromSearchToVariables(this.search),
groupFullPath: this.groupFullPath,
};
},
runnersLoading() {
return this.$apollo.queries.runners.loading;
},
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
groupRunnersCount() {
if (this.groupRunnersLimitedCount > GROUP_RUNNER_COUNT_LIMIT) {
return `${formatNumber(GROUP_RUNNER_COUNT_LIMIT)}+`;
}
return formatNumber(this.groupRunnersLimitedCount);
},
runnerCountMessage() {
return sprintf(s__('Runners|Runners in this group: %{groupRunnersCount}'), {
groupRunnersCount: this.groupRunnersCount,
});
},
searchTokens() {
return [statusTokenConfig, typeTokenConfig];
},
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
},
},
watch: {
search: {
deep: true,
handler() {
// TODO Implement back button reponse using onpopstate
updateHistory({
url: fromSearchToUrl(this.search),
title: document.title,
});
},
},
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
}, },
GROUP_TYPE, GROUP_TYPE,
}; };
...@@ -31,5 +148,23 @@ export default { ...@@ -31,5 +148,23 @@ export default {
/> />
</div> </div>
</div> </div>
<runner-filtered-search-bar
v-model="search"
:tokens="searchTokens"
:namespace="filteredSearchNamespace"
>
<template #runner-count>
{{ runnerCountMessage }}
</template>
</runner-filtered-search-bar>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
<template v-else>
<runner-list :runners="runners.items" :loading="runnersLoading" />
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div> </div>
</template> </template>
...@@ -12,7 +12,13 @@ export const initGroupRunners = (selector = '#js-group-runners') => { ...@@ -12,7 +12,13 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
return null; return null;
} }
const { registrationToken, groupId } = el.dataset; const {
registrationToken,
runnerInstallHelpPage,
groupId,
groupFullPath,
groupRunnersLimitedCount,
} = el.dataset;
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(
...@@ -27,12 +33,15 @@ export const initGroupRunners = (selector = '#js-group-runners') => { ...@@ -27,12 +33,15 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
el, el,
apolloProvider, apolloProvider,
provide: { provide: {
runnerInstallHelpPage,
groupId, groupId,
}, },
render(h) { render(h) {
return h(GroupRunnersApp, { return h(GroupRunnersApp, {
props: { props: {
registrationToken, registrationToken,
groupFullPath,
groupRunnersLimitedCount: parseInt(groupRunnersLimitedCount, 10),
}, },
}); });
}, },
......
...@@ -250,6 +250,10 @@ ...@@ -250,6 +250,10 @@
.commit-row-description { .commit-row-description {
display: none; display: none;
flex: 1; flex: 1;
a {
color: $blue-600;
}
} }
&.inline-commit { &.inline-commit {
......
...@@ -29,9 +29,16 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController ...@@ -29,9 +29,16 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController
redirect_back fallback_location: { action: 'index' } redirect_back fallback_location: { action: 'index' }
end end
def retry
migration = batched_migration_class.find(params[:id])
migration.retry_failed_jobs! if migration.failed?
redirect_back fallback_location: { action: 'index' }
end
private private
def batched_migration_class def batched_migration_class
Gitlab::Database::BackgroundMigration::BatchedMigration @batched_migration_class ||= Gitlab::Database::BackgroundMigration::BatchedMigration
end end
end end
...@@ -10,6 +10,8 @@ class Groups::RunnersController < Groups::ApplicationController ...@@ -10,6 +10,8 @@ class Groups::RunnersController < Groups::ApplicationController
feature_category :runner feature_category :runner
def index def index
finder = Ci::RunnersFinder.new(current_user: current_user, params: { group: @group })
@group_runners_limited_count = finder.execute.except(:limit, :offset).page.total_count_with_limit(:all, limit: 1000)
end end
def runner_list_group_view_vue_ui_enabled def runner_list_group_view_vue_ui_enabled
...@@ -59,7 +61,7 @@ class Groups::RunnersController < Groups::ApplicationController ...@@ -59,7 +61,7 @@ class Groups::RunnersController < Groups::ApplicationController
private private
def runner def runner
@runner ||= Ci::RunnersFinder.new(current_user: current_user, group: @group, params: {}).execute @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
.except(:limit, :offset) .except(:limit, :offset)
.find(params[:id]) .find(params[:id])
end end
......
...@@ -17,7 +17,7 @@ module Groups ...@@ -17,7 +17,7 @@ module Groups
NUMBER_OF_RUNNERS_PER_PAGE = 4 NUMBER_OF_RUNNERS_PER_PAGE = 4
def show def show
runners_finder = Ci::RunnersFinder.new(current_user: current_user, group: @group, params: params) runners_finder = Ci::RunnersFinder.new(current_user: current_user, params: params.merge({ group: @group }))
# We need all runners for count # We need all runners for count
@all_group_runners = runners_finder.execute.except(:limit, :offset) @all_group_runners = runners_finder.execute.except(:limit, :offset)
@group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) @group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
......
# frozen_string_literal: true
class Projects::UsageQuotasController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :verify_usage_quotas_enabled!
layout "project_settings"
feature_category :utilization
private
def verify_usage_quotas_enabled!
render_404 unless Feature.enabled?(:project_storage_ui, project&.group, default_enabled: :yaml)
end
end
...@@ -7,9 +7,9 @@ module Ci ...@@ -7,9 +7,9 @@ module Ci
ALLOWED_SORTS = %w[contacted_asc contacted_desc created_at_asc created_at_desc created_date].freeze ALLOWED_SORTS = %w[contacted_asc contacted_desc created_at_asc created_at_desc created_date].freeze
DEFAULT_SORT = 'created_at_desc' DEFAULT_SORT = 'created_at_desc'
def initialize(current_user:, group: nil, params:) def initialize(current_user:, params:)
@params = params @params = params
@group = group @group = params.delete(:group)
@current_user = current_user @current_user = current_user
end end
...@@ -48,10 +48,16 @@ module Ci ...@@ -48,10 +48,16 @@ module Ci
def group_runners def group_runners
raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group) raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group)
# Getting all runners from the group itself and all its descendants @runners = case @params[:membership]
when :direct
Ci::Runner.belonging_to_group(@group.id)
when :descendants, nil
# Getting all runners from the group itself and all its descendant groups/projects
descendant_projects = Project.for_group_and_its_subgroups(@group) descendant_projects = Project.for_group_and_its_subgroups(@group)
Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
@runners = Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects) else
raise ArgumentError, 'Invalid membership filter'
end
end end
def filter_by_status! def filter_by_status!
......
# frozen_string_literal: true
# Groups::UserGroupsFinder
#
# Used to filter Groups where a user is member
#
# Arguments:
# current_user - user requesting group info on target user
# target_user - user for which groups will be found
# params:
# permissions: string (see Types::Groups::UserPermissionsEnum)
# search: string used for search on path and group name
#
# Initially created to filter user groups and descendants where the user can create projects
module Groups
class UserGroupsFinder
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@params = params
end
def execute
return Group.none unless current_user&.can?(:read_user_groups, target_user)
return Group.none if target_user.blank?
items = by_permission_scope
items = by_search(items)
sort(items)
end
private
attr_reader :current_user, :target_user, :params
def sort(items)
items.order(path: :asc, id: :asc) # rubocop: disable CodeReuse/ActiveRecord
end
def by_search(items)
return items if params[:search].blank?
items.search(params[:search])
end
def by_permission_scope
if permission_scope_create_projects?
target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
else
target_user.groups
end
end
def permission_scope_create_projects?
params[:permission_scope] == :create_projects &&
Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
end
end
end
...@@ -12,11 +12,16 @@ module Packages ...@@ -12,11 +12,16 @@ module Packages
end end
def execute def execute
base.npm results = base.npm
.with_name(@package_name) .with_name(@package_name)
.installable .installable
.last_of_each_version .last_of_each_version
.preload_files
unless Feature.enabled?(:npm_presenter_queries_tuning)
results = results.preload_files
end
results
end end
private private
......
...@@ -124,6 +124,16 @@ module Resolvers ...@@ -124,6 +124,16 @@ module Resolvers
[args[:iid], args[:iids]].any? ? 0 : 0.01 [args[:iid], args[:iids]].any? ? 0 : 0.01
end end
def self.before_connection_authorization(&block)
@before_connection_authorization_block = block
end
# rubocop: disable Style/TrivialAccessors
def self.before_connection_authorization_block
@before_connection_authorization_block
end
# rubocop: enable Style/TrivialAccessors
def offset_pagination(relation) def offset_pagination(relation)
::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(relation) ::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(relation)
end end
......
# frozen_string_literal: true
module Resolvers
module Ci
class GroupRunnersResolver < RunnersResolver
type Types::Ci::RunnerType.connection_type, null: true
argument :membership, ::Types::Ci::RunnerMembershipFilterEnum,
required: false,
default_value: :descendants,
description: 'Control which runners to include in the results.'
protected
def runners_finder_params(params)
super(params).merge(membership: params[:membership])
end
def parent_param
raise 'Expected group missing' unless parent.is_a?(Group)
{ group: parent }
end
end
end
end
...@@ -34,7 +34,7 @@ module Resolvers ...@@ -34,7 +34,7 @@ module Resolvers
.execute) .execute)
end end
private protected
def runners_finder_params(params) def runners_finder_params(params)
{ {
...@@ -47,6 +47,19 @@ module Resolvers ...@@ -47,6 +47,19 @@ module Resolvers
tag_name: node_selection&.selects?(:tag_list) tag_name: node_selection&.selects?(:tag_list)
} }
}.compact }.compact
.merge(parent_param)
end
def parent_param
return {} unless parent
raise "Unexpected parent type: #{parent.class}"
end
private
def parent
object.respond_to?(:sync) ? object.sync : object
end end
end end
end end
......
# frozen_string_literal: true
module Resolvers
module Users
class GroupsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
type Types::GroupType.connection_type, null: true
authorize :read_user_groups
authorizes_object!
argument :search, GraphQL::Types::String,
required: false,
description: 'Search by group name or path.'
argument :permission_scope,
::Types::PermissionTypes::GroupEnum,
required: false,
description: 'Filter by permissions the user has on groups.'
before_connection_authorization do |nodes, current_user|
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
end
def resolve_with_lookahead(**args)
return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
end
private
def preloads
{
path: [:route],
full_path: [:route]
}
end
end
end
end
Resolvers::Users::GroupsResolver.prepend_mod_with('Resolvers::Users::GroupsResolver')
# frozen_string_literal: true
module Types
module Ci
class RunnerMembershipFilterEnum < BaseEnum
graphql_name 'RunnerMembershipFilter'
description 'Values for filtering runners in namespaces.'
value 'DIRECT',
description: "Include runners that have a direct relationship.",
value: :direct
value 'DESCENDANTS',
description: "Include runners that have either a direct relationship or a relationship with descendants. These can be project runners or group runners (in the case where group is queried).",
value: :descendants
end
end
end
...@@ -155,6 +155,12 @@ module Types ...@@ -155,6 +155,12 @@ module Types
complexity: 5, complexity: 5,
resolver: Resolvers::GroupsResolver resolver: Resolvers::GroupsResolver
field :runners, Types::Ci::RunnerType.connection_type,
null: true,
resolver: Resolvers::Ci::GroupRunnersResolver,
description: "Find runners visible to the current user.",
feature_flag: :runner_graphql_query
def avatar_url def avatar_url
object.avatar_url(only_path: false) object.avatar_url(only_path: false)
end end
......
...@@ -5,7 +5,7 @@ module Types ...@@ -5,7 +5,7 @@ module Types
class Group < BasePermissionType class Group < BasePermissionType
graphql_name 'GroupPermissions' graphql_name 'GroupPermissions'
abilities :read_group abilities :read_group, :create_projects
end end
end end
end end
# frozen_string_literal: true
module Types
module PermissionTypes
class GroupEnum < BaseEnum
graphql_name 'GroupPermission'
description 'User permission on groups'
value 'CREATE_PROJECTS', value: :create_projects, description: 'Groups where the user can create projects.'
end
end
end
...@@ -59,6 +59,9 @@ module Types ...@@ -59,6 +59,9 @@ module Types
type: Types::GroupMemberType.connection_type, type: Types::GroupMemberType.connection_type,
null: true, null: true,
description: 'Group memberships of the user.' description: 'Group memberships of the user.'
field :groups,
resolver: Resolvers::Users::GroupsResolver,
description: 'Groups where the user has access.'
field :group_count, field :group_count,
resolver: Resolvers::Users::GroupCountResolver, resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user.' description: 'Group count for the user.'
......
...@@ -16,7 +16,6 @@ module Ci ...@@ -16,7 +16,6 @@ module Ci
"ci-config-path": project.ci_config_path_or_default, "ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'), "ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'), "ci-help-page-path" => help_page_path('ci/index'),
"commit-sha" => commit_sha,
"default-branch" => project.default_branch_or_main, "default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'), "empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch, "initial-branch-name" => initial_branch,
......
...@@ -174,7 +174,11 @@ module IssuesHelper ...@@ -174,7 +174,11 @@ module IssuesHelper
end end
def issue_header_actions_data(project, issuable, current_user) def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?) new_issuable_params = { issue: { description: _('Related to #%{issue_id}.') % { issue_id: issuable.iid } + "\n\n" } }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
new_issuable_params[:issue][:issue_type] = 'incident'
end
{ {
can_create_issue: show_new_issue_link?(project).to_s, can_create_issue: show_new_issue_link?(project).to_s,
......
...@@ -31,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -31,7 +31,7 @@ class ApplicationRecord < ActiveRecord::Base
end end
def self.safe_ensure_unique(retries: 0) def self.safe_ensure_unique(retries: 0)
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
yield yield
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
...@@ -55,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -55,7 +55,7 @@ class ApplicationRecord < ActiveRecord::Base
# currently one third of the default 15-second timeout # currently one third of the default 15-second timeout
def self.with_fast_read_statement_timeout(timeout_ms = 5000) def self.with_fast_read_statement_timeout(timeout_ms = 5000)
::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do ::Gitlab::Database::LoadBalancing::Session.current.fallback_to_replicas_for_ambiguous_queries do
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}") connection.exec_query("SET LOCAL statement_timeout = #{timeout_ms}")
yield yield
...@@ -80,7 +80,7 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -80,7 +80,7 @@ class ApplicationRecord < ActiveRecord::Base
# #
# When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create` # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create`
# and that skips some code that adds the newly created record to the association. # and that skips some code that adds the newly created record to the association.
transaction(requires_new: true) { all.create(*args, &block) } transaction(requires_new: true) { all.create(*args, &block) } # rubocop:disable Performance/ActiveRecordSubtransactions
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
find_by(*args) find_by(*args)
end end
......
...@@ -622,7 +622,7 @@ class ApplicationSetting < ApplicationRecord ...@@ -622,7 +622,7 @@ class ApplicationSetting < ApplicationRecord
def self.create_from_defaults def self.create_from_defaults
check_schema! check_schema!
transaction(requires_new: true) do transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
super super
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
......
...@@ -10,6 +10,9 @@ module Clusters ...@@ -10,6 +10,9 @@ module Clusters
has_many :agent_tokens, class_name: 'Clusters::AgentToken' has_many :agent_tokens, class_name: 'Clusters::AgentToken'
has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken', inverse_of: :agent
has_many :group_authorizations, class_name: 'Clusters::Agents::GroupAuthorization'
has_many :authorized_groups, class_name: '::Group', through: :group_authorizations, source: :group
scope :ordered_by_name, -> { order(:name) } scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) } scope :with_name, -> (name) { where(name: name) }
......
# frozen_string_literal: true
module Clusters
module Agents
class GroupAuthorization < ApplicationRecord
self.table_name = 'agent_group_authorizations'
belongs_to :agent, class_name: 'Clusters::Agent', optional: false
belongs_to :group, class_name: '::Group', optional: false
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
end
end
end
...@@ -13,7 +13,7 @@ class InstanceConfiguration ...@@ -13,7 +13,7 @@ class InstanceConfiguration
{ ssh_algorithms_hashes: ssh_algorithms_hashes, { ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host, host: host,
gitlab_pages: gitlab_pages, gitlab_pages: gitlab_pages,
gitlab_ci: gitlab_ci, size_limits: size_limits,
package_file_size_limits: package_file_size_limits, package_file_size_limits: package_file_size_limits,
rate_limits: rate_limits }.deep_symbolize_keys rate_limits: rate_limits }.deep_symbolize_keys
end end
...@@ -38,11 +38,16 @@ class InstanceConfiguration ...@@ -38,11 +38,16 @@ class InstanceConfiguration
rescue Resolv::ResolvError rescue Resolv::ResolvError
end end
def gitlab_ci def size_limits
Settings.gitlab_ci {
.to_h max_attachment_size: application_settings[:max_attachment_size].megabytes,
.merge(artifacts_max_size: { value: Gitlab::CurrentSettings.max_artifacts_size.megabytes, receive_max_input_size: application_settings[:receive_max_input_size]&.megabytes,
default: 100.megabytes }) max_import_size: application_settings[:max_import_size] > 0 ? application_settings[:max_import_size].megabytes : nil,
diff_max_patch_bytes: application_settings[:diff_max_patch_bytes].bytes,
max_artifacts_size: application_settings[:max_artifacts_size].megabytes,
max_pages_size: application_settings[:max_pages_size] > 0 ? application_settings[:max_pages_size].megabytes : nil,
snippet_size_limit: application_settings[:snippet_size_limit]&.bytes
}
end end
def package_file_size_limits def package_file_size_limits
......
...@@ -201,32 +201,57 @@ class InternalId < ApplicationRecord ...@@ -201,32 +201,57 @@ class InternalId < ApplicationRecord
InternalId.find_by(**scope, usage: usage_value) InternalId.find_by(**scope, usage: usage_value)
end end
def initial_value(subject, scope)
raise ArgumentError, 'Cannot initialize without init!' unless init
# `init` computes the maximum based on actual records. We use the
# primary to make sure we have up to date results
Gitlab::Database::LoadBalancing::Session.current.use_primary do
instance = subject.is_a?(::Class) ? nil : subject
init.call(instance, scope) || 0
end
end
def usage_value def usage_value
@usage_value ||= InternalId.usages[usage.to_s] @usage_value ||= InternalId.usages[usage.to_s]
end end
# Create InternalId record for (scope, usage) combination, if it doesn't exist # Create InternalId record for (scope, usage) combination, if it doesn't exist
# #
# We blindly insert without synchronization. If another process # We blindly insert ignoring conflicts on the unique key constraint.
# was faster in doing this, we'll realize once we hit the unique key constraint # If another process was faster in doing this, we'll end up with that record
# violation. We can safely roll-back the nested transaction and perform # when we do the lookup after the insert.
# a lookup instead to retrieve the record.
def create_record def create_record
raise ArgumentError, 'Cannot initialize without init!' unless init if Feature.enabled?(:use_insert_all_in_internal_id, default_enabled: :yaml)
scope[:project].save! if scope[:project] && !scope[:project].persisted?
scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted?
instance = subject.is_a?(::Class) ? nil : subject attributes = {
project_id: scope[:project]&.id || scope[:project_id],
namespace_id: scope[:namespace]&.id || scope[:namespace_id],
usage: usage_value,
last_value: initial_value(subject, scope)
}
subject.transaction(requires_new: true) do InternalId.insert_all([attributes])
lookup
else
begin
subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
last_value: init.call(instance, scope) || 0 last_value: initial_value(subject, scope)
) )
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
lookup lookup
end end
end end
end
end
class ImplicitlyLockingInternalIdGenerator class ImplicitlyLockingInternalIdGenerator
# Generate next internal id for a given scope and usage. # Generate next internal id for a given scope and usage.
...@@ -247,6 +272,8 @@ class InternalId < ApplicationRecord ...@@ -247,6 +272,8 @@ class InternalId < ApplicationRecord
# init: Proc that accepts the subject and the scope and returns Integer|NilClass # init: Proc that accepts the subject and the scope and returns Integer|NilClass
attr_reader :subject, :scope, :scope_attrs, :usage, :init attr_reader :subject, :scope, :scope_attrs, :usage, :init
RecordAlreadyExists = Class.new(StandardError)
def initialize(subject, scope, usage, init = nil) def initialize(subject, scope, usage, init = nil)
@subject = subject @subject = subject
@scope = scope @scope = scope
...@@ -270,10 +297,8 @@ class InternalId < ApplicationRecord ...@@ -270,10 +297,8 @@ class InternalId < ApplicationRecord
return next_iid if next_iid return next_iid if next_iid
create_record!(subject, scope, usage, init) do |iid| create_record!(subject, scope, usage, initial_value(subject, scope) + 1)
iid.last_value += 1 rescue RecordAlreadyExists
end
rescue ActiveRecord::RecordNotUnique
retry retry
end end
...@@ -302,10 +327,8 @@ class InternalId < ApplicationRecord ...@@ -302,10 +327,8 @@ class InternalId < ApplicationRecord
next_iid = update_record!(subject, scope, usage, function) next_iid = update_record!(subject, scope, usage, function)
return next_iid if next_iid return next_iid if next_iid
create_record!(subject, scope, usage, init) do |object| create_record!(subject, scope, usage, [initial_value(subject, scope), new_value].max)
object.last_value = [object.last_value, new_value].max rescue RecordAlreadyExists
end
rescue ActiveRecord::RecordNotUnique
retry retry
end end
...@@ -317,27 +340,56 @@ class InternalId < ApplicationRecord ...@@ -317,27 +340,56 @@ class InternalId < ApplicationRecord
stmt.set(arel_table[:last_value] => new_value) stmt.set(arel_table[:last_value] => new_value)
stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints stmt.wheres = InternalId.filter_by(scope, usage).arel.constraints
ActiveRecord::Base.connection.insert(stmt, 'Update InternalId', 'last_value') # rubocop: disable Database/MultipleDatabases InternalId.connection.insert(stmt, 'Update InternalId', 'last_value')
end end
def create_record!(subject, scope, usage, init) def create_record!(subject, scope, usage, value)
raise ArgumentError, 'Cannot initialize without init!' unless init if Feature.enabled?(:use_insert_all_in_internal_id, default_enabled: :yaml)
scope[:project].save! if scope[:project] && !scope[:project].persisted?
scope[:namespace].save! if scope[:namespace] && !scope[:namespace].persisted?
instance = subject.is_a?(::Class) ? nil : subject attributes = {
project_id: scope[:project]&.id || scope[:project_id],
namespace_id: scope[:namespace]&.id || scope[:namespace_id],
usage: usage_value,
last_value: value
}
subject.transaction(requires_new: true) do result = InternalId.insert_all([attributes])
last_value = init.call(instance, scope) || 0
internal_id = InternalId.create!(**scope, usage: usage, last_value: last_value) do |subject| raise RecordAlreadyExists if result.empty?
yield subject if block_given?
end
value
else
begin
subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
internal_id = InternalId.create!(**scope, usage: usage, last_value: value)
internal_id.last_value internal_id.last_value
end end
rescue ActiveRecord::RecordNotUnique
raise RecordAlreadyExists
end
end
end end
def arel_table def arel_table
InternalId.arel_table InternalId.arel_table
end end
def initial_value(subject, scope)
raise ArgumentError, 'Cannot initialize without init!' unless init
# `init` computes the maximum based on actual records. We use the
# primary to make sure we have up to date results
Gitlab::Database::LoadBalancing::Session.current.use_primary do
instance = subject.is_a?(::Class) ? nil : subject
init.call(instance, scope) || 0
end
end
def usage_value
@usage_value ||= InternalId.usages[usage.to_s]
end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class Packages::Package < ApplicationRecord class Packages::Package < ApplicationRecord
include EachBatch
include Sortable include Sortable
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include UsageStatistics include UsageStatistics
...@@ -104,6 +105,7 @@ class Packages::Package < ApplicationRecord ...@@ -104,6 +105,7 @@ class Packages::Package < ApplicationRecord
scope :including_build_info, -> { includes(pipelines: :user) } scope :including_build_info, -> { includes(pipelines: :user) }
scope :including_project_route, -> { includes(project: { namespace: :route }) } scope :including_project_route, -> { includes(project: { namespace: :route }) }
scope :including_tags, -> { includes(:tags) } scope :including_tags, -> { includes(:tags) }
scope :including_dependency_links, -> { includes(dependency_links: :dependency) }
scope :with_conan_channel, ->(package_channel) do scope :with_conan_channel, ->(package_channel) do
joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel }) joins(:conan_metadatum).where(packages_conan_metadata: { package_channel: package_channel })
......
# frozen_string_literal: true
module Preloaders
# This class preloads the max access level (role) for the user within the given groups and
# stores the values in requests store.
# Will only be able to preload max access level for groups where the user is a direct member
class UserMaxAccessLevelInGroupsPreloader
include BulkMemberAccessLoad
def initialize(groups, user)
@groups = groups
@user = user
end
def execute
group_memberships = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(user: @user, source_id: @groups)
.group(:source_id)
.maximum(:access_level)
group_memberships.each do |group_id, max_access_level|
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
end
end
end
end
...@@ -2093,6 +2093,10 @@ class Project < ApplicationRecord ...@@ -2093,6 +2093,10 @@ class Project < ApplicationRecord
# Docker doesn't allow. The proxy expects it to be downcased. # Docker doesn't allow. The proxy expects it to be downcased.
value: "#{Gitlab.host_with_port}/#{namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}" value: "#{Gitlab.host_with_port}/#{namespace.root_ancestor.path.downcase}#{DependencyProxy::URL_SUFFIX}"
) )
variables.append(
key: 'CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX',
value: "#{Gitlab.host_with_port}/#{namespace.full_path.downcase}#{DependencyProxy::URL_SUFFIX}"
)
end end
end end
......
...@@ -25,6 +25,7 @@ class UserPolicy < BasePolicy ...@@ -25,6 +25,7 @@ class UserPolicy < BasePolicy
enable :update_user_status enable :update_user_status
enable :read_user_personal_access_tokens enable :read_user_personal_access_tokens
enable :read_group_count enable :read_group_count
enable :read_user_groups
end end
rule { default }.enable :read_user_profile rule { default }.enable :read_user_profile
......
...@@ -7,14 +7,26 @@ module Packages ...@@ -7,14 +7,26 @@ module Packages
attr_reader :name, :packages attr_reader :name, :packages
NPM_VALID_DEPENDENCY_TYPES = %i[dependencies devDependencies bundleDependencies peerDependencies].freeze
def initialize(name, packages) def initialize(name, packages)
@name = name @name = name
@packages = packages @packages = packages
end end
def versions def versions
if queries_tuning?
new_versions
else
legacy_versions
end
end
def dist_tags
build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last }
end
private
def legacy_versions
package_versions = {} package_versions = {}
packages.each do |package| packages.each do |package|
...@@ -28,11 +40,23 @@ module Packages ...@@ -28,11 +40,23 @@ module Packages
package_versions package_versions
end end
def dist_tags def new_versions
build_package_tags.tap { |t| t["latest"] ||= sorted_versions.last } package_versions = {}
packages.each_batch do |relation|
relation.including_dependency_links
.preload_files
.each do |package|
package_file = package.package_files.last
next unless package_file
package_versions[package.version] = build_package_version(package, package_file)
end
end end
private package_versions
end
def build_package_tags def build_package_tags
package_tags.to_h { |tag| [tag.name, tag.package.version] } package_tags.to_h { |tag| [tag.name, tag.package.version] }
...@@ -59,20 +83,28 @@ module Packages ...@@ -59,20 +83,28 @@ module Packages
def build_package_dependencies(package) def build_package_dependencies(package)
dependencies = Hash.new { |h, key| h[key] = {} } dependencies = Hash.new { |h, key| h[key] = {} }
if queries_tuning?
package.dependency_links.each do |dependency_link|
dependency = dependency_link.dependency
dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
end
else
dependency_links = package.dependency_links dependency_links = package.dependency_links
.with_dependency_type(NPM_VALID_DEPENDENCY_TYPES) .with_dependency_type(%i[dependencies devDependencies bundleDependencies peerDependencies])
.includes_dependency .includes_dependency
dependency_links.find_each do |dependency_link| dependency_links.find_each do |dependency_link|
dependency = dependency_link.dependency dependency = dependency_link.dependency
dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern dependencies[dependency_link.dependency_type][dependency.name] = dependency.version_pattern
end end
end
dependencies dependencies
end end
def sorted_versions def sorted_versions
versions = packages.map(&:version).compact versions = packages.pluck_versions.compact
VersionSorter.sort(versions) VersionSorter.sort(versions)
end end
...@@ -80,6 +112,10 @@ module Packages ...@@ -80,6 +112,10 @@ module Packages
Packages::Tag.for_packages(packages) Packages::Tag.for_packages(packages)
.preload_package .preload_package
end end
def queries_tuning?
Feature.enabled?(:npm_presenter_queries_tuning)
end
end end
end end
end end
# frozen_string_literal: true
module Clusters
module Agents
class RefreshAuthorizationService
include Gitlab::Utils::StrongMemoize
AUTHORIZED_GROUP_LIMIT = 100
delegate :project, to: :agent, private: true
def initialize(agent, config:)
@agent = agent
@config = config
end
def execute
if allowed_group_configurations.present?
group_ids = allowed_group_configurations.map { |config| config.fetch(:group_id) }
agent.with_lock do
agent.group_authorizations.upsert_all(allowed_group_configurations, unique_by: [:agent_id, :group_id])
agent.group_authorizations.where.not(group_id: group_ids).delete_all # rubocop: disable CodeReuse/ActiveRecord
end
else
agent.group_authorizations.delete_all(:delete_all)
end
true
end
private
attr_reader :agent, :config
def allowed_group_configurations
strong_memoize(:allowed_group_configurations) do
group_entries = config.dig('ci_access', 'groups')&.first(AUTHORIZED_GROUP_LIMIT)
if group_entries
groups_by_path = group_entries.index_by { |config| config.delete('id') }
allowed_groups.where_full_path_in(groups_by_path.keys).map do |group|
{ group_id: group.id, config: groups_by_path[group.full_path] }
end
end
end
end
def allowed_groups
if project.root_ancestor.group?
project.root_ancestor.self_and_descendants
else
::Group.none
end
end
end
end
end
...@@ -54,28 +54,10 @@ module Issues ...@@ -54,28 +54,10 @@ module Issues
end end
handle_assignee_changes(issue, old_assignees) handle_assignee_changes(issue, old_assignees)
handle_confidential_change(issue)
if issue.previous_changes.include?('confidential') handle_added_labels(issue, old_labels)
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
track_usage_event(:incident_management_incident_change_confidential, current_user.id)
end
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
handle_milestone_change(issue) handle_milestone_change(issue)
handle_added_mentions(issue, old_mentioned_users)
added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
handle_severity_change(issue, old_severity) handle_severity_change(issue, old_severity)
handle_issue_type_change(issue) handle_issue_type_change(issue)
end end
...@@ -157,6 +139,23 @@ module Issues ...@@ -157,6 +139,23 @@ module Issues
MergeRequests::CreateFromIssueService.new(project: project, current_user: current_user, mr_params: create_merge_request_params).execute MergeRequests::CreateFromIssueService.new(project: project, current_user: current_user, mr_params: create_merge_request_params).execute
end end
def handle_confidential_change(issue)
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
track_usage_event(:incident_management_incident_change_confidential, current_user.id)
end
end
def handle_added_labels(issue, old_labels)
added_labels = issue.labels - old_labels
if added_labels.present?
notification_service.async.relabeled_issue(issue, added_labels, current_user)
end
end
def handle_milestone_change(issue) def handle_milestone_change(issue)
return unless issue.previous_changes.include?('milestone_id') return unless issue.previous_changes.include?('milestone_id')
...@@ -185,6 +184,14 @@ module Issues ...@@ -185,6 +184,14 @@ module Issues
end end
end end
def handle_added_mentions(issue, old_mentioned_users)
added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end
end
def handle_severity_change(issue, old_severity) def handle_severity_change(issue, old_severity)
return unless old_severity && issue.severity != old_severity return unless old_severity && issue.severity != old_severity
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_deploy_keys_projects move_deploy_keys_projects
remove_remaining_deploy_keys_projects if remove_remaining_elements remove_remaining_deploy_keys_projects if remove_remaining_elements
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super && source_project.fork_network return unless super && source_project.fork_network
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_fork_network_members move_fork_network_members
update_root_project update_root_project
refresh_forks_count refresh_forks_count
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_lfs_objects_projects move_lfs_objects_projects
remove_remaining_lfs_objects_project if remove_remaining_elements remove_remaining_lfs_objects_project if remove_remaining_elements
......
...@@ -5,7 +5,7 @@ module Projects ...@@ -5,7 +5,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_notification_settings move_notification_settings
remove_remaining_notification_settings if remove_remaining_elements remove_remaining_notification_settings if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_authorizations move_project_authorizations
remove_remaining_authorizations if remove_remaining_elements remove_remaining_authorizations if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_group_links move_group_links
remove_remaining_project_group_links if remove_remaining_elements remove_remaining_project_group_links if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
def execute(source_project, remove_remaining_elements: true) def execute(source_project, remove_remaining_elements: true)
return unless super return unless super
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
move_project_members move_project_members
remove_remaining_members if remove_remaining_elements remove_remaining_members if remove_remaining_elements
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
return unless user_stars.any? return unless user_stars.any?
Project.transaction(requires_new: true) do Project.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
user_stars.update_all(project_id: @project.id) user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects Project.reset_counters @project.id, :users_star_projects
......
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Cluster Agent configuration for an authorized project or group",
"type": "object",
"additionalProperties": true
}
...@@ -17,3 +17,7 @@ ...@@ -17,3 +17,7 @@
= button_to resume_admin_background_migration_path(migration), = button_to resume_admin_background_migration_path(migration),
class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do
= sprite_icon('play', css_class: 'gl-button-icon gl-icon') = sprite_icon('play', css_class: 'gl-button-icon gl-icon')
- elsif migration.failed?
= button_to retry_admin_background_migration_path(migration),
class: 'gl-button btn btn-icon has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') do
= sprite_icon('retry', css_class: 'gl-button-icon gl-icon')
...@@ -3,4 +3,4 @@ ...@@ -3,4 +3,4 @@
%h2.page-title %h2.page-title
= s_('Runners|Group Runners') = s_('Runners|Group Runners')
#js-group-runners{ data: { registration_token: @group.runners_token, group_id: @group.id } } #js-group-runners{ data: { registration_token: @group.runners_token, runner_install_help_page: 'https://docs.gitlab.com/runner/install/', group_id: @group.id, group_full_path: @group.full_path, group_runners_limited_count: @group_runners_limited_count } }
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= render 'help/instance_configuration/ssh_info' = render 'help/instance_configuration/ssh_info'
= render 'help/instance_configuration/gitlab_pages' = render 'help/instance_configuration/gitlab_pages'
= render 'help/instance_configuration/gitlab_ci' = render 'help/instance_configuration/size_limits'
= render 'help/instance_configuration/package_registry' = render 'help/instance_configuration/package_registry'
= render 'help/instance_configuration/rate_limits' = render 'help/instance_configuration/rate_limits'
%p %p
......
- content_for :table_content do
%li= link_to _('GitLab CI'), '#gitlab-ci'
- content_for :settings_content do
%h2#gitlab-ci
= _('GitLab CI')
%p
= _('Below are the current settings regarding')
= succeed('.') { link_to(_('GitLab CI'), 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
.table-responsive
%table
%thead
%tr
%th= _('Setting')
%th= instance_configuration_host(@instance_configuration.settings[:host])
%th= _('Default')
%tbody
%tr
- artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
%td= _('Artifacts maximum size')
%td= instance_configuration_human_size_cell(artifacts_size[:value])
%td= instance_configuration_human_size_cell(artifacts_size[:default])
...@@ -28,8 +28,3 @@ ...@@ -28,8 +28,3 @@
%td= _('Port') %td= _('Port')
%td %td
%code= instance_configuration_cell_html(gitlab_pages[:port]) %code= instance_configuration_cell_html(gitlab_pages[:port])
%br
%p
- link_to_gitlab_ci = link_to(_('GitLab CI'), '#gitlab-ci')
= _("The maximum size of your Pages site is regulated by the artifacts maximum size which is part of %{link_to_gitlab_ci}.").html_safe % { link_to_gitlab_ci: link_to_gitlab_ci }
- size_limits = @instance_configuration.settings[:size_limits]
- content_for :table_content do
- if size_limits.present?
%li= link_to _('Size Limits'), '#size-limits'
- content_for :settings_content do
- if size_limits.present?
%h2#size-limits
= _('Size Limits')
%p
= _('There are several size limits in place.')
.table-responsive
%table
%thead
%tr
%th= _('Setting')
%th= instance_configuration_host(@instance_configuration.settings[:host])
%tbody
%tr
%td= _('Maximum attachment size')
%td= instance_configuration_human_size_cell(size_limits[:max_attachment_size])
%tr
%td= _('Maximum push size')
%td= instance_configuration_human_size_cell(size_limits[:receive_max_input_size])
%tr
%td= _('Maximum import size')
%td= instance_configuration_human_size_cell(size_limits[:max_import_size])
%tr
%td= _('Maximum diff patch size')
%td= instance_configuration_human_size_cell(size_limits[:diff_max_patch_bytes])
%tr
%td= _('Maximum job artifact size')
%td= instance_configuration_human_size_cell(size_limits[:max_artifacts_size])
%tr
%td= _('Maximum page size')
%td= instance_configuration_human_size_cell(size_limits[:max_pages_size])
%tr
%td= _('Maximum snippet size')
%td= instance_configuration_human_size_cell(size_limits[:snippet_size_limit])
- page_title s_("UsageQuota|Usage")
%h3.page-title
= s_('UsageQuota|Usage Quotas')
.row
.col-sm-6
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name }
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
%ul.nav.nav-tabs.nav-links.scrolling-tabs.separator.js-usage-quota-tabs{ role: 'tablist' }
%li.nav-item
%a.nav-link#storage-quota{ data: { toggle: "tab", action: '#storage-quota-tab' }, href: '#storage-quota-tab', 'aria-controls': '#storage-quota-tab', 'aria-selected': 'true' }
= s_('UsageQuota|Storage')
.tab-content
.tab-pane#storage-quota-tab
#js-project-storage-count-app
---
name: create_vulnerabilities_via_api
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68158
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338694
milestone: '14.3'
type: development
group: group::threat insights
default_enabled: false
--- ---
name: dast_meta_tag_validation name: npm_presenter_queries_tuning
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67945 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68275
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337711 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338603
milestone: '14.2' milestone: '14.2'
type: development type: development
group: group::dynamic analysis group: group::package
default_enabled: true default_enabled: false
---
name: paginatable_namespace_drop_down_for_project_creation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66112
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338930
milestone: '14.3'
type: development
group: group::project management
default_enabled: false
--- ---
name: dast_runner_site_validation name: project_storage_ui
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61649 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68289
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331082 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334889
milestone: '14.0' milestone: '14.2'
type: development type: development
group: group::dynamic analysis group: group::utilization
default_enabled: true default_enabled: false
---
name: use_insert_all_in_internal_id
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68617
rollout_issue_url:
milestone: '14.3'
type: development
group: group::project management
default_enabled: false
...@@ -251,6 +251,7 @@ Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci ...@@ -251,6 +251,7 @@ Settings.gitlab_ci['url'] ||= Settings.__send__(:build_gitlab_ci
# #
Settings['incoming_email'] ||= Settingslogic.new({}) Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil? Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
Settings.incoming_email['inbox_method'] ||= 'imap'
# #
# Service desk email # Service desk email
......
...@@ -93,6 +93,7 @@ namespace :admin do ...@@ -93,6 +93,7 @@ namespace :admin do
member do member do
post :pause post :pause
post :resume post :resume
post :retry
end end
end end
......
...@@ -145,6 +145,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -145,6 +145,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :packages_and_registries, only: [:show] resource :packages_and_registries, only: [:show]
end end
resources :usage_quotas, only: [:index]
resources :autocomplete_sources, only: [] do resources :autocomplete_sources, only: [] do
collection do collection do
get 'members' get 'members'
......
- title: Add pronunciation to GitLab profile page
body: |
You can now add pronunciation to your user profile. In distributed teams where team members are from different countries, it can be difficult to determine how to say someone's name correctly. This will help others know how to pronounce your name.
stage: Manage
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/profile/#add-your-pronunciation'
image_url: https://about.gitlab.com/images/14_2/pronounce.png
published_at: 2021-08-22
release: 14.2
- title: View historical CI pipeline minute usage
body: |
Before GitLab 14.2, the CI pipeline minutes usage on the [Usage Quotas](https://docs.gitlab.com/ee/user/admin_area/settings/continuous_integration.html#shared-runners-pipeline-minutes-quota) page only showed the current month's usage. This data would reset every month and there was no way to view activity from the past months for analyzing historical usage.
Now there are two charts that show historical CI pipeline minutes usage by month or by project, so you can make informed decisions about your pipeline usage.
stage: Verify
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/subscriptions/gitlab_com/index.html#ci-pipeline-minutes'
image_url: https://about.gitlab.com/images/14_2/CI_minutes_usage_graph.png
published_at: 2021-08-22
release: 14.2
- title: Edit issue title from an issue board
body: |
Editing an issue in an issue board currently requires many steps and takes you out of your workflow. We've added an easy way to edit an issue's title right in the issue board, without navigating to another page. To edit the title, in the right sidebar, select the issue, then select **Edit**.
stage: Plan
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/project/issue_board.html#edit-an-issue'
image_url: https://about.gitlab.com/images/14_2/issue-board-edit-title.gif
published_at: 2021-08-22
release: 14.2
- title: Preview Markdown live while editing
body: |
Markdown is a fast and intuitive syntax for writing rich web content. Until it isn't. Luckily, it's easy to preview the rendered output of Markdown to ensure the accuracy of your markup from the **Preview** tab. Unfortunately, the context switch required to move between the raw source code and the preview can be tedious and disruptive to your flow.
Now, in both the Web IDE and single file editor, Markdown files have a new live preview option available. Right-click the editor and select **Preview Markdown** or use `Command/Control + Shift + P` to toggle a split-screen live preview of your Markdown content. The preview refreshes as you type, so you can be confident that your markup is valid and will render as you intended.
stage: Create
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/project/web_ide/#markdown-editing'
image_url: https://about.gitlab.com/images/14_2/create-markdown-live-preview.png
published_at: 2021-08-22
release: 14.2
- title: Stageless pipelines
body: |
Using the [`needs`](https://docs.gitlab.com/ee/ci/yaml/#needs) keyword in your pipeline configuration helps to reduce cycle times by ignoring stage ordering and running jobs without waiting for others to complete. Previously, `needs` could only be used between jobs on different stages.
In this release, we've removed this limitation so you can define a `needs` relationship between any job you want. As a result, you can now create a complete CI/CD pipeline without using stages by including `needs` in every job to implicitly configure the execution order. This lets you define a less verbose pipeline that takes less time to create and can run even faster.
stage: Verify
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/ci/yaml/#needs'
image_url: https://about.gitlab.com/images/14_2/need.png
published_at: 2021-08-22
release: 14.2
- title: New GitLab Kubernetes Agent UI
body: |
The GitLab Kubernetes Agent allows a secure bi-directional connection between GitLab and any Kubernetes cluster. Until now, registering a new Kubernetes Agent required writing GraphQL queries.
As of GitLab 14.2, GitLab ships with a user-friendly user interface and a registration form to help you get started with the Kubernetes Agent with ease.
stage: Configure
self-managed: true
gitlab-com: true
packages: [Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/user/clusters/agent/'
image_url: https://about.gitlab.com/images/14_2/k8s-agent-registration.png
published_at: 2021-08-22
release: 14.2
- title: Create a GitLab branch from a Jira issue
body: |
Users of the [GitLab.com for Jira Cloud](https://marketplace.atlassian.com/apps/1221011/gitlab-com-for-jira-cloud) application can now create GitLab branches directly from a Jira issue's [development panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/). This enables developers to begin work on issues without having to switch tools and lose context.
stage: Create
self-managed: true
gitlab-com: true
packages: [Free, Premium, Ultimate]
url: 'https://docs.gitlab.com/ee/integration/jira/connect-app.html'
image_url: https://about.gitlab.com/images/14_2/jira_dev_panel_jira_setup_3.png
published_at: 2021-08-22
release: 14.2
migrate
\ No newline at end of file
# frozen_string_literal: true
class CreateAgentGroupAuthorizations < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
def change
create_table :agent_group_authorizations do |t|
t.bigint :group_id, null: false
t.bigint :agent_id, null: false
t.jsonb :config, null: false
t.index :group_id
t.index [:agent_id, :group_id], unique: true
end
end
end
# frozen_string_literal: true
class AddAgentGroupAuthorizationsForeignKeys < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
add_concurrent_foreign_key :agent_group_authorizations, :namespaces, column: :group_id
add_concurrent_foreign_key :agent_group_authorizations, :cluster_agents, column: :agent_id
end
def down
with_lock_retries do
remove_foreign_key_if_exists :agent_group_authorizations, column: :group_id
end
with_lock_retries do
remove_foreign_key_if_exists :agent_group_authorizations, column: :agent_id
end
end
end
# frozen_string_literal: true
class AddProjectIdNameVersionIdToNpmPackages < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'idx_installable_npm_pkgs_on_project_id_name_version_id'
def up
add_concurrent_index :packages_packages, [:project_id, :name, :version, :id], where: 'package_type = 2 AND status = 0', name: INDEX_NAME
end
def down
remove_concurrent_index :packages_packages, [:project_id, :name, :version, :id], where: 'package_type = 2 AND status = 0', name: INDEX_NAME
end
end
# frozen_string_literal: true
class AddDefaultProjectApprovalRulesVulnAllowed < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DEFAULT_VALUE = 0
def up
change_column_default :approval_project_rules, :vulnerabilities_allowed, DEFAULT_VALUE
update_column_in_batches(:approval_project_rules, :vulnerabilities_allowed, DEFAULT_VALUE) do |table, query|
query.where(table[:vulnerabilities_allowed].eq(nil))
end
change_column_null :approval_project_rules, :vulnerabilities_allowed, false
end
def down
change_column_default :approval_project_rules, :vulnerabilities_allowed, nil
change_column_null :approval_project_rules, :vulnerabilities_allowed, true
end
end
...@@ -85,7 +85,7 @@ class DeduplicateEpicIids < ActiveRecord::Migration[6.0] ...@@ -85,7 +85,7 @@ class DeduplicateEpicIids < ActiveRecord::Migration[6.0]
instance = subject.is_a?(::Class) ? nil : subject instance = subject.is_a?(::Class) ? nil : subject
subject.transaction(requires_new: true) do subject.transaction(requires_new: true) do # rubocop:disable Performance/ActiveRecordSubtransactions
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
......
# frozen_string_literal: true
class BackfillStageEventHash < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
BATCH_SIZE = 100
EVENT_ID_IDENTIFIER_MAPPING = {
1 => :issue_created,
2 => :issue_first_mentioned_in_commit,
3 => :issue_closed,
4 => :issue_first_added_to_board,
5 => :issue_first_associated_with_milestone,
7 => :issue_last_edited,
8 => :issue_label_added,
9 => :issue_label_removed,
10 => :issue_deployed_to_production,
100 => :merge_request_created,
101 => :merge_request_first_deployed_to_production,
102 => :merge_request_last_build_finished,
103 => :merge_request_last_build_started,
104 => :merge_request_merged,
105 => :merge_request_closed,
106 => :merge_request_last_edited,
107 => :merge_request_label_added,
108 => :merge_request_label_removed,
109 => :merge_request_first_commit_at,
1000 => :code_stage_start,
1001 => :issue_stage_end,
1002 => :plan_stage_start
}.freeze
LABEL_BASED_EVENTS = Set.new([8, 9, 107, 108]).freeze
class GroupStage < ActiveRecord::Base
include EachBatch
self.table_name = 'analytics_cycle_analytics_group_stages'
end
class ProjectStage < ActiveRecord::Base
include EachBatch
self.table_name = 'analytics_cycle_analytics_project_stages'
end
class StageEventHash < ActiveRecord::Base
self.table_name = 'analytics_cycle_analytics_stage_event_hashes'
end
def up
GroupStage.reset_column_information
ProjectStage.reset_column_information
StageEventHash.reset_column_information
update_stage_table(GroupStage)
update_stage_table(ProjectStage)
add_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
add_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
end
def down
remove_not_null_constraint :analytics_cycle_analytics_group_stages, :stage_event_hash_id
remove_not_null_constraint :analytics_cycle_analytics_project_stages, :stage_event_hash_id
end
private
def update_stage_table(klass)
klass.each_batch(of: BATCH_SIZE) do |relation|
klass.transaction do
records = relation.where(stage_event_hash_id: nil).lock!.to_a # prevent concurrent modification (unlikely to happen)
records = delete_invalid_records(records)
next if records.empty?
hashes_by_stage = records.to_h { |stage| [stage, calculate_stage_events_hash(stage)] }
hashes = hashes_by_stage.values.uniq
StageEventHash.insert_all(hashes.map { |hash| { hash_sha256: hash } })
stage_event_hashes_by_hash = StageEventHash.where(hash_sha256: hashes).index_by(&:hash_sha256)
records.each do |stage|
stage.update!(stage_event_hash_id: stage_event_hashes_by_hash[hashes_by_stage[stage]].id)
end
end
end
end
def calculate_stage_events_hash(stage)
start_event_hash = calculate_event_hash(stage.start_event_identifier, stage.start_event_label_id)
end_event_hash = calculate_event_hash(stage.end_event_identifier, stage.end_event_label_id)
Digest::SHA256.hexdigest("#{start_event_hash}-#{end_event_hash}")
end
def calculate_event_hash(event_identifier, label_id = nil)
str = EVENT_ID_IDENTIFIER_MAPPING.fetch(event_identifier).to_s
str << "-#{label_id}" if LABEL_BASED_EVENTS.include?(event_identifier)
Digest::SHA256.hexdigest(str)
end
# Invalid records are safe to delete, since they are not working properly anyway
def delete_invalid_records(records)
to_be_deleted = records.select do |record|
EVENT_ID_IDENTIFIER_MAPPING[record.start_event_identifier].nil? ||
EVENT_ID_IDENTIFIER_MAPPING[record.end_event_identifier].nil?
end
to_be_deleted.each(&:delete)
records - to_be_deleted
end
end
97d968bba0eb2bf6faa19de8a3e4fe93dc03a623b623dc802ab0fe0a4afb0370
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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