Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
78cfc7cf4a
commit
0f50c47cd7
46 changed files with 1341 additions and 185 deletions
|
@ -429,6 +429,13 @@ db:check-migrations-decomposed:
|
||||||
- .decomposed-database
|
- .decomposed-database
|
||||||
- .rails:rules:decomposed-databases
|
- .rails:rules:decomposed-databases
|
||||||
|
|
||||||
|
db:migrate-non-superuser:
|
||||||
|
extends:
|
||||||
|
- .db-job-base
|
||||||
|
- .rails:rules:ee-and-foss-mr-with-migration
|
||||||
|
script:
|
||||||
|
- bundle exec rake gitlab:db:reset_as_non_superuser
|
||||||
|
|
||||||
db:gitlabcom-database-testing:
|
db:gitlabcom-database-testing:
|
||||||
extends: .rails:rules:db:gitlabcom-database-testing
|
extends: .rails:rules:db:gitlabcom-database-testing
|
||||||
stage: test
|
stage: test
|
||||||
|
|
|
@ -230,6 +230,9 @@
|
||||||
.controllers-patterns: &controllers-patterns
|
.controllers-patterns: &controllers-patterns
|
||||||
- "{,ee/,jh/}{app/controllers}/**/*"
|
- "{,ee/,jh/}{app/controllers}/**/*"
|
||||||
|
|
||||||
|
.models-patterns: &models-patterns
|
||||||
|
- "{,ee/,jh/}{app/models}/**/*"
|
||||||
|
|
||||||
.startup-css-patterns: &startup-css-patterns
|
.startup-css-patterns: &startup-css-patterns
|
||||||
- "{,ee/,jh/}app/assets/stylesheets/startup/**/*"
|
- "{,ee/,jh/}app/assets/stylesheets/startup/**/*"
|
||||||
|
|
||||||
|
@ -1429,6 +1432,8 @@
|
||||||
changes: *frontend-patterns
|
changes: *frontend-patterns
|
||||||
- <<: *if-dot-com-gitlab-org-merge-request
|
- <<: *if-dot-com-gitlab-org-merge-request
|
||||||
changes: *controllers-patterns
|
changes: *controllers-patterns
|
||||||
|
- <<: *if-dot-com-gitlab-org-merge-request
|
||||||
|
changes: *models-patterns
|
||||||
- <<: *if-dot-com-gitlab-org-merge-request
|
- <<: *if-dot-com-gitlab-org-merge-request
|
||||||
changes: *qa-patterns
|
changes: *qa-patterns
|
||||||
- <<: *if-dot-com-gitlab-org-merge-request
|
- <<: *if-dot-com-gitlab-org-merge-request
|
||||||
|
|
|
@ -6,10 +6,10 @@ import {
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlLink,
|
GlLink,
|
||||||
GlTooltipDirective as GlTooltip,
|
GlTooltipDirective as GlTooltip,
|
||||||
|
GlTruncate,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import { truncate } from '~/lib/utils/text_utility';
|
|
||||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import DeploymentStatusBadge from './deployment_status_badge.vue';
|
import DeploymentStatusBadge from './deployment_status_badge.vue';
|
||||||
|
@ -25,6 +25,7 @@ export default {
|
||||||
GlCollapse,
|
GlCollapse,
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlLink,
|
GlLink,
|
||||||
|
GlTruncate,
|
||||||
TimeAgoTooltip,
|
TimeAgoTooltip,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
|
@ -75,7 +76,7 @@ export default {
|
||||||
return this.deployment?.user;
|
return this.deployment?.user;
|
||||||
},
|
},
|
||||||
username() {
|
username() {
|
||||||
return truncate(this.user?.username, 25);
|
return `@${this.user.username}`;
|
||||||
},
|
},
|
||||||
userPath() {
|
userPath() {
|
||||||
return this.user?.path;
|
return this.user?.path;
|
||||||
|
@ -84,11 +85,23 @@ export default {
|
||||||
return this.deployment?.deployable;
|
return this.deployment?.deployable;
|
||||||
},
|
},
|
||||||
jobName() {
|
jobName() {
|
||||||
return truncate(this.deployable?.name ?? '', 25);
|
return this.deployable?.name;
|
||||||
},
|
},
|
||||||
jobPath() {
|
jobPath() {
|
||||||
return this.deployable?.buildPath;
|
return this.deployable?.buildPath;
|
||||||
},
|
},
|
||||||
|
refLabel() {
|
||||||
|
return this.deployment?.tag ? this.$options.i18n.tag : this.$options.i18n.branch;
|
||||||
|
},
|
||||||
|
ref() {
|
||||||
|
return this.deployment?.ref;
|
||||||
|
},
|
||||||
|
refName() {
|
||||||
|
return this.ref?.name;
|
||||||
|
},
|
||||||
|
refPath() {
|
||||||
|
return this.ref?.refPath;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleCollapse() {
|
toggleCollapse() {
|
||||||
|
@ -105,6 +118,8 @@ export default {
|
||||||
triggerer: s__('Deployment|Triggerer'),
|
triggerer: s__('Deployment|Triggerer'),
|
||||||
job: __('Job'),
|
job: __('Job'),
|
||||||
api: __('API'),
|
api: __('API'),
|
||||||
|
branch: __('Branch'),
|
||||||
|
tag: __('Tag'),
|
||||||
},
|
},
|
||||||
headerClasses: [
|
headerClasses: [
|
||||||
'gl-display-flex',
|
'gl-display-flex',
|
||||||
|
@ -144,10 +159,12 @@ export default {
|
||||||
<div
|
<div
|
||||||
v-if="iid"
|
v-if="iid"
|
||||||
v-gl-tooltip
|
v-gl-tooltip
|
||||||
|
class="gl-display-flex"
|
||||||
:title="$options.i18n.deploymentId"
|
:title="$options.i18n.deploymentId"
|
||||||
:aria-label="$options.i18n.deploymentId"
|
:aria-label="$options.i18n.deploymentId"
|
||||||
>
|
>
|
||||||
<gl-icon ref="deployment-iid-icon" name="deployments" /> #{{ iid }}
|
<gl-icon ref="deployment-iid-icon" name="deployments" />
|
||||||
|
<span class="gl-ml-2">#{{ iid }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="shortSha"
|
v-if="shortSha"
|
||||||
|
@ -163,8 +180,11 @@ export default {
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<time-ago-tooltip v-if="createdAt" :time="createdAt">
|
<time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-display-flex">
|
||||||
<template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template>
|
<template #default="{ timeAgo }">
|
||||||
|
<gl-icon name="calendar" />
|
||||||
|
<span class="gl-mr-2 gl-white-space-nowrap">{{ timeAgo }}</span>
|
||||||
|
</template>
|
||||||
</time-ago-tooltip>
|
</time-ago-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,25 +200,40 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
<commit v-if="commit" :commit="commit" class="gl-mt-3" />
|
<commit v-if="commit" :commit="commit" class="gl-mt-3" />
|
||||||
<gl-collapse :visible="visible">
|
<gl-collapse :visible="visible">
|
||||||
<div class="gl-display-flex gl-align-items-center gl-mt-5">
|
<div
|
||||||
<div v-if="user" class="gl-display-flex gl-flex-direction-column">
|
class="gl-display-flex gl-md-align-items-center gl-mt-5 gl-flex-direction-column gl-md-flex-direction-row gl-pr-4 gl-md-pr-0"
|
||||||
<span class="gl-text-gray-500 gl-font-weight-bold">{{ $options.i18n.triggerer }}</span>
|
>
|
||||||
<gl-link :href="userPath" class="gl-font-monospace gl-mt-3"> @{{ username }} </gl-link>
|
<div v-if="user" class="gl-display-flex gl-flex-direction-column gl-md-max-w-15p">
|
||||||
|
<span class="gl-text-gray-500">{{ $options.i18n.triggerer }}</span>
|
||||||
|
<gl-link :href="userPath" class="gl-font-monospace gl-mt-3">
|
||||||
|
<gl-truncate :text="username" with-tooltip />
|
||||||
|
</gl-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="gl-display-flex gl-flex-direction-column gl-ml-5">
|
<div
|
||||||
<span class="gl-text-gray-500 gl-font-weight-bold" :class="{ 'gl-ml-3': !deployable }">
|
class="gl-display-flex gl-flex-direction-column gl-md-pl-7 gl-md-max-w-15p gl-mt-4 gl-md-mt-0"
|
||||||
|
>
|
||||||
|
<span class="gl-text-gray-500" :class="{ 'gl-ml-3': !deployable }">
|
||||||
{{ $options.i18n.job }}
|
{{ $options.i18n.job }}
|
||||||
</span>
|
</span>
|
||||||
<gl-link v-if="jobPath" :href="jobPath" class="gl-font-monospace gl-mt-3">
|
<gl-link v-if="jobPath" :href="jobPath" class="gl-font-monospace gl-mt-3">
|
||||||
{{ jobName }}
|
<gl-truncate :text="jobName" with-tooltip position="middle" />
|
||||||
</gl-link>
|
</gl-link>
|
||||||
<span v-else-if="jobName" class="gl-font-monospace gl-mt-3">
|
<span v-else-if="jobName" class="gl-font-monospace gl-mt-3">
|
||||||
{{ jobName }}
|
<gl-truncate :text="jobName" with-tooltip position="middle" />
|
||||||
</span>
|
</span>
|
||||||
<gl-badge v-else class="gl-font-monospace gl-mt-3" variant="info">
|
<gl-badge v-else class="gl-font-monospace gl-mt-3" variant="info">
|
||||||
{{ $options.i18n.api }}
|
{{ $options.i18n.api }}
|
||||||
</gl-badge>
|
</gl-badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="ref"
|
||||||
|
class="gl-display-flex gl-flex-direction-column gl-md-pl-7 gl-md-max-w-15p gl-mt-4 gl-md-mt-0"
|
||||||
|
>
|
||||||
|
<span class="gl-text-gray-500">{{ refLabel }}</span>
|
||||||
|
<gl-link :href="refPath" class="gl-font-monospace gl-mt-3">
|
||||||
|
<gl-truncate :text="refName" with-tooltip />
|
||||||
|
</gl-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</gl-collapse>
|
</gl-collapse>
|
||||||
</div>
|
</div>
|
||||||
|
|
27
app/assets/javascripts/runner/components/cells/link_cell.vue
Normal file
27
app/assets/javascripts/runner/components/cells/link_cell.vue
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { GlLink } from '@gitlab/ui';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
href: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
component() {
|
||||||
|
if (this.href) {
|
||||||
|
return GlLink;
|
||||||
|
}
|
||||||
|
return 'span';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="component" :href="href" v-bind="$attrs" v-on="$listeners">
|
||||||
|
<slot></slot>
|
||||||
|
</component>
|
||||||
|
</template>
|
|
@ -1,22 +1,26 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
|
import { GlBadge, GlTabs, GlTab, GlIntersperse } from '@gitlab/ui';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
|
||||||
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
|
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
|
||||||
|
import { formatJobCount } from '../utils';
|
||||||
import RunnerDetail from './runner_detail.vue';
|
import RunnerDetail from './runner_detail.vue';
|
||||||
import RunnerGroups from './runner_groups.vue';
|
import RunnerGroups from './runner_groups.vue';
|
||||||
import RunnerProjects from './runner_projects.vue';
|
import RunnerProjects from './runner_projects.vue';
|
||||||
|
import RunnerJobs from './runner_jobs.vue';
|
||||||
import RunnerTags from './runner_tags.vue';
|
import RunnerTags from './runner_tags.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
GlBadge,
|
||||||
GlTabs,
|
GlTabs,
|
||||||
GlTab,
|
GlTab,
|
||||||
GlIntersperse,
|
GlIntersperse,
|
||||||
RunnerDetail,
|
RunnerDetail,
|
||||||
RunnerGroups,
|
RunnerGroups,
|
||||||
RunnerProjects,
|
RunnerProjects,
|
||||||
|
RunnerJobs,
|
||||||
RunnerTags,
|
RunnerTags,
|
||||||
TimeAgo,
|
TimeAgo,
|
||||||
},
|
},
|
||||||
|
@ -53,6 +57,9 @@ export default {
|
||||||
isProjectRunner() {
|
isProjectRunner() {
|
||||||
return this.runner?.runnerType === PROJECT_TYPE;
|
return this.runner?.runnerType === PROJECT_TYPE;
|
||||||
},
|
},
|
||||||
|
jobCount() {
|
||||||
|
return formatJobCount(this.runner?.jobCount);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ACCESS_LEVEL_REF_PROTECTED,
|
ACCESS_LEVEL_REF_PROTECTED,
|
||||||
};
|
};
|
||||||
|
@ -65,7 +72,7 @@ export default {
|
||||||
|
|
||||||
<template v-if="runner">
|
<template v-if="runner">
|
||||||
<div class="gl-pt-4">
|
<div class="gl-pt-4">
|
||||||
<dl class="gl-mb-0">
|
<dl class="gl-mb-0" data-testid="runner-details-list">
|
||||||
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
|
<runner-detail :label="s__('Runners|Description')" :value="runner.description" />
|
||||||
<runner-detail
|
<runner-detail
|
||||||
:label="s__('Runners|Last contact')"
|
:label="s__('Runners|Last contact')"
|
||||||
|
@ -103,5 +110,15 @@ export default {
|
||||||
<runner-projects v-if="isProjectRunner" :runner="runner" />
|
<runner-projects v-if="isProjectRunner" :runner="runner" />
|
||||||
</template>
|
</template>
|
||||||
</gl-tab>
|
</gl-tab>
|
||||||
|
<gl-tab>
|
||||||
|
<template #title>
|
||||||
|
{{ s__('Runners|Jobs') }}
|
||||||
|
<gl-badge v-if="jobCount" data-testid="job-count-badge" class="gl-ml-1" size="sm">
|
||||||
|
{{ jobCount }}
|
||||||
|
</gl-badge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<runner-jobs v-if="runner" :runner="runner" />
|
||||||
|
</gl-tab>
|
||||||
</gl-tabs>
|
</gl-tabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
82
app/assets/javascripts/runner/components/runner_jobs.vue
Normal file
82
app/assets/javascripts/runner/components/runner_jobs.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||||
|
import { createAlert } from '~/flash';
|
||||||
|
import getRunnerJobsQuery from '../graphql/get_runner_jobs.query.graphql';
|
||||||
|
import { I18N_FETCH_ERROR, I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '../constants';
|
||||||
|
import { captureException } from '../sentry_utils';
|
||||||
|
import { getPaginationVariables } from '../utils';
|
||||||
|
import RunnerJobsTable from './runner_jobs_table.vue';
|
||||||
|
import RunnerPagination from './runner_pagination.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RunnerJobs',
|
||||||
|
components: {
|
||||||
|
GlSkeletonLoading,
|
||||||
|
RunnerJobsTable,
|
||||||
|
RunnerPagination,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
runner: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
jobs: {
|
||||||
|
items: [],
|
||||||
|
pageInfo: {},
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
jobs: {
|
||||||
|
query: getRunnerJobsQuery,
|
||||||
|
variables() {
|
||||||
|
return this.variables;
|
||||||
|
},
|
||||||
|
update({ runner }) {
|
||||||
|
return {
|
||||||
|
items: runner?.jobs?.nodes || [],
|
||||||
|
pageInfo: runner?.jobs?.pageInfo || {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
error(error) {
|
||||||
|
createAlert({ message: I18N_FETCH_ERROR });
|
||||||
|
this.reportToSentry(error);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
variables() {
|
||||||
|
const { id } = this.runner;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
...getPaginationVariables(this.pagination, RUNNER_DETAILS_JOBS_PAGE_SIZE),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
loading() {
|
||||||
|
return this.$apollo.queries.jobs.loading;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
reportToSentry(error) {
|
||||||
|
captureException({ error, component: this.$options.name });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
I18N_NO_JOBS_FOUND,
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="gl-pt-3">
|
||||||
|
<gl-skeleton-loading v-if="loading" class="gl-py-5" />
|
||||||
|
<runner-jobs-table v-else-if="jobs.items.length" :jobs="jobs.items" />
|
||||||
|
<p v-else>{{ $options.I18N_NO_JOBS_FOUND }}</p>
|
||||||
|
|
||||||
|
<runner-pagination v-model="pagination" :disabled="loading" :page-info="jobs.pageInfo" />
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<script>
|
||||||
|
import { GlTableLite } from '@gitlab/ui';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
|
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||||
|
import RunnerTags from '~/runner/components/runner_tags.vue';
|
||||||
|
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
import { tableField } from '../utils';
|
||||||
|
import LinkCell from './cells/link_cell.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CiBadge,
|
||||||
|
GlTableLite,
|
||||||
|
LinkCell,
|
||||||
|
RunnerTags,
|
||||||
|
TimeAgo,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
jobs: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
trAttr(job) {
|
||||||
|
if (job?.id) {
|
||||||
|
return { 'data-testid': `job-row-${getIdFromGraphQLId(job.id)}` };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
jobId(job) {
|
||||||
|
return getIdFromGraphQLId(job.id);
|
||||||
|
},
|
||||||
|
jobPath(job) {
|
||||||
|
return job.detailedStatus?.detailsPath;
|
||||||
|
},
|
||||||
|
projectName(job) {
|
||||||
|
return job.pipeline?.project?.name;
|
||||||
|
},
|
||||||
|
projectWebUrl(job) {
|
||||||
|
return job.pipeline?.project?.webUrl;
|
||||||
|
},
|
||||||
|
commitShortSha(job) {
|
||||||
|
return job.shortSha;
|
||||||
|
},
|
||||||
|
commitPath(job) {
|
||||||
|
return job.commitPath;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
tableField({ key: 'status', label: s__('Job|Status') }),
|
||||||
|
tableField({ key: 'job', label: __('Job') }),
|
||||||
|
tableField({ key: 'project', label: __('Project') }),
|
||||||
|
tableField({ key: 'commit', label: __('Commit') }),
|
||||||
|
tableField({ key: 'finished_at', label: s__('Job|Finished at') }),
|
||||||
|
tableField({ key: 'tags', label: s__('Runners|Tags') }),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<gl-table-lite
|
||||||
|
:items="jobs"
|
||||||
|
:fields="$options.fields"
|
||||||
|
:tbody-tr-attr="trAttr"
|
||||||
|
primary-key="id"
|
||||||
|
stacked="md"
|
||||||
|
fixed
|
||||||
|
>
|
||||||
|
<template #cell(status)="{ item = {} }">
|
||||||
|
<ci-badge v-if="item.detailedStatus" :status="item.detailedStatus" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(job)="{ item = {} }">
|
||||||
|
<link-cell :href="jobPath(item)"> #{{ jobId(item) }} </link-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(project)="{ item = {} }">
|
||||||
|
<link-cell :href="projectWebUrl(item)">{{ projectName(item) }}</link-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(commit)="{ item = {} }">
|
||||||
|
<link-cell :href="commitPath(item)"> {{ commitShortSha(item) }}</link-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(tags)="{ item = {} }">
|
||||||
|
<runner-tags :tag-list="item.tags" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(finished_at)="{ item = {} }">
|
||||||
|
<time-ago v-if="item.finishedAt" :time="item.finishedAt" />
|
||||||
|
</template>
|
||||||
|
</gl-table-lite>
|
||||||
|
</template>
|
|
@ -4,6 +4,7 @@ export const RUNNER_PAGE_SIZE = 20;
|
||||||
export const RUNNER_JOB_COUNT_LIMIT = 1000;
|
export const RUNNER_JOB_COUNT_LIMIT = 1000;
|
||||||
|
|
||||||
export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5;
|
export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5;
|
||||||
|
export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30;
|
||||||
|
|
||||||
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}');
|
||||||
|
@ -45,6 +46,7 @@ export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run
|
||||||
|
|
||||||
export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
|
export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
|
||||||
export const I18N_NONE = __('None');
|
export const I18N_NONE = __('None');
|
||||||
|
export const I18N_NO_JOBS_FOUND = s__('Runner|This runner has not run any jobs.');
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
|
||||||
|
|
||||||
|
query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String, $after: String) {
|
||||||
|
runner(id: $id) {
|
||||||
|
id
|
||||||
|
projectCount
|
||||||
|
jobs(before: $before, after: $after, first: $first, last: $last) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
detailedStatus {
|
||||||
|
# fields for `<ci-badge>`
|
||||||
|
id
|
||||||
|
detailsPath
|
||||||
|
group
|
||||||
|
icon
|
||||||
|
text
|
||||||
|
}
|
||||||
|
pipeline {
|
||||||
|
id
|
||||||
|
project {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
webUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shortSha
|
||||||
|
commitPath
|
||||||
|
tags
|
||||||
|
finishedAt
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
...PageInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ fragment RunnerDetailsShared on CiRunner {
|
||||||
ipAddress
|
ipAddress
|
||||||
description
|
description
|
||||||
maximumTimeout
|
maximumTimeout
|
||||||
|
jobCount
|
||||||
tagList
|
tagList
|
||||||
createdAt
|
createdAt
|
||||||
status(legacyMode: null)
|
status(legacyMode: null)
|
||||||
|
|
|
@ -1,37 +1,36 @@
|
||||||
|
# Make sure that this file has the keys sorted
|
||||||
---
|
---
|
||||||
dast_site_profiles_pipelines:
|
ci_build_report_results:
|
||||||
- table: ci_pipelines
|
|
||||||
column: ci_pipeline_id
|
|
||||||
on_delete: async_delete
|
|
||||||
vulnerability_feedback:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: pipeline_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_pipeline_chat_data:
|
|
||||||
- table: chat_names
|
|
||||||
column: chat_name_id
|
|
||||||
on_delete: async_delete
|
|
||||||
dast_scanner_profiles_builds:
|
|
||||||
- table: ci_builds
|
|
||||||
column: ci_build_id
|
|
||||||
on_delete: async_delete
|
|
||||||
dast_site_profiles_builds:
|
|
||||||
- table: ci_builds
|
|
||||||
column: ci_build_id
|
|
||||||
on_delete: async_delete
|
|
||||||
dast_profiles_pipelines:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: ci_pipeline_id
|
|
||||||
on_delete: async_delete
|
|
||||||
clusters_applications_runners:
|
|
||||||
- table: ci_runners
|
|
||||||
column: runner_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_variables:
|
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_runner_projects:
|
ci_builds:
|
||||||
|
- table: users
|
||||||
|
column: user_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_builds_metadata:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_daily_build_group_report_results:
|
||||||
|
- table: namespaces
|
||||||
|
column: group_id
|
||||||
|
on_delete: async_delete
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_freeze_periods:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_group_variables:
|
||||||
|
- table: namespaces
|
||||||
|
column: group_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_job_artifacts:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
|
@ -45,20 +44,13 @@ ci_job_token_project_scope_links:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: target_project_id
|
column: target_project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_daily_build_group_report_results:
|
ci_minutes_additional_packs:
|
||||||
- table: namespaces
|
- table: namespaces
|
||||||
column: group_id
|
column: namespace_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
- table: projects
|
ci_namespace_mirrors:
|
||||||
column: project_id
|
- table: namespaces
|
||||||
on_delete: async_delete
|
column: namespace_id
|
||||||
external_pull_requests:
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_freeze_periods:
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_pending_builds:
|
ci_pending_builds:
|
||||||
- table: namespaces
|
- table: namespaces
|
||||||
|
@ -67,37 +59,17 @@ ci_pending_builds:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_resource_groups:
|
ci_pipeline_artifacts:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_runner_namespaces:
|
ci_pipeline_chat_data:
|
||||||
- table: namespaces
|
- table: chat_names
|
||||||
column: namespace_id
|
column: chat_name_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_running_builds:
|
ci_pipeline_schedules:
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_namespace_mirrors:
|
|
||||||
- table: namespaces
|
|
||||||
column: namespace_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_sources_projects:
|
|
||||||
- table: projects
|
|
||||||
column: source_project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_build_report_results:
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_job_artifacts:
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_builds:
|
|
||||||
- table: users
|
- table: users
|
||||||
column: user_id
|
column: owner_id
|
||||||
on_delete: async_nullify
|
on_delete: async_nullify
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
|
@ -122,97 +94,31 @@ ci_project_mirrors:
|
||||||
- table: namespaces
|
- table: namespaces
|
||||||
column: namespace_id
|
column: namespace_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_unit_tests:
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
merge_requests:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: head_pipeline_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
vulnerability_statistics:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: latest_pipeline_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
vulnerability_occurrence_pipelines:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: pipeline_id
|
|
||||||
on_delete: async_delete
|
|
||||||
packages_build_infos:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: pipeline_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
packages_package_file_build_infos:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: pipeline_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_project_monthly_usages:
|
ci_project_monthly_usages:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
pages_deployments:
|
|
||||||
- table: ci_builds
|
|
||||||
column: ci_build_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_builds_metadata:
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
terraform_state_versions:
|
|
||||||
- table: ci_builds
|
|
||||||
column: ci_build_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
merge_request_metrics:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: pipeline_id
|
|
||||||
on_delete: async_delete
|
|
||||||
project_pages_metadata:
|
|
||||||
- table: ci_job_artifacts
|
|
||||||
column: artifacts_archive_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_pipeline_schedules:
|
|
||||||
- table: users
|
|
||||||
column: owner_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
- table: projects
|
|
||||||
column: project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
merge_trains:
|
|
||||||
- table: ci_pipelines
|
|
||||||
column: pipeline_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_refs:
|
ci_refs:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_group_variables:
|
ci_resource_groups:
|
||||||
- table: namespaces
|
|
||||||
column: group_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_minutes_additional_packs:
|
|
||||||
- table: namespaces
|
|
||||||
column: namespace_id
|
|
||||||
on_delete: async_delete
|
|
||||||
requirements_management_test_reports:
|
|
||||||
- table: ci_builds
|
|
||||||
column: build_id
|
|
||||||
on_delete: async_nullify
|
|
||||||
ci_subscriptions_projects:
|
|
||||||
- table: projects
|
|
||||||
column: downstream_project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
- table: projects
|
|
||||||
column: upstream_project_id
|
|
||||||
on_delete: async_delete
|
|
||||||
security_scans:
|
|
||||||
- table: ci_builds
|
|
||||||
column: build_id
|
|
||||||
on_delete: async_delete
|
|
||||||
ci_secure_files:
|
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
ci_pipeline_artifacts:
|
ci_runner_namespaces:
|
||||||
|
- table: namespaces
|
||||||
|
column: namespace_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_runner_projects:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_running_builds:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_secure_files:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
|
@ -223,10 +129,21 @@ ci_sources_pipelines:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
|
ci_sources_projects:
|
||||||
|
- table: projects
|
||||||
|
column: source_project_id
|
||||||
|
on_delete: async_delete
|
||||||
ci_stages:
|
ci_stages:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
|
ci_subscriptions_projects:
|
||||||
|
- table: projects
|
||||||
|
column: downstream_project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
- table: projects
|
||||||
|
column: upstream_project_id
|
||||||
|
on_delete: async_delete
|
||||||
ci_triggers:
|
ci_triggers:
|
||||||
- table: users
|
- table: users
|
||||||
column: owner_id
|
column: owner_id
|
||||||
|
@ -234,3 +151,87 @@ ci_triggers:
|
||||||
- table: projects
|
- table: projects
|
||||||
column: project_id
|
column: project_id
|
||||||
on_delete: async_delete
|
on_delete: async_delete
|
||||||
|
ci_unit_tests:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
ci_variables:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
clusters_applications_runners:
|
||||||
|
- table: ci_runners
|
||||||
|
column: runner_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
dast_profiles_pipelines:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: ci_pipeline_id
|
||||||
|
on_delete: async_delete
|
||||||
|
dast_scanner_profiles_builds:
|
||||||
|
- table: ci_builds
|
||||||
|
column: ci_build_id
|
||||||
|
on_delete: async_delete
|
||||||
|
dast_site_profiles_builds:
|
||||||
|
- table: ci_builds
|
||||||
|
column: ci_build_id
|
||||||
|
on_delete: async_delete
|
||||||
|
dast_site_profiles_pipelines:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: ci_pipeline_id
|
||||||
|
on_delete: async_delete
|
||||||
|
external_pull_requests:
|
||||||
|
- table: projects
|
||||||
|
column: project_id
|
||||||
|
on_delete: async_delete
|
||||||
|
merge_request_metrics:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: pipeline_id
|
||||||
|
on_delete: async_delete
|
||||||
|
merge_requests:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: head_pipeline_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
merge_trains:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: pipeline_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
packages_build_infos:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: pipeline_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
packages_package_file_build_infos:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: pipeline_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
pages_deployments:
|
||||||
|
- table: ci_builds
|
||||||
|
column: ci_build_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
project_pages_metadata:
|
||||||
|
- table: ci_job_artifacts
|
||||||
|
column: artifacts_archive_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
requirements_management_test_reports:
|
||||||
|
- table: ci_builds
|
||||||
|
column: build_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
security_scans:
|
||||||
|
- table: ci_builds
|
||||||
|
column: build_id
|
||||||
|
on_delete: async_delete
|
||||||
|
terraform_state_versions:
|
||||||
|
- table: ci_builds
|
||||||
|
column: ci_build_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
vulnerability_feedback:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: pipeline_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
vulnerability_occurrence_pipelines:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: pipeline_id
|
||||||
|
on_delete: async_delete
|
||||||
|
vulnerability_statistics:
|
||||||
|
- table: ci_pipelines
|
||||||
|
column: latest_pipeline_id
|
||||||
|
on_delete: async_nullify
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddScanMethodToDastSiteProfile < Gitlab::Database::Migration[1.0]
|
||||||
|
def up
|
||||||
|
add_column :dast_site_profiles, :scan_method, :integer, limit: 2, default: 0, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :dast_site_profiles, :scan_method
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||||
|
# for more information on how to write migrations for GitLab.
|
||||||
|
|
||||||
|
class UpdateDefaultScanMethodOfDastSiteProfile < Gitlab::Database::Migration[1.0]
|
||||||
|
BATCH_SIZE = 500
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
each_batch_range('dast_site_profiles', scope: ->(table) { table.where(target_type: 1) }, of: BATCH_SIZE) do |min, max|
|
||||||
|
execute <<~SQL
|
||||||
|
UPDATE dast_site_profiles
|
||||||
|
SET scan_method = 1
|
||||||
|
WHERE id BETWEEN #{min} AND #{max}
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# noop
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class FixApprovalRulesCodeOwnersRuleTypeIndex < Gitlab::Database::Migration[1.0]
|
||||||
|
INDEX_NAME = 'index_approval_rules_code_owners_rule_type'
|
||||||
|
OLD_INDEX_NAME = 'index_approval_rules_code_owners_rule_type_old'
|
||||||
|
TABLE = :approval_merge_request_rules
|
||||||
|
COLUMN = :merge_request_id
|
||||||
|
WHERE_CONDITION = 'rule_type = 2'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
rename_index TABLE, INDEX_NAME, OLD_INDEX_NAME if index_exists_by_name?(TABLE, INDEX_NAME) && !index_exists_by_name?(TABLE, OLD_INDEX_NAME)
|
||||||
|
|
||||||
|
add_concurrent_index TABLE, COLUMN, where: WHERE_CONDITION, name: INDEX_NAME
|
||||||
|
|
||||||
|
remove_concurrent_index_by_name TABLE, OLD_INDEX_NAME
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
# No-op
|
||||||
|
end
|
||||||
|
end
|
1
db/schema_migrations/20220119220620
Normal file
1
db/schema_migrations/20220119220620
Normal file
|
@ -0,0 +1 @@
|
||||||
|
535f476a358dcb3f3472f1e0ec1afef738f995197b5d1f4fcd61e58a9c9e8e75
|
1
db/schema_migrations/20220128155814
Normal file
1
db/schema_migrations/20220128155814
Normal file
|
@ -0,0 +1 @@
|
||||||
|
77cc8fc86f2c6a5ed017dde40dd4db796821a35e6ce4d8dcbe24b2cdaccbb5d9
|
1
db/schema_migrations/20220208171826
Normal file
1
db/schema_migrations/20220208171826
Normal file
|
@ -0,0 +1 @@
|
||||||
|
e48473172d7561fb7474e16e291e555843c0ec4543300b007f86cd4a5923db85
|
|
@ -13414,6 +13414,7 @@ CREATE TABLE dast_site_profiles (
|
||||||
auth_password_field text,
|
auth_password_field text,
|
||||||
auth_username text,
|
auth_username text,
|
||||||
target_type smallint DEFAULT 0 NOT NULL,
|
target_type smallint DEFAULT 0 NOT NULL,
|
||||||
|
scan_method smallint DEFAULT 0 NOT NULL,
|
||||||
CONSTRAINT check_5203110fee CHECK ((char_length(auth_username_field) <= 255)),
|
CONSTRAINT check_5203110fee CHECK ((char_length(auth_username_field) <= 255)),
|
||||||
CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)),
|
CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)),
|
||||||
CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)),
|
CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)),
|
||||||
|
|
|
@ -17,6 +17,7 @@ swap:
|
||||||
e-mail: '"email"'
|
e-mail: '"email"'
|
||||||
GFM: '"GitLab Flavored Markdown"'
|
GFM: '"GitLab Flavored Markdown"'
|
||||||
it is recommended: '"we recommend"'
|
it is recommended: '"we recommend"'
|
||||||
|
navigate: go
|
||||||
OAuth2: '"OAuth 2.0"'
|
OAuth2: '"OAuth 2.0"'
|
||||||
once that: '"after that"'
|
once that: '"after that"'
|
||||||
once the: '"after the"'
|
once the: '"after the"'
|
||||||
|
|
|
@ -561,6 +561,8 @@ Do not use **navigate**. Use **go** instead. For example:
|
||||||
- Go to this webpage.
|
- Go to this webpage.
|
||||||
- Open a terminal and go to the `runner` directory.
|
- Open a terminal and go to the `runner` directory.
|
||||||
|
|
||||||
|
([Vale](../testing.md#vale) rule: [`SubstitutionSuggestions.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionSuggestions.yml))
|
||||||
|
|
||||||
## need to, should
|
## need to, should
|
||||||
|
|
||||||
Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**.
|
Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**.
|
||||||
|
|
|
@ -15,6 +15,7 @@ For any of the following scenarios, the `start-review-app-pipeline` job would be
|
||||||
- for merge requests with CI config changes
|
- for merge requests with CI config changes
|
||||||
- for merge requests with frontend changes
|
- for merge requests with frontend changes
|
||||||
- for merge requests with changes to `{,ee/,jh/}{app/controllers}/**/*`
|
- for merge requests with changes to `{,ee/,jh/}{app/controllers}/**/*`
|
||||||
|
- for merge requests with changes to `{,ee/,jh/}{app/models}/**/*`
|
||||||
- for merge requests with QA changes
|
- for merge requests with QA changes
|
||||||
- for scheduled pipelines
|
- for scheduled pipelines
|
||||||
- the MR has the `pipeline:run-review-app` label set
|
- the MR has the `pipeline:run-review-app` label set
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
BIN
doc/user/analytics/img/issues_created_per_month_v14_8.png
Normal file
BIN
doc/user/analytics/img/issues_created_per_month_v14_8.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -34,7 +34,7 @@ You can change the total number of months displayed by setting a URL parameter.
|
||||||
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
|
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
|
||||||
shows a total of 15 months for the chart in the GitLab.org group.
|
shows a total of 15 months for the chart in the GitLab.org group.
|
||||||
|
|
||||||
![Issues created per month](img/issues_created_per_month_v13_11.png)
|
![Issues created per month](img/issues_created_per_month_v14_8.png)
|
||||||
|
|
||||||
## Drill into the information
|
## Drill into the information
|
||||||
|
|
||||||
|
|
BIN
doc/user/profile/img/personal_readme_setup_v14_5.png
Normal file
BIN
doc/user/profile/img/personal_readme_setup_v14_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -102,20 +102,36 @@ user profiles are only visible to signed-in users.
|
||||||
|
|
||||||
## Add details to your profile with a README
|
## Add details to your profile with a README
|
||||||
|
|
||||||
### *Add personal README to profile*
|
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
|
||||||
|
|
||||||
If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page.
|
You can add more information to your profile page with a README file. When you populate
|
||||||
|
the README file with information, it's included on your profile page.
|
||||||
|
|
||||||
To add a README to your profile:
|
### From a new project
|
||||||
|
|
||||||
1. Create a new public project with the same project path as your GitLab username.
|
To create a new project and add its README to your profile:
|
||||||
|
|
||||||
|
1. On the top bar, select **Menu > Project**.
|
||||||
|
1. Select **Create new project**.
|
||||||
|
1. Select **Create blank project**.
|
||||||
|
1. Enter the project details:
|
||||||
|
- In the **Project name** field, enter the name for your new project.
|
||||||
|
- In the **Project URL** field, select your GitLab username.
|
||||||
|
- In the **Project slug** field, enter your GitLab username.
|
||||||
|
1. For **Visibility Level**, select **Public**.
|
||||||
|
![Proper project path for an individual on the hosted product](img/personal_readme_setup_v14_5.png)
|
||||||
|
1. For **Project Configuration**, ensure **Initialize repository with a README** is selected.
|
||||||
|
1. Select **Create project**.
|
||||||
1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files).
|
1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files).
|
||||||
1. Populate the README file with [Markdown](../markdown.md).
|
1. Populate the README file with [Markdown](../markdown.md).
|
||||||
|
|
||||||
To use an existing project, [update the path](../project/settings/index.md#renaming-a-repository) of the project to match
|
GitLab displays the contents of your README below your contribution graph.
|
||||||
your username.
|
|
||||||
|
### From an existing project
|
||||||
|
|
||||||
|
To add the README from an existing project to your profile,
|
||||||
|
[update the path](../project/settings/index.md#renaming-a-repository) of the project
|
||||||
|
to match your username.
|
||||||
|
|
||||||
## Add external accounts to your user profile page
|
## Add external accounts to your user profile page
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
LOG_MAX_DURATION_THRESHOLD = 2.seconds
|
||||||
|
|
||||||
def initialize(project:, current_user:, sha: nil)
|
def initialize(project:, current_user:, sha: nil)
|
||||||
@project = project
|
@project = project
|
||||||
@current_user = current_user
|
@current_user = current_user
|
||||||
|
@ -49,12 +51,9 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def static_validation(content)
|
def static_validation(content)
|
||||||
result = Gitlab::Ci::YamlProcessor.new(
|
logger = build_logger
|
||||||
content,
|
|
||||||
project: @project,
|
result = yaml_processor_result(content, logger)
|
||||||
user: @current_user,
|
|
||||||
sha: @sha
|
|
||||||
).execute
|
|
||||||
|
|
||||||
Result.new(
|
Result.new(
|
||||||
jobs: static_validation_convert_to_jobs(result),
|
jobs: static_validation_convert_to_jobs(result),
|
||||||
|
@ -62,6 +61,17 @@ module Gitlab
|
||||||
errors: result.errors,
|
errors: result.errors,
|
||||||
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
|
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
|
||||||
)
|
)
|
||||||
|
ensure
|
||||||
|
logger.commit(pipeline: ::Ci::Pipeline.new, caller: self.class.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def yaml_processor_result(content, logger)
|
||||||
|
logger.instrument(:yaml_process) do
|
||||||
|
Gitlab::Ci::YamlProcessor.new(content, project: @project,
|
||||||
|
user: @current_user,
|
||||||
|
sha: @sha,
|
||||||
|
logger: logger).execute
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def dry_run_convert_to_jobs(stages)
|
def dry_run_convert_to_jobs(stages)
|
||||||
|
@ -109,6 +119,17 @@ module Gitlab
|
||||||
|
|
||||||
jobs
|
jobs
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_logger
|
||||||
|
Gitlab::Ci::Pipeline::Logger.new(project: @project) do |l|
|
||||||
|
l.log_when do |observations|
|
||||||
|
values = observations['yaml_process_duration_s']
|
||||||
|
next false if values.empty?
|
||||||
|
|
||||||
|
values.max >= LOG_MAX_DURATION_THRESHOLD
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -59,7 +59,7 @@ module Gitlab
|
||||||
attributes = {
|
attributes = {
|
||||||
class: self.class.name.to_s,
|
class: self.class.name.to_s,
|
||||||
pipeline_creation_caller: caller,
|
pipeline_creation_caller: caller,
|
||||||
project_id: project.id,
|
project_id: project&.id, # project is not available when called from `/ci/lint`
|
||||||
pipeline_persisted: pipeline.persisted?,
|
pipeline_persisted: pipeline.persisted?,
|
||||||
pipeline_source: pipeline.source,
|
pipeline_source: pipeline.source,
|
||||||
pipeline_creation_service_duration_s: age
|
pipeline_creation_service_duration_s: age
|
||||||
|
|
|
@ -109,6 +109,26 @@ module Gitlab
|
||||||
name.to_s == CI_DATABASE_NAME
|
name.to_s == CI_DATABASE_NAME
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class PgUser < ApplicationRecord
|
||||||
|
self.table_name = 'pg_user'
|
||||||
|
self.primary_key = :usename
|
||||||
|
end
|
||||||
|
|
||||||
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
|
def self.check_for_non_superuser
|
||||||
|
user = PgUser.find_by('usename = CURRENT_USER')
|
||||||
|
am_i_superuser = user.usesuper
|
||||||
|
|
||||||
|
Gitlab::AppLogger.info(
|
||||||
|
"Account details: User: \"#{user.usename}\", UseSuper: (#{am_i_superuser})"
|
||||||
|
)
|
||||||
|
|
||||||
|
raise 'Error: detected superuser' if am_i_superuser
|
||||||
|
rescue ActiveRecord::StatementInvalid
|
||||||
|
raise 'User CURRENT_USER not found'
|
||||||
|
end
|
||||||
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
def self.check_postgres_version_and_print_warning
|
def self.check_postgres_version_and_print_warning
|
||||||
return if Gitlab::Runtime.rails_runner?
|
return if Gitlab::Runtime.rails_runner?
|
||||||
|
|
||||||
|
|
|
@ -270,6 +270,19 @@ namespace :gitlab do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'Run migration as gitlab non-superuser'
|
||||||
|
task :reset_as_non_superuser, [:username] => :environment do |_, args|
|
||||||
|
username = args.fetch(:username, 'gitlab')
|
||||||
|
puts "Migrate using username #{username}"
|
||||||
|
Rake::Task['db:drop'].invoke
|
||||||
|
Rake::Task['db:create'].invoke
|
||||||
|
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
|
||||||
|
ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(username: username)) # rubocop: disable Database/EstablishConnection
|
||||||
|
Gitlab::Database.check_for_non_superuser
|
||||||
|
Rake::Task['db:migrate'].invoke
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Only for development environments,
|
# Only for development environments,
|
||||||
# we execute pending data migrations inline for convenience.
|
# we execute pending data migrations inline for convenience.
|
||||||
Rake::Task['db:migrate'].enhance do
|
Rake::Task['db:migrate'].enhance do
|
||||||
|
|
|
@ -20374,6 +20374,9 @@ msgstr ""
|
||||||
msgid "It looks like you have some draft commits in this branch."
|
msgid "It looks like you have some draft commits in this branch."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "It looks like you're attempting to activate your subscription. Use %{a_start}the Subscription page%{a_end} instead."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "It may be several days before you see feature usage data."
|
msgid "It may be several days before you see feature usage data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20878,6 +20881,9 @@ msgstr ""
|
||||||
msgid "Job|Erase job log and artifacts"
|
msgid "Job|Erase job log and artifacts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Job|Finished at"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Job|Job artifacts"
|
msgid "Job|Job artifacts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20902,6 +20908,9 @@ msgstr ""
|
||||||
msgid "Job|Show complete raw"
|
msgid "Job|Show complete raw"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Job|Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Job|The artifacts were removed"
|
msgid "Job|The artifacts were removed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27138,9 +27147,6 @@ msgstr ""
|
||||||
msgid "Please enter a valid time interval"
|
msgid "Please enter a valid time interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Please enter or upload a valid license."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Please enter your current password."
|
msgid "Please enter your current password."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -31321,6 +31327,9 @@ msgstr ""
|
||||||
msgid "Runners|Instance"
|
msgid "Runners|Instance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runners|Jobs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Runners|Last contact"
|
msgid "Runners|Last contact"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -31561,6 +31570,9 @@ msgstr ""
|
||||||
msgid "Runners|stale"
|
msgid "Runners|stale"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Runner|This runner has not run any jobs."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Running"
|
msgid "Running"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -36336,6 +36348,9 @@ msgstr ""
|
||||||
msgid "The latest pipeline for this merge request has failed."
|
msgid "The latest pipeline for this merge request has failed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The license key is invalid."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
|
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -36351,6 +36366,9 @@ msgstr ""
|
||||||
msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
|
msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "The license you uploaded is invalid. If the issue persists, contact support at %{link}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "The list creation wizard is already open"
|
msgid "The list creation wizard is already open"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -213,8 +213,28 @@ describe('~/environments/components/deployment.vue', () => {
|
||||||
expect(job.attributes('href')).toBe(deployment.deployable.buildPath);
|
expect(job.attributes('href')).toBe(deployment.deployable.buildPath);
|
||||||
const apiBadge = wrapper.findByText(__('API'));
|
const apiBadge = wrapper.findByText(__('API'));
|
||||||
expect(apiBadge.exists()).toBe(false);
|
expect(apiBadge.exists()).toBe(false);
|
||||||
|
|
||||||
|
const branchLabel = wrapper.findByText(__('Branch'));
|
||||||
|
expect(branchLabel.exists()).toBe(true);
|
||||||
|
const tagLabel = wrapper.findByText(__('Tag'));
|
||||||
|
expect(tagLabel.exists()).toBe(false);
|
||||||
|
const ref = wrapper.findByRole('link', { name: deployment.ref.name });
|
||||||
|
expect(ref.attributes('href')).toBe(deployment.ref.refPath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with tagged deployment', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } });
|
||||||
|
await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows tag instead of branch', () => {
|
||||||
|
const refLabel = wrapper.findByText(__('Tag'));
|
||||||
|
expect(refLabel.exists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('with API deployment', () => {
|
describe('with API deployment', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
|
wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
|
||||||
|
@ -237,7 +257,7 @@ describe('~/environments/components/deployment.vue', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a span instead of a link', () => {
|
it('shows a span instead of a link', () => {
|
||||||
const job = wrapper.findByText(deployment.deployable.name);
|
const job = wrapper.findByTitle(deployment.deployable.name);
|
||||||
expect(job.attributes('href')).toBeUndefined();
|
expect(job.attributes('href')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
|
||||||
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
|
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
|
||||||
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
|
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
|
||||||
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
|
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
|
||||||
|
let_it_be(:build) { create(:ci_build, runner: instance_runner) }
|
||||||
|
|
||||||
query_path = 'runner/graphql/'
|
query_path = 'runner/graphql/'
|
||||||
fixtures_path = 'graphql/runner/'
|
fixtures_path = 'graphql/runner/'
|
||||||
|
@ -104,6 +105,22 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
|
||||||
expect_graphql_errors_to_be_empty
|
expect_graphql_errors_to_be_empty
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe GraphQL::Query, type: :request do
|
||||||
|
get_runner_jobs_query_name = 'get_runner_jobs.query.graphql'
|
||||||
|
|
||||||
|
let_it_be(:query) do
|
||||||
|
get_graphql_query_as_string("#{query_path}#{get_runner_jobs_query_name}")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "#{fixtures_path}#{get_runner_jobs_query_name}.json" do
|
||||||
|
post_graphql(query, current_user: admin, variables: {
|
||||||
|
id: instance_runner.to_global_id.to_s
|
||||||
|
})
|
||||||
|
|
||||||
|
expect_graphql_errors_to_be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe do
|
describe do
|
||||||
|
|
72
spec/frontend/runner/components/cells/link_cell_spec.js
Normal file
72
spec/frontend/runner/components/cells/link_cell_spec.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { GlLink } from '@gitlab/ui';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import LinkCell from '~/runner/components/cells/link_cell.vue';
|
||||||
|
|
||||||
|
describe('LinkCell', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const findGlLink = () => wrapper.find(GlLink);
|
||||||
|
const findSpan = () => wrapper.find('span');
|
||||||
|
|
||||||
|
const createComponent = ({ props = {}, ...options } = {}) => {
|
||||||
|
wrapper = shallowMountExtended(LinkCell, {
|
||||||
|
propsData: {
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
it('when an href is provided, renders a link', () => {
|
||||||
|
createComponent({ props: { href: '/url' } });
|
||||||
|
expect(findGlLink().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when an href is not provided, renders no link', () => {
|
||||||
|
createComponent();
|
||||||
|
expect(findGlLink().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each`
|
||||||
|
href | findContent
|
||||||
|
${null} | ${findSpan}
|
||||||
|
${'/url'} | ${findGlLink}
|
||||||
|
`('When href is $href', ({ href, findContent }) => {
|
||||||
|
const content = 'My Text';
|
||||||
|
const attrs = { foo: 'bar' };
|
||||||
|
const listeners = {
|
||||||
|
click: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({
|
||||||
|
props: { href },
|
||||||
|
slots: {
|
||||||
|
default: content,
|
||||||
|
},
|
||||||
|
attrs,
|
||||||
|
listeners,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
listeners.click.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders content', () => {
|
||||||
|
expect(findContent().text()).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Passes attributes', () => {
|
||||||
|
expect(findContent().attributes()).toMatchObject(attrs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Passes event listeners', () => {
|
||||||
|
expect(listeners.click).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
findContent().vm.$emit('click');
|
||||||
|
|
||||||
|
expect(listeners.click).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { GlSprintf, GlIntersperse } from '@gitlab/ui';
|
import { GlSprintf, GlIntersperse, GlTab } from '@gitlab/ui';
|
||||||
import { createWrapper, ErrorWrapper } from '@vue/test-utils';
|
import { createWrapper, ErrorWrapper } from '@vue/test-utils';
|
||||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
|
@ -8,6 +8,7 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner
|
||||||
import RunnerDetails from '~/runner/components/runner_details.vue';
|
import RunnerDetails from '~/runner/components/runner_details.vue';
|
||||||
import RunnerDetail from '~/runner/components/runner_detail.vue';
|
import RunnerDetail from '~/runner/components/runner_detail.vue';
|
||||||
import RunnerGroups from '~/runner/components/runner_groups.vue';
|
import RunnerGroups from '~/runner/components/runner_groups.vue';
|
||||||
|
import RunnersJobs from '~/runner/components/runner_jobs.vue';
|
||||||
import RunnerTags from '~/runner/components/runner_tags.vue';
|
import RunnerTags from '~/runner/components/runner_tags.vue';
|
||||||
import RunnerTag from '~/runner/components/runner_tag.vue';
|
import RunnerTag from '~/runner/components/runner_tag.vue';
|
||||||
|
|
||||||
|
@ -38,6 +39,8 @@ describe('RunnerDetails', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
|
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
|
||||||
|
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
|
||||||
|
const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
|
||||||
|
|
||||||
const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
|
const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
|
||||||
wrapper = mountFn(RunnerDetails, {
|
wrapper = mountFn(RunnerDetails, {
|
||||||
|
@ -146,4 +149,41 @@ describe('RunnerDetails', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Jobs tab', () => {
|
||||||
|
const stubs = { GlTab };
|
||||||
|
|
||||||
|
it('without a runner, shows no jobs', () => {
|
||||||
|
createComponent({
|
||||||
|
props: { runner: null },
|
||||||
|
stubs,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findJobCountBadge().exists()).toBe(false);
|
||||||
|
expect(findRunnersJobs().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without a job count, shows no jobs count', () => {
|
||||||
|
createComponent({
|
||||||
|
props: {
|
||||||
|
runner: { ...mockRunner, jobCount: undefined },
|
||||||
|
},
|
||||||
|
stubs,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findJobCountBadge().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with a job count, shows jobs count', () => {
|
||||||
|
const runner = { ...mockRunner, jobCount: 3 };
|
||||||
|
|
||||||
|
createComponent({
|
||||||
|
props: { runner },
|
||||||
|
stubs,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findJobCountBadge().text()).toBe('3');
|
||||||
|
expect(findRunnersJobs().props('runner')).toBe(runner);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
156
spec/frontend/runner/components/runner_jobs_spec.js
Normal file
156
spec/frontend/runner/components/runner_jobs_spec.js
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import { GlSkeletonLoading } from '@gitlab/ui';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueApollo from 'vue-apollo';
|
||||||
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
|
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||||
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
|
import { createAlert } from '~/flash';
|
||||||
|
import RunnerJobs from '~/runner/components/runner_jobs.vue';
|
||||||
|
import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
|
||||||
|
import RunnerPagination from '~/runner/components/runner_pagination.vue';
|
||||||
|
import { captureException } from '~/runner/sentry_utils';
|
||||||
|
import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
|
||||||
|
|
||||||
|
import getRunnerJobsQuery from '~/runner/graphql/get_runner_jobs.query.graphql';
|
||||||
|
|
||||||
|
import { runnerData, runnerJobsData } from '../mock_data';
|
||||||
|
|
||||||
|
jest.mock('~/flash');
|
||||||
|
jest.mock('~/runner/sentry_utils');
|
||||||
|
|
||||||
|
const mockRunner = runnerData.data.runner;
|
||||||
|
const mockRunnerWithJobs = runnerJobsData.data.runner;
|
||||||
|
const mockJobs = mockRunnerWithJobs.jobs.nodes;
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
|
describe('RunnerJobs', () => {
|
||||||
|
let wrapper;
|
||||||
|
let mockRunnerJobsQuery;
|
||||||
|
|
||||||
|
const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
|
||||||
|
const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
|
||||||
|
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
|
||||||
|
|
||||||
|
const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
|
||||||
|
wrapper = mountFn(RunnerJobs, {
|
||||||
|
apolloProvider: createMockApollo([[getRunnerJobsQuery, mockRunnerJobsQuery]]),
|
||||||
|
propsData: {
|
||||||
|
runner: mockRunner,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockRunnerJobsQuery = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockRunnerJobsQuery.mockReset();
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Requests runner jobs', async () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockRunnerJobsQuery).toHaveBeenCalledWith({
|
||||||
|
id: mockRunner.id,
|
||||||
|
first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When there are jobs assigned', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockRunnerJobsQuery.mockResolvedValueOnce(runnerJobsData);
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shows jobs', () => {
|
||||||
|
const jobs = findRunnerJobsTable().props('jobs');
|
||||||
|
|
||||||
|
expect(jobs).toHaveLength(mockJobs.length);
|
||||||
|
expect(jobs[0]).toMatchObject(mockJobs[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When "Next" page is clicked', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
findRunnerPagination().vm.$emit('input', { page: 2, after: 'AFTER_CURSOR' });
|
||||||
|
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('A new page is requested', () => {
|
||||||
|
expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockRunnerJobsQuery).toHaveBeenLastCalledWith({
|
||||||
|
id: mockRunner.id,
|
||||||
|
first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
|
||||||
|
after: 'AFTER_CURSOR',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When loading', () => {
|
||||||
|
it('shows loading indicator and no other content', () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findGlSkeletonLoading().exists()).toBe(true);
|
||||||
|
expect(findRunnerJobsTable().exists()).toBe(false);
|
||||||
|
expect(findRunnerPagination().attributes('disabled')).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When there are no jobs', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockRunnerJobsQuery.mockResolvedValueOnce({
|
||||||
|
data: {
|
||||||
|
runner: {
|
||||||
|
id: mockRunner.id,
|
||||||
|
projectCount: 0,
|
||||||
|
jobs: {
|
||||||
|
nodes: [],
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
startCursor: '',
|
||||||
|
endCursor: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Shows a "None" label', () => {
|
||||||
|
expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When an error occurs', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockRunnerJobsQuery.mockRejectedValue(new Error('Error!'));
|
||||||
|
|
||||||
|
createComponent();
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error', () => {
|
||||||
|
expect(createAlert).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports an error', () => {
|
||||||
|
expect(captureException).toHaveBeenCalledWith({
|
||||||
|
component: 'RunnerJobs',
|
||||||
|
error: expect.any(Error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
119
spec/frontend/runner/components/runner_jobs_table_spec.js
Normal file
119
spec/frontend/runner/components/runner_jobs_table_spec.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import { GlTableLite } from '@gitlab/ui';
|
||||||
|
import {
|
||||||
|
extendedWrapper,
|
||||||
|
shallowMountExtended,
|
||||||
|
mountExtended,
|
||||||
|
} from 'helpers/vue_test_utils_helper';
|
||||||
|
import { __, s__ } from '~/locale';
|
||||||
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
|
import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
|
||||||
|
import { useFakeDate } from 'helpers/fake_date';
|
||||||
|
import { runnerJobsData } from '../mock_data';
|
||||||
|
|
||||||
|
const mockJobs = runnerJobsData.data.runner.jobs.nodes;
|
||||||
|
|
||||||
|
describe('RunnerJobsTable', () => {
|
||||||
|
let wrapper;
|
||||||
|
const mockNow = '2021-01-15T12:00:00Z';
|
||||||
|
const mockOneHourAgo = '2021-01-15T11:00:00Z';
|
||||||
|
|
||||||
|
useFakeDate(mockNow);
|
||||||
|
|
||||||
|
const findTable = () => wrapper.findComponent(GlTableLite);
|
||||||
|
const findHeaders = () => wrapper.findAll('th');
|
||||||
|
const findRows = () => wrapper.findAll('[data-testid^="job-row-"]');
|
||||||
|
const findCell = ({ field }) =>
|
||||||
|
extendedWrapper(findRows().at(0).find(`[data-testid="td-${field}"]`));
|
||||||
|
|
||||||
|
const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => {
|
||||||
|
wrapper = mountFn(RunnerJobsTable, {
|
||||||
|
propsData: {
|
||||||
|
jobs: mockJobs,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
GlTableLite,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Sets job id as a row key', () => {
|
||||||
|
createComponent();
|
||||||
|
|
||||||
|
expect(findTable().attributes('primarykey')).toBe('id');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Table data', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createComponent({}, mountExtended);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Displays headers', () => {
|
||||||
|
const headerLabels = findHeaders().wrappers.map((w) => w.text());
|
||||||
|
|
||||||
|
expect(headerLabels).toEqual([
|
||||||
|
s__('Job|Status'),
|
||||||
|
__('Job'),
|
||||||
|
__('Project'),
|
||||||
|
__('Commit'),
|
||||||
|
s__('Job|Finished at'),
|
||||||
|
s__('Runners|Tags'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Displays a list of jobs', () => {
|
||||||
|
expect(findRows()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Displays details of a job', () => {
|
||||||
|
const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
|
||||||
|
|
||||||
|
expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
|
||||||
|
|
||||||
|
expect(findCell({ field: 'job' }).text()).toContain(`#${getIdFromGraphQLId(id)}`);
|
||||||
|
expect(findCell({ field: 'job' }).find('a').attributes('href')).toBe(
|
||||||
|
detailedStatus.detailsPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
|
||||||
|
expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
|
||||||
|
pipeline.project.webUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
|
||||||
|
expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Table data formatting', () => {
|
||||||
|
let mockJobsCopy;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockJobsCopy = [
|
||||||
|
{
|
||||||
|
...mockJobs[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Formats finishedAt time', () => {
|
||||||
|
mockJobsCopy[0].finishedAt = mockOneHourAgo;
|
||||||
|
|
||||||
|
createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
|
||||||
|
|
||||||
|
expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Formats tags', () => {
|
||||||
|
mockJobsCopy[0].tags = ['tag-1', 'tag-2'];
|
||||||
|
|
||||||
|
createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
|
||||||
|
|
||||||
|
expect(findCell({ field: 'tags' }).text()).toMatchInterpolatedText('tag-1 tag-2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -123,6 +123,7 @@ describe('RunnerUpdateForm', () => {
|
||||||
|
|
||||||
// Some read-only fields are not submitted
|
// Some read-only fields are not submitted
|
||||||
const {
|
const {
|
||||||
|
__typename,
|
||||||
ipAddress,
|
ipAddress,
|
||||||
runnerType,
|
runnerType,
|
||||||
createdAt,
|
createdAt,
|
||||||
|
@ -132,7 +133,7 @@ describe('RunnerUpdateForm', () => {
|
||||||
userPermissions,
|
userPermissions,
|
||||||
version,
|
version,
|
||||||
groups,
|
groups,
|
||||||
__typename,
|
jobCount,
|
||||||
...submitted
|
...submitted
|
||||||
} = mockRunner;
|
} = mockRunner;
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query
|
||||||
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
|
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
|
||||||
import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
|
import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
|
||||||
import runnerProjectsData from 'test_fixtures/graphql/runner/get_runner_projects.query.graphql.json';
|
import runnerProjectsData from 'test_fixtures/graphql/runner/get_runner_projects.query.graphql.json';
|
||||||
|
import runnerJobsData from 'test_fixtures/graphql/runner/get_runner_jobs.query.graphql.json';
|
||||||
|
|
||||||
// Group queries
|
// Group queries
|
||||||
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
|
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
|
||||||
|
@ -20,6 +21,7 @@ export {
|
||||||
runnerData,
|
runnerData,
|
||||||
runnerWithGroupData,
|
runnerWithGroupData,
|
||||||
runnerProjectsData,
|
runnerProjectsData,
|
||||||
|
runnerJobsData,
|
||||||
groupRunnersData,
|
groupRunnersData,
|
||||||
groupRunnersCountData,
|
groupRunnersCountData,
|
||||||
groupRunnersDataPaginated,
|
groupRunnersDataPaginated,
|
||||||
|
|
|
@ -322,4 +322,102 @@ RSpec.describe Gitlab::Ci::Lint do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'pipeline logger' do
|
||||||
|
let(:counters) do
|
||||||
|
{
|
||||||
|
'count' => a_kind_of(Numeric),
|
||||||
|
'avg' => a_kind_of(Numeric),
|
||||||
|
'max' => a_kind_of(Numeric),
|
||||||
|
'min' => a_kind_of(Numeric)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:loggable_data) do
|
||||||
|
{
|
||||||
|
'class' => 'Gitlab::Ci::Pipeline::Logger',
|
||||||
|
'config_build_context_duration_s' => counters,
|
||||||
|
'config_build_variables_duration_s' => counters,
|
||||||
|
'config_compose_duration_s' => counters,
|
||||||
|
'config_expand_duration_s' => counters,
|
||||||
|
'config_external_process_duration_s' => counters,
|
||||||
|
'config_stages_inject_duration_s' => counters,
|
||||||
|
'config_tags_resolve_duration_s' => counters,
|
||||||
|
'config_yaml_extend_duration_s' => counters,
|
||||||
|
'config_yaml_load_duration_s' => counters,
|
||||||
|
'pipeline_creation_caller' => 'Gitlab::Ci::Lint',
|
||||||
|
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
|
||||||
|
'pipeline_persisted' => false,
|
||||||
|
'pipeline_source' => 'unknown',
|
||||||
|
'project_id' => project&.id,
|
||||||
|
'yaml_process_duration_s' => counters
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:content) do
|
||||||
|
<<~YAML
|
||||||
|
build:
|
||||||
|
script: echo
|
||||||
|
YAML
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:validate) { lint.validate(content, dry_run: false) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project&.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the duration is under the threshold' do
|
||||||
|
it 'does not create a log entry' do
|
||||||
|
expect(Gitlab::AppJsonLogger).not_to receive(:info)
|
||||||
|
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the durations exceeds the threshold' do
|
||||||
|
let(:timer) do
|
||||||
|
proc do
|
||||||
|
@timer = @timer.to_i + 30
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Gitlab::Ci::Pipeline::Logger)
|
||||||
|
.to receive(:current_monotonic_time) { timer.call }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a log entry' do
|
||||||
|
expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data)
|
||||||
|
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the feature flag is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(ci_pipeline_creation_logger: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create a log entry' do
|
||||||
|
expect(Gitlab::AppJsonLogger).not_to receive(:info)
|
||||||
|
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project is not provided' do
|
||||||
|
let(:project) { nil }
|
||||||
|
|
||||||
|
let(:project_nil_loggable_data) do
|
||||||
|
loggable_data.except('project_id')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a log entry without project_id' do
|
||||||
|
expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data)
|
||||||
|
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -203,6 +203,35 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
|
||||||
expect(commit).to be_truthy
|
expect(commit).to be_truthy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when project is not passed and pipeline is not persisted' do
|
||||||
|
let(:project) {}
|
||||||
|
let(:pipeline) { build(:ci_pipeline) }
|
||||||
|
|
||||||
|
let(:loggable_data) do
|
||||||
|
{
|
||||||
|
'class' => described_class.name.to_s,
|
||||||
|
'pipeline_persisted' => false,
|
||||||
|
'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
|
||||||
|
'pipeline_creation_caller' => 'source',
|
||||||
|
'pipeline_save_duration_s' => {
|
||||||
|
'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60
|
||||||
|
},
|
||||||
|
'pipeline_creation_duration_s' => {
|
||||||
|
'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'logs to application.json' do
|
||||||
|
expect(Gitlab::AppJsonLogger)
|
||||||
|
.to receive(:info)
|
||||||
|
.with(a_hash_including(loggable_data))
|
||||||
|
.and_call_original
|
||||||
|
|
||||||
|
expect(commit).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the feature flag is disabled' do
|
context 'when the feature flag is disabled' do
|
||||||
|
|
|
@ -18,6 +18,15 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
|
||||||
))
|
))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'ensure keys are sorted' do
|
||||||
|
it 'does not have any keys that are out of order' do
|
||||||
|
parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
|
||||||
|
mapping = parsed.children.first
|
||||||
|
table_names = mapping.children.select(&:scalar?).map(&:value)
|
||||||
|
expect(table_names).to eq(table_names.sort), "expected sorted table names in the YAML file"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'ensure no duplicates are found' do
|
context 'ensure no duplicates are found' do
|
||||||
it 'does not have duplicate tables defined' do
|
it 'does not have duplicate tables defined' do
|
||||||
# since we use hash to detect duplicate hash keys we need to parse YAML document
|
# since we use hash to detect duplicate hash keys we need to parse YAML document
|
||||||
|
|
|
@ -104,6 +104,34 @@ RSpec.describe Gitlab::Database do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.check_for_non_superuser' do
|
||||||
|
subject { described_class.check_for_non_superuser }
|
||||||
|
|
||||||
|
let(:non_superuser) { Gitlab::Database::PgUser.new(usename: 'foo', usesuper: false ) }
|
||||||
|
let(:superuser) { Gitlab::Database::PgUser.new(usename: 'bar', usesuper: true) }
|
||||||
|
|
||||||
|
it 'prints user details if not superuser' do
|
||||||
|
allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(non_superuser)
|
||||||
|
|
||||||
|
expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"foo\", UseSuper: (false)")
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an exception if superuser' do
|
||||||
|
allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(superuser)
|
||||||
|
|
||||||
|
expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"bar\", UseSuper: (true)")
|
||||||
|
expect { subject }.to raise_error('Error: detected superuser')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'catches exception if find_by fails' do
|
||||||
|
allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_raise(ActiveRecord::StatementInvalid)
|
||||||
|
|
||||||
|
expect { subject }.to raise_error('User CURRENT_USER not found')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.check_postgres_version_and_print_warning' do
|
describe '.check_postgres_version_and_print_warning' do
|
||||||
let(:reflect) { instance_spy(Gitlab::Database::Reflection) }
|
let(:reflect) { instance_spy(Gitlab::Database::Reflection) }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require_migration!('fix_approval_rules_code_owners_rule_type_index')
|
||||||
|
|
||||||
|
RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration do
|
||||||
|
let(:table_name) { :approval_merge_request_rules }
|
||||||
|
let(:index_name) { 'index_approval_rules_code_owners_rule_type' }
|
||||||
|
|
||||||
|
it 'correctly migrates up and down' do
|
||||||
|
reversible_migration do |migration|
|
||||||
|
migration.before -> {
|
||||||
|
expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
|
||||||
|
}
|
||||||
|
|
||||||
|
migration.after -> {
|
||||||
|
expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the index already exists' do
|
||||||
|
before do
|
||||||
|
subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps the index' do
|
||||||
|
migrate!
|
||||||
|
|
||||||
|
expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,32 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
require_migration!
|
||||||
|
|
||||||
|
RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile do
|
||||||
|
let(:namespaces) { table(:namespaces) }
|
||||||
|
let(:projects) { table(:projects) }
|
||||||
|
let(:dast_sites) { table(:dast_sites) }
|
||||||
|
let(:dast_site_profiles) { table(:dast_site_profiles) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
namespace = namespaces.create!(name: 'test', path: 'test')
|
||||||
|
project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab')
|
||||||
|
dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id)
|
||||||
|
|
||||||
|
dast_site_profiles.create!(id: 1, project_id: project.id, dast_site_id: dast_site.id,
|
||||||
|
name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0",
|
||||||
|
scan_method: 0, target_type: 0)
|
||||||
|
|
||||||
|
dast_site_profiles.create!(id: 2, project_id: project.id, dast_site_id: dast_site.id,
|
||||||
|
name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1",
|
||||||
|
scan_method: 0, target_type: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the scan_method to 1 for profiles with target_type 1' do
|
||||||
|
migrate!
|
||||||
|
|
||||||
|
expect(dast_site_profiles.where(scan_method: 1).count).to eq 1
|
||||||
|
expect(dast_site_profiles.where(scan_method: 0).count).to eq 1
|
||||||
|
end
|
||||||
|
end
|
|
@ -446,6 +446,44 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'gitlab:db:reset_as_non_superuser' do
|
||||||
|
let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool ) }
|
||||||
|
let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
|
||||||
|
let(:configurations) { double(ActiveRecord::DatabaseConfigurations) }
|
||||||
|
let(:configuration) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig) }
|
||||||
|
let(:config_hash) { { username: 'foo' } }
|
||||||
|
|
||||||
|
it 'migrate as nonsuperuser check with default username' do
|
||||||
|
allow(Rake::Task['db:drop']).to receive(:invoke)
|
||||||
|
allow(Rake::Task['db:create']).to receive(:invoke)
|
||||||
|
allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
|
||||||
|
allow(configurations).to receive(:configs_for).and_return([configuration])
|
||||||
|
allow(configuration).to receive(:configuration_hash).and_return(config_hash)
|
||||||
|
allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
|
||||||
|
|
||||||
|
expect(config_hash).to receive(:merge).with({ username: 'gitlab' })
|
||||||
|
expect(Gitlab::Database).to receive(:check_for_non_superuser)
|
||||||
|
expect(Rake::Task['db:migrate']).to receive(:invoke)
|
||||||
|
|
||||||
|
run_rake_task('gitlab:db:reset_as_non_superuser')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'migrate as nonsuperuser check with specified username' do
|
||||||
|
allow(Rake::Task['db:drop']).to receive(:invoke)
|
||||||
|
allow(Rake::Task['db:create']).to receive(:invoke)
|
||||||
|
allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
|
||||||
|
allow(configurations).to receive(:configs_for).and_return([configuration])
|
||||||
|
allow(configuration).to receive(:configuration_hash).and_return(config_hash)
|
||||||
|
allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
|
||||||
|
|
||||||
|
expect(config_hash).to receive(:merge).with({ username: 'foo' })
|
||||||
|
expect(Gitlab::Database).to receive(:check_for_non_superuser)
|
||||||
|
expect(Rake::Task['db:migrate']).to receive(:invoke)
|
||||||
|
|
||||||
|
run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def run_rake_task(task_name, arguments = '')
|
def run_rake_task(task_name, arguments = '')
|
||||||
Rake::Task[task_name].reenable
|
Rake::Task[task_name].reenable
|
||||||
Rake.application.invoke_task("#{task_name}#{arguments}")
|
Rake.application.invoke_task("#{task_name}#{arguments}")
|
||||||
|
|
Loading…
Reference in a new issue