Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a8476fe0cd
commit
83d921d51b
62 changed files with 773 additions and 479 deletions
2
Gemfile
2
Gemfile
|
@ -345,7 +345,7 @@ end
|
|||
|
||||
group :development do
|
||||
gem 'lefthook', '~> 0.7.0', require: false
|
||||
gem 'solargraph', '~> 0.40.4', require: false
|
||||
gem 'solargraph', '~> 0.42', require: false
|
||||
|
||||
gem 'letter_opener_web', '~> 1.4.0'
|
||||
|
||||
|
|
28
Gemfile.lock
28
Gemfile.lock
|
@ -137,7 +137,7 @@ GEM
|
|||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1.11.0.rc2)
|
||||
babosa (1.0.4)
|
||||
backport (1.1.2)
|
||||
backport (1.2.0)
|
||||
base32 (0.3.2)
|
||||
batch-loader (2.0.1)
|
||||
bcrypt (3.1.16)
|
||||
|
@ -352,13 +352,24 @@ GEM
|
|||
factory_bot_rails (6.1.0)
|
||||
factory_bot (~> 6.1.0)
|
||||
railties (>= 5.0.0)
|
||||
faraday (1.0.1)
|
||||
faraday (1.4.2)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-http-cache (2.2.0)
|
||||
faraday (>= 0.8)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http_persistent (1.1.0)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday_middleware-aws-sigv4 (0.3.0)
|
||||
|
@ -1135,7 +1146,7 @@ GEM
|
|||
nokogiri (>= 1.10.5)
|
||||
rexml
|
||||
ruby-statistics (2.1.2)
|
||||
ruby2_keywords (0.0.2)
|
||||
ruby2_keywords (0.0.4)
|
||||
ruby_parser (3.15.0)
|
||||
sexp_processor (~> 4.9)
|
||||
rubyntlm (0.6.2)
|
||||
|
@ -1205,10 +1216,11 @@ GEM
|
|||
slack-messenger (2.3.4)
|
||||
snowplow-tracker (0.6.1)
|
||||
contracts (~> 0.7, <= 0.11)
|
||||
solargraph (0.40.4)
|
||||
backport (~> 1.1)
|
||||
solargraph (0.42.3)
|
||||
backport (~> 1.2)
|
||||
benchmark
|
||||
bundler (>= 1.17.2)
|
||||
diff-lcs (~> 1.4)
|
||||
e2mmap
|
||||
jaro_winkler (~> 1.5)
|
||||
kramdown (~> 2.3)
|
||||
|
@ -1260,8 +1272,8 @@ GEM
|
|||
terser (1.0.2)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
test-prof (0.12.0)
|
||||
test_file_finder (0.1.3)
|
||||
faraday (~> 1.0.1)
|
||||
test_file_finder (0.1.4)
|
||||
faraday (~> 1.0)
|
||||
text (1.3.1)
|
||||
thin (1.8.0)
|
||||
daemons (~> 1.0, >= 1.0.9)
|
||||
|
@ -1632,7 +1644,7 @@ DEPENDENCIES
|
|||
simplecov-cobertura (~> 1.3.1)
|
||||
slack-messenger (~> 2.3.4)
|
||||
snowplow-tracker (~> 0.6.1)
|
||||
solargraph (~> 0.40.4)
|
||||
solargraph (~> 0.42)
|
||||
spamcheck (~> 0.1.0)
|
||||
spring (~> 2.1.0)
|
||||
spring-commands-rspec (~> 1.0.4)
|
||||
|
|
|
@ -9,18 +9,29 @@ export default {
|
|||
inject: ['timeTrackingLimitToHours'],
|
||||
computed: {
|
||||
...mapGetters(['activeBoardItem']),
|
||||
initialTimeTracking() {
|
||||
const {
|
||||
timeEstimate,
|
||||
totalTimeSpent,
|
||||
humanTimeEstimate,
|
||||
humanTotalTimeSpent,
|
||||
} = this.activeBoardItem;
|
||||
return {
|
||||
timeEstimate,
|
||||
totalTimeSpent,
|
||||
humanTimeEstimate,
|
||||
humanTotalTimeSpent,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<issuable-time-tracker
|
||||
:issuable-id="activeBoardItem.id.toString()"
|
||||
:time-estimate="activeBoardItem.timeEstimate"
|
||||
:time-spent="activeBoardItem.totalTimeSpent"
|
||||
:human-time-estimate="activeBoardItem.humanTimeEstimate"
|
||||
:human-time-spent="activeBoardItem.humanTotalTimeSpent"
|
||||
:issuable-iid="activeBoardItem.iid.toString()"
|
||||
:limit-to-hours="timeTrackingLimitToHours"
|
||||
:initial-time-tracking="initialTimeTracking"
|
||||
:show-collapsed="false"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -2,5 +2,6 @@ export default class IssueProject {
|
|||
constructor(obj) {
|
||||
this.id = obj.id;
|
||||
this.path = obj.path;
|
||||
this.fullPath = obj.path_with_namespace;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
|
||||
import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
|
||||
import { escape } from 'lodash';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
@ -30,7 +30,6 @@ export default {
|
|||
GlModal,
|
||||
GlButton,
|
||||
GlFormInput,
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
clusterPath: {
|
||||
|
@ -135,17 +134,6 @@ export default {
|
|||
<div v-if="confirmCleanup">
|
||||
{{ s__('ClusterIntegration|This will permanently delete the following resources:') }}
|
||||
<ul>
|
||||
<li>
|
||||
{{ s__('ClusterIntegration|All installed applications and related resources') }}
|
||||
</li>
|
||||
<li>
|
||||
<gl-sprintf :message="s__('ClusterIntegration|The %{gitlabNamespace} namespace')">
|
||||
<template #gitlabNamespace>
|
||||
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
|
||||
<code>{{ 'gitlab-managed-apps' }}</code>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</li>
|
||||
<li>{{ s__('ClusterIntegration|Any project namespaces') }}</li>
|
||||
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
|
||||
<li><code>clusterroles</code></li>
|
||||
|
|
|
@ -53,8 +53,8 @@ export default {
|
|||
<div
|
||||
class="diff-stats"
|
||||
:class="{
|
||||
'is-compare-versions-header d-none d-lg-inline-flex': isCompareVersionsHeader,
|
||||
'd-none d-sm-inline-flex': !isCompareVersionsHeader,
|
||||
'is-compare-versions-header gl-display-none gl-lg-display-inline-flex': isCompareVersionsHeader,
|
||||
'gl-display-none gl-sm-display-inline-flex': !isCompareVersionsHeader,
|
||||
}"
|
||||
>
|
||||
<div v-if="notDiffable" :class="fileStats.classes">
|
||||
|
@ -66,18 +66,18 @@ export default {
|
|||
<span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="diff-stats-group cgreen d-flex align-items-center"
|
||||
class="diff-stats-group gl-text-green-600 gl-display-flex gl-align-items-center"
|
||||
:class="{ bold: isCompareVersionsHeader }"
|
||||
>
|
||||
<span>+</span>
|
||||
<span class="js-file-addition-line">{{ addedLines }}</span>
|
||||
<span data-testid="js-file-addition-line">{{ addedLines }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="diff-stats-group cred d-flex align-items-center"
|
||||
class="diff-stats-group gl-text-red-500 gl-display-flex gl-align-items-center"
|
||||
:class="{ bold: isCompareVersionsHeader }"
|
||||
>
|
||||
<span>-</span>
|
||||
<span class="js-file-deletion-line">{{ removedLines }}</span>
|
||||
<span data-testid="js-file-deletion-line">{{ removedLines }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -105,9 +105,9 @@ export function stats(file) {
|
|||
valid = true;
|
||||
|
||||
if (diff > 0) {
|
||||
classes = 'cgreen';
|
||||
classes = 'gl-text-green-600';
|
||||
} else if (diff < 0) {
|
||||
classes = 'cred';
|
||||
classes = 'gl-text-red-500';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ import { intersection } from 'lodash';
|
|||
import '~/smart_interval';
|
||||
|
||||
import eventHub from '../../event_hub';
|
||||
import Mediator from '../../sidebar_mediator';
|
||||
import Store from '../../stores/sidebar_store';
|
||||
import IssuableTimeTracker from './time_tracker.vue';
|
||||
|
||||
export default {
|
||||
|
@ -14,16 +12,20 @@ export default {
|
|||
IssuableTimeTracker,
|
||||
},
|
||||
props: {
|
||||
issuableId: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
issuableIid: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
mediator: new Mediator(),
|
||||
store: new Store(),
|
||||
};
|
||||
limitToHours: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.listenForQuickActions();
|
||||
|
@ -47,7 +49,7 @@ export default {
|
|||
changedCommands = [];
|
||||
}
|
||||
if (changedCommands && intersection(subscribedCommands, changedCommands).length) {
|
||||
this.mediator.fetch();
|
||||
eventHub.$emit('timeTracker:refresh');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -57,12 +59,9 @@ export default {
|
|||
<template>
|
||||
<div class="block">
|
||||
<issuable-time-tracker
|
||||
:issuable-id="issuableId"
|
||||
:time-estimate="store.timeEstimate"
|
||||
:time-spent="store.totalTimeSpent"
|
||||
:human-time-estimate="store.humanTimeEstimate"
|
||||
:human-time-spent="store.humanTotalTimeSpent"
|
||||
:limit-to-hours="store.timeTrackingLimitToHours"
|
||||
:full-path="fullPath"
|
||||
:issuable-iid="issuableIid"
|
||||
:limit-to-hours="limitToHours"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
<script>
|
||||
import { GlIcon, GlLink, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import { GlIcon, GlLink, GlModal, GlModalDirective, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { timeTrackingQueries } from '~/sidebar/constants';
|
||||
|
||||
import eventHub from '../../event_hub';
|
||||
import TimeTrackingCollapsedState from './collapsed_state.vue';
|
||||
import TimeTrackingComparisonPane from './comparison_pane.vue';
|
||||
|
@ -19,6 +21,7 @@ export default {
|
|||
GlIcon,
|
||||
GlLink,
|
||||
GlModal,
|
||||
GlLoadingIcon,
|
||||
TimeTrackingCollapsedState,
|
||||
TimeTrackingSpentOnlyPane,
|
||||
TimeTrackingComparisonPane,
|
||||
|
@ -30,34 +33,26 @@ export default {
|
|||
},
|
||||
inject: ['issuableType'],
|
||||
props: {
|
||||
timeEstimate: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
timeSpent: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
humanTimeEstimate: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
humanTimeSpent: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
limitToHours: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
issuableId: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
issuableIid: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
initialTimeTracking: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
/*
|
||||
In issue list, "time-tracking-collapsed-state" is always rendered even if the sidebar isn't collapsed.
|
||||
The actual hiding is controlled with css classes:
|
||||
|
@ -77,26 +72,73 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
showHelp: false,
|
||||
timeTracking: {
|
||||
...this.initialTimeTracking,
|
||||
},
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
issuableTimeTracking: {
|
||||
query() {
|
||||
return timeTrackingQueries[this.issuableType].query;
|
||||
},
|
||||
skip() {
|
||||
// We don't fetch info via GraphQL in following cases
|
||||
// 1. Time tracking info was provided via prop
|
||||
// 2. issuableIid and fullPath are not provided.
|
||||
if (!this.initialTimeTracking) {
|
||||
return false;
|
||||
} else if (this.issuableIid && this.fullPath) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
iid: this.issuableIid,
|
||||
fullPath: this.fullPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
this.timeTracking = {
|
||||
...data.workspace?.issuable,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasTimeSpent() {
|
||||
return Boolean(this.timeSpent);
|
||||
isTimeTrackingInfoLoading() {
|
||||
return this.$apollo?.queries.issuableTimeTracking.loading ?? false;
|
||||
},
|
||||
timeEstimate() {
|
||||
return this.timeTracking?.timeEstimate || 0;
|
||||
},
|
||||
totalTimeSpent() {
|
||||
return this.timeTracking?.totalTimeSpent || 0;
|
||||
},
|
||||
humanTimeEstimate() {
|
||||
return this.timeTracking?.humanTimeEstimate || '';
|
||||
},
|
||||
humanTotalTimeSpent() {
|
||||
return this.timeTracking?.humanTotalTimeSpent || '';
|
||||
},
|
||||
hasTotalTimeSpent() {
|
||||
return Boolean(this.totalTimeSpent);
|
||||
},
|
||||
hasTimeEstimate() {
|
||||
return Boolean(this.timeEstimate);
|
||||
},
|
||||
showComparisonState() {
|
||||
return this.hasTimeEstimate && this.hasTimeSpent;
|
||||
return this.hasTimeEstimate && this.hasTotalTimeSpent;
|
||||
},
|
||||
showEstimateOnlyState() {
|
||||
return this.hasTimeEstimate && !this.hasTimeSpent;
|
||||
return this.hasTimeEstimate && !this.hasTotalTimeSpent;
|
||||
},
|
||||
showSpentOnlyState() {
|
||||
return this.hasTimeSpent && !this.hasTimeEstimate;
|
||||
return this.hasTotalTimeSpent && !this.hasTimeEstimate;
|
||||
},
|
||||
showNoTimeTrackingState() {
|
||||
return !this.hasTimeEstimate && !this.hasTimeSpent;
|
||||
return !this.hasTimeEstimate && !this.hasTotalTimeSpent;
|
||||
},
|
||||
showHelpState() {
|
||||
return Boolean(this.showHelp);
|
||||
|
@ -104,26 +146,29 @@ export default {
|
|||
isTimeReportSupported() {
|
||||
return (
|
||||
[IssuableType.Issue, IssuableType.MergeRequest].includes(this.issuableType) &&
|
||||
this.issuableId
|
||||
this.issuableIid
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When `initialTimeTracking` is provided via prop,
|
||||
* we don't query the same via GraphQl and instead
|
||||
* monitor it for any updates (eg; Epic Swimlanes)
|
||||
*/
|
||||
initialTimeTracking(timeTracking) {
|
||||
this.timeTracking = timeTracking;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
eventHub.$on('timeTracker:updateData', this.update);
|
||||
eventHub.$on('timeTracker:refresh', this.refresh);
|
||||
},
|
||||
methods: {
|
||||
toggleHelpState(show) {
|
||||
this.showHelp = show;
|
||||
},
|
||||
update(data) {
|
||||
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data;
|
||||
|
||||
/* eslint-disable vue/no-mutating-props */
|
||||
this.timeEstimate = timeEstimate;
|
||||
this.timeSpent = timeSpent;
|
||||
this.humanTimeEstimate = humanTimeEstimate;
|
||||
this.humanTimeSpent = humanTimeSpent;
|
||||
/* eslint-enable vue/no-mutating-props */
|
||||
refresh() {
|
||||
this.$apollo.queries.issuableTimeTracking.refetch();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -138,11 +183,12 @@ export default {
|
|||
:show-help-state="showHelpState"
|
||||
:show-spent-only-state="showSpentOnlyState"
|
||||
:show-estimate-only-state="showEstimateOnlyState"
|
||||
:time-spent-human-readable="humanTimeSpent"
|
||||
:time-spent-human-readable="humanTotalTimeSpent"
|
||||
:time-estimate-human-readable="humanTimeEstimate"
|
||||
/>
|
||||
<div class="hide-collapsed gl-line-height-20 gl-text-gray-900">
|
||||
{{ __('Time tracking') }}
|
||||
<gl-loading-icon v-if="isTimeTrackingInfoLoading" inline />
|
||||
<div
|
||||
v-if="!showHelpState"
|
||||
data-testid="helpButton"
|
||||
|
@ -160,14 +206,14 @@ export default {
|
|||
<gl-icon name="close" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hide-collapsed">
|
||||
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
|
||||
<div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane">
|
||||
<span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span
|
||||
>{{ humanTimeEstimate }}
|
||||
</div>
|
||||
<time-tracking-spent-only-pane
|
||||
v-if="showSpentOnlyState"
|
||||
:time-spent-human-readable="humanTimeSpent"
|
||||
:time-spent-human-readable="humanTotalTimeSpent"
|
||||
/>
|
||||
<div v-if="showNoTimeTrackingState" data-testid="noTrackingPane">
|
||||
<span class="gl-text-gray-500">{{ $options.i18n.noTimeTrackingText }}</span>
|
||||
|
@ -175,14 +221,14 @@ export default {
|
|||
<time-tracking-comparison-pane
|
||||
v-if="showComparisonState"
|
||||
:time-estimate="timeEstimate"
|
||||
:time-spent="timeSpent"
|
||||
:time-spent-human-readable="humanTimeSpent"
|
||||
:time-spent="totalTimeSpent"
|
||||
:time-spent-human-readable="humanTotalTimeSpent"
|
||||
:time-estimate-human-readable="humanTimeEstimate"
|
||||
:limit-to-hours="limitToHours"
|
||||
/>
|
||||
<template v-if="isTimeReportSupported">
|
||||
<gl-link
|
||||
v-if="hasTimeSpent"
|
||||
v-if="hasTotalTimeSpent"
|
||||
v-gl-modal="'time-tracking-report'"
|
||||
data-testid="reportLink"
|
||||
href="#"
|
||||
|
@ -194,7 +240,7 @@ export default {
|
|||
:title="__('Time tracking report')"
|
||||
:hide-footer="true"
|
||||
>
|
||||
<time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" />
|
||||
<time-tracking-report :limit-to-hours="limitToHours" :issuable-iid="issuableIid" />
|
||||
</gl-modal>
|
||||
</template>
|
||||
<transition name="help-state-toggle">
|
||||
|
|
|
@ -9,8 +9,10 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g
|
|||
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
|
||||
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
|
||||
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
|
||||
import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
|
||||
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
|
||||
import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
|
||||
import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
|
||||
import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
|
||||
import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
|
||||
import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
|
||||
|
@ -120,6 +122,15 @@ export const subscribedQueries = {
|
|||
},
|
||||
};
|
||||
|
||||
export const timeTrackingQueries = {
|
||||
[IssuableType.Issue]: {
|
||||
query: issueTimeTrackingQuery,
|
||||
},
|
||||
[IssuableType.MergeRequest]: {
|
||||
query: mergeRequestTimeTrackingQuery,
|
||||
},
|
||||
};
|
||||
|
||||
export const dueDateQueries = {
|
||||
[IssuableType.Issue]: {
|
||||
query: issueDueDateQuery,
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class SidebarMilestone {
|
|||
humanTimeEstimate,
|
||||
humanTimeSpent,
|
||||
limitToHours,
|
||||
id,
|
||||
iid,
|
||||
} = el.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
|
@ -30,12 +30,14 @@ export default class SidebarMilestone {
|
|||
render: (createElement) =>
|
||||
createElement('timeTracker', {
|
||||
props: {
|
||||
timeEstimate: parseInt(timeEstimate, 10),
|
||||
timeSpent: parseInt(timeSpent, 10),
|
||||
humanTimeEstimate,
|
||||
humanTimeSpent,
|
||||
limitToHours: parseBoolean(limitToHours),
|
||||
issuableId: id.toString(),
|
||||
issuableIid: iid.toString(),
|
||||
initialTimeTracking: {
|
||||
timeEstimate: parseInt(timeEstimate, 10),
|
||||
totalTimeSpent: parseInt(timeSpent, 10),
|
||||
humanTimeEstimate,
|
||||
humanTotalTimeSpent: humanTimeSpent,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -391,7 +391,7 @@ function mountSubscriptionsComponent() {
|
|||
|
||||
function mountTimeTrackingComponent() {
|
||||
const el = document.getElementById('issuable-time-tracker');
|
||||
const { id, issuableType } = getSidebarOptions();
|
||||
const { iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
|
||||
|
||||
if (!el) return;
|
||||
|
||||
|
@ -403,7 +403,9 @@ function mountTimeTrackingComponent() {
|
|||
render: (createElement) =>
|
||||
createElement(SidebarTimeTracking, {
|
||||
props: {
|
||||
issuableId: id.toString(),
|
||||
fullPath,
|
||||
issuableIid: iid.toString(),
|
||||
limitToHours: timeTrackingLimitToHours,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
query issueTimeTracking($fullPath: ID!, $iid: String) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: issue(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
humanTimeEstimate
|
||||
humanTotalTimeSpent
|
||||
timeEstimate
|
||||
totalTimeSpent
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
query mergeRequestTimeTracking($fullPath: ID!, $iid: String!) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
__typename
|
||||
issuable: mergeRequest(iid: $iid) {
|
||||
__typename
|
||||
id
|
||||
humanTimeEstimate
|
||||
humanTotalTimeSpent
|
||||
timeEstimate
|
||||
totalTimeSpent
|
||||
}
|
||||
}
|
||||
}
|
|
@ -51,9 +51,8 @@ class GitlabSchema < GraphQL::Schema
|
|||
end
|
||||
|
||||
def get_type(type_name)
|
||||
# This is a backwards compatibility hack to work around an accidentally
|
||||
# released argument typed as EEIterationID
|
||||
type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID')
|
||||
type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name)
|
||||
|
||||
super(type_name)
|
||||
end
|
||||
|
||||
|
@ -162,10 +161,9 @@ class GitlabSchema < GraphQL::Schema
|
|||
end
|
||||
end
|
||||
|
||||
# This is a backwards compatibility hack to work around an accidentally
|
||||
# released argument typed as EE{Type}ID
|
||||
def get_type(type_name)
|
||||
type_name = type_name.gsub(/^EE/, '') if type_name.end_with?('ID')
|
||||
type_name = Gitlab::GlobalId::Deprecations.apply_to_graphql_name(type_name)
|
||||
|
||||
super(type_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,11 +52,20 @@ module Types
|
|||
@id_types ||= {}
|
||||
|
||||
@id_types[model_class] ||= Class.new(self) do
|
||||
graphql_name "#{model_class.name.gsub(/::/, '')}ID"
|
||||
description <<~MD
|
||||
model_name = model_class.name
|
||||
|
||||
graphql_name model_name_to_graphql_name(model_name)
|
||||
description <<~MD.strip
|
||||
A `#{graphql_name}` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `#{graphql_name}` is: `"#{::Gitlab::GlobalId.build(model_name: model_class.name, id: 1)}"`.
|
||||
An example `#{graphql_name}` is: `"#{::Gitlab::GlobalId.build(model_name: model_name, id: 1)}"`.
|
||||
#{
|
||||
if deprecation = Gitlab::GlobalId::Deprecations.deprecation_by(model_name)
|
||||
'The older format `"' +
|
||||
::Gitlab::GlobalId.build(model_name: deprecation.old_model_name, id: 1).to_s +
|
||||
'"` was deprecated in ' + deprecation.milestone + '.'
|
||||
end}
|
||||
|
||||
MD
|
||||
|
||||
define_singleton_method(:to_s) do
|
||||
|
@ -69,7 +78,7 @@ module Types
|
|||
|
||||
define_singleton_method(:as) do |new_name|
|
||||
if @renamed && graphql_name != new_name
|
||||
raise "Conflicting names for ID of #{model_class.name}: " \
|
||||
raise "Conflicting names for ID of #{model_name}: " \
|
||||
"#{graphql_name} and #{new_name}"
|
||||
end
|
||||
|
||||
|
@ -79,11 +88,11 @@ module Types
|
|||
end
|
||||
|
||||
define_singleton_method(:coerce_result) do |gid, ctx|
|
||||
global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_class.name)
|
||||
global_id = ::Gitlab::GlobalId.as_global_id(gid, model_name: model_name)
|
||||
|
||||
next global_id.to_s if suitable?(global_id)
|
||||
|
||||
raise GraphQL::CoercionError, "Expected a #{model_class.name} ID, got #{global_id}"
|
||||
raise GraphQL::CoercionError, "Expected a #{model_name} ID, got #{global_id}"
|
||||
end
|
||||
|
||||
define_singleton_method(:suitable?) do |gid|
|
||||
|
@ -97,9 +106,13 @@ module Types
|
|||
gid = super(string, ctx)
|
||||
next gid if suitable?(gid)
|
||||
|
||||
raise GraphQL::CoercionError, "#{string.inspect} does not represent an instance of #{model_class.name}"
|
||||
raise GraphQL::CoercionError, "#{string.inspect} does not represent an instance of #{model_name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.model_name_to_graphql_name(model_name)
|
||||
"#{model_name.gsub(/::/, '')}ID"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -168,18 +168,16 @@ module Clusters
|
|||
|
||||
state_machine :cleanup_status, initial: :cleanup_not_started do
|
||||
state :cleanup_not_started, value: 1
|
||||
state :cleanup_uninstalling_applications, value: 2
|
||||
state :cleanup_removing_project_namespaces, value: 3
|
||||
state :cleanup_removing_service_account, value: 4
|
||||
state :cleanup_errored, value: 5
|
||||
|
||||
event :start_cleanup do |cluster|
|
||||
transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications
|
||||
transition [:cleanup_not_started, :cleanup_errored] => :cleanup_removing_project_namespaces
|
||||
end
|
||||
|
||||
event :continue_cleanup do
|
||||
transition(
|
||||
cleanup_uninstalling_applications: :cleanup_removing_project_namespaces,
|
||||
cleanup_removing_project_namespaces: :cleanup_removing_service_account)
|
||||
end
|
||||
|
||||
|
@ -192,13 +190,7 @@ module Clusters
|
|||
cluster.cleanup_status_reason = status_reason if status_reason
|
||||
end
|
||||
|
||||
after_transition [:cleanup_not_started, :cleanup_errored] => :cleanup_uninstalling_applications do |cluster|
|
||||
cluster.run_after_commit do
|
||||
Clusters::Cleanup::AppWorker.perform_async(cluster.id)
|
||||
end
|
||||
end
|
||||
|
||||
after_transition cleanup_uninstalling_applications: :cleanup_removing_project_namespaces do |cluster|
|
||||
after_transition [:cleanup_not_started, :cleanup_errored] => :cleanup_removing_project_namespaces do |cluster|
|
||||
cluster.run_after_commit do
|
||||
Clusters::Cleanup::ProjectNamespaceWorker.perform_async(cluster.id)
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ class IssueBoardEntity < Grape::Entity
|
|||
end
|
||||
|
||||
expose :project do |issue|
|
||||
API::Entities::Project.represent issue.project, only: [:id, :path]
|
||||
API::Entities::Project.represent issue.project, only: [:id, :path, :path_with_namespace]
|
||||
end
|
||||
|
||||
expose :milestone, if: -> (issue) { issue.milestone } do |issue|
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Cleanup
|
||||
class AppService < Clusters::Cleanup::BaseService
|
||||
def execute
|
||||
persisted_applications = @cluster.persisted_applications
|
||||
|
||||
persisted_applications.each do |app|
|
||||
next unless app.available?
|
||||
next unless app.can_uninstall?
|
||||
|
||||
log_event(:uninstalling_app, application: app.class.application_name)
|
||||
uninstall_app_async(app)
|
||||
end
|
||||
|
||||
# Keep calling the worker untill all dependencies are uninstalled
|
||||
return schedule_next_execution(Clusters::Cleanup::AppWorker) if persisted_applications.any?
|
||||
|
||||
log_event(:schedule_remove_project_namespaces)
|
||||
cluster.continue_cleanup!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def uninstall_app_async(application)
|
||||
application.make_scheduled!
|
||||
|
||||
Clusters::Applications::UninstallWorker.perform_async(application.name, application.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Clusters
|
||||
module Cleanup
|
||||
class ProjectNamespaceService < BaseService
|
||||
class ProjectNamespaceService < ::Clusters::Cleanup::BaseService
|
||||
KUBERNETES_NAMESPACE_BATCH_SIZE = 100
|
||||
|
||||
def execute
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Clusters
|
||||
module Cleanup
|
||||
class ServiceAccountService < BaseService
|
||||
class ServiceAccountService < ::Clusters::Cleanup::BaseService
|
||||
def execute
|
||||
delete_gitlab_service_account
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
.block.time-tracking
|
||||
%time-tracker{ ":time-estimate" => "issue.timeEstimate || 0",
|
||||
":time-spent" => "issue.timeSpent || 0",
|
||||
":human-time-estimate" => "issue.humanTimeEstimate",
|
||||
":human-time-spent" => "issue.humanTimeSpent",
|
||||
":limit-to-hours" => "timeTrackingLimitToHours",
|
||||
":issuable-id" => "issue.id ? issue.id.toString() : ''",
|
||||
%time-tracker{ ":limit-to-hours" => "timeTrackingLimitToHours",
|
||||
":issuable-iid" => "issue.iid ? issue.iid.toString() : ''",
|
||||
":full-path" => "issue.project ? issue.project.fullPath : ''",
|
||||
"root-path" => "#{root_url}" }
|
||||
|
|
|
@ -98,7 +98,7 @@
|
|||
time_spent: @milestone.total_time_spent,
|
||||
human_time_estimate: @milestone.human_total_time_estimate,
|
||||
human_time_spent: @milestone.human_total_time_spent,
|
||||
id: @milestone.id,
|
||||
iid: @milestone.iid,
|
||||
limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } }
|
||||
|
||||
= render_if_exists 'shared/milestones/weight', milestone: milestone
|
||||
|
|
|
@ -808,15 +808,6 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: gcp_cluster:clusters_cleanup_app
|
||||
:worker_name: Clusters::Cleanup::AppWorker
|
||||
:feature_category: :kubernetes_management
|
||||
:has_external_dependencies: true
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: gcp_cluster:clusters_cleanup_project_namespace
|
||||
:worker_name: Clusters::Cleanup::ProjectNamespaceWorker
|
||||
:feature_category: :kubernetes_management
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Clusters
|
||||
module Cleanup
|
||||
class AppWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ClusterCleanupMethods
|
||||
|
||||
def perform(cluster_id, execution_count = 0)
|
||||
Clusters::Cluster.with_persisted_applications.find_by_id(cluster_id).try do |cluster|
|
||||
break unless cluster.cleanup_uninstalling_applications?
|
||||
|
||||
break exceeded_execution_limit(cluster) if exceeded_execution_limit?(execution_count)
|
||||
|
||||
::Clusters::Cleanup::AppService.new(cluster, execution_count).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,6 +4,6 @@ if Gitlab::Utils.to_boolean(ENV['ENABLE_ACTIVERECORD_EMPTY_PING'], default: true
|
|||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(Gitlab::Database::PostgresqlAdapter::EmptyQueryPing)
|
||||
end
|
||||
|
||||
if Gitlab::Utils.to_boolean(ENV['ENABLE_ACTIVERECORD_TYPEMAP_CACHE'], default: false)
|
||||
if Gitlab::Utils.to_boolean(ENV['ENABLE_ACTIVERECORD_TYPEMAP_CACHE'], default: true)
|
||||
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(Gitlab::Database::PostgresqlAdapter::TypeMapCache)
|
||||
end
|
||||
|
|
3
config/initializers/global_id.rb
Normal file
3
config/initializers/global_id.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
GlobalID.prepend(Gitlab::Patch::GlobalID)
|
|
@ -8,18 +8,10 @@ class AddIndexForCadenceIterationsAutomation < ActiveRecord::Migration[6.0]
|
|||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return if index_exists_by_name?(:iterations_cadences, INDEX_NAME)
|
||||
|
||||
execute(
|
||||
<<-SQL
|
||||
CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON iterations_cadences
|
||||
USING BTREE(automatic, duration_in_weeks, (DATE ((COALESCE("iterations_cadences"."last_run_date", DATE('01-01-1970')) + "iterations_cadences"."duration_in_weeks" * INTERVAL '1 week'))))
|
||||
WHERE duration_in_weeks IS NOT NULL
|
||||
SQL
|
||||
)
|
||||
# no-op
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :iterations_cadences, INDEX_NAME
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ class EnableEnforceSshKeyExpiration < ActiveRecord::Migration[6.0]
|
|||
def up
|
||||
ApplicationSetting.reset_column_information
|
||||
|
||||
ApplicationSetting.where.not(enforce_ssh_key_expiration: false).each do |application_setting|
|
||||
ApplicationSetting.where.not(enforce_ssh_key_expiration: true).each do |application_setting|
|
||||
application_setting.update!(enforce_ssh_key_expiration: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RebuildIndexForCadenceIterationsAutomation < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
INDEX_NAME = 'cadence_create_iterations_automation'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return if index_exists_and_is_valid?
|
||||
|
||||
remove_concurrent_index_by_name :iterations_cadences, INDEX_NAME
|
||||
|
||||
disable_statement_timeout do
|
||||
execute(
|
||||
<<-SQL
|
||||
CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON iterations_cadences
|
||||
USING BTREE(automatic, duration_in_weeks, (DATE ((COALESCE("iterations_cadences"."last_run_date", DATE('01-01-1970')) + "iterations_cadences"."duration_in_weeks" * INTERVAL '1 week'))))
|
||||
WHERE duration_in_weeks IS NOT NULL
|
||||
SQL
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :iterations_cadences, INDEX_NAME
|
||||
end
|
||||
|
||||
def index_exists_and_is_valid?
|
||||
execute(
|
||||
<<-SQL
|
||||
SELECT identifier
|
||||
FROM postgres_indexes
|
||||
WHERE identifier LIKE '%#{INDEX_NAME}' AND valid_index=TRUE
|
||||
SQL
|
||||
).any?
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210611100359
Normal file
1
db/schema_migrations/20210611100359
Normal file
|
@ -0,0 +1 @@
|
|||
9429a8adca0bc85167f64e76d8d72b45d09d4303a01bd9c4ca39560bb4d89799
|
|
@ -15378,6 +15378,7 @@ An example `IssueID` is: `"gid://gitlab/Issue/1"`.
|
|||
A `IterationID` is a global ID. It is encoded as a string.
|
||||
|
||||
An example `IterationID` is: `"gid://gitlab/Iteration/1"`.
|
||||
The older format `"gid://gitlab/EEIteration/1"` was deprecated in 13.3.
|
||||
|
||||
### `IterationsCadenceID`
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ Complementary reads:
|
|||
- [Security process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#security-releases-critical-non-critical-as-a-developer)
|
||||
- [Guidelines for implementing Enterprise Edition features](ee_features.md)
|
||||
- [Danger bot](dangerbot.md)
|
||||
- [Generate a changelog entry with `bin/changelog`](changelog.md)
|
||||
- [Guidelines for changelogs](changelog.md)
|
||||
- [Requesting access to ChatOps on GitLab.com](chatops_on_gitlabcom.md#requesting-access) (for GitLab team members)
|
||||
- [Patch release process for developers](https://gitlab.com/gitlab-org/release/docs/blob/master/general/patch/process.md#process-for-developers)
|
||||
- [Adding a new service component to GitLab](adding_service_component.md)
|
||||
|
|
|
@ -46,7 +46,7 @@ request is as follows:
|
|||
your personal namespace (or group) on GitLab.com.
|
||||
1. Create a feature branch in your fork (don't work off `master`).
|
||||
1. Write [tests](../rake_tasks.md#run-tests) and code.
|
||||
1. [Generate a changelog entry with `bin/changelog`](../changelog.md)
|
||||
1. [Ensure a changelog is created](../changelog.md).
|
||||
1. If you are writing documentation, make sure to follow the
|
||||
[documentation guidelines](../documentation/index.md).
|
||||
1. Follow the [commit messages guidelines](#commit-messages-guidelines).
|
||||
|
|
|
@ -58,7 +58,8 @@ The DevOps Adoption tab shows you which groups within your organization are usin
|
|||
- Pipelines
|
||||
- Deployments
|
||||
|
||||
Buttons to manage your groups appear in the DevOps Adoption section of the page.
|
||||
When managing groups in the UI, you can add your groups with the **Add group to table**
|
||||
button, in the top right hand section the page.
|
||||
|
||||
DevOps Adoption allows you to:
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 52 KiB |
Binary file not shown.
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 52 KiB |
|
@ -36,7 +36,7 @@ Group DevOps Adoption shows you how individual groups and sub-groups within your
|
|||
- Pipelines
|
||||
- Deployments
|
||||
|
||||
When managing groups in the UI, you can manage your sub-groups with the **Add/Remove sub-groups**
|
||||
When managing groups in the UI, you can add your sub-groups with the **Add sub-group to table**
|
||||
button, in the top right hand section of your Groups pages.
|
||||
|
||||
With DevOps Adoption you can:
|
||||
|
|
|
@ -147,7 +147,7 @@ module Gitlab
|
|||
is required for this version of GitLab.
|
||||
<% if Rails.env.development? || Rails.env.test? %>
|
||||
If using gitlab-development-kit, please find the relevant steps here:
|
||||
https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/postgresql.md#upgrade-postgresql
|
||||
https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/postgresql.md#upgrade-postgresql
|
||||
<% end %>
|
||||
Please upgrade your environment to a supported PostgreSQL version, see
|
||||
https://docs.gitlab.com/ee/install/requirements.html#database for details.
|
||||
|
|
47
lib/gitlab/global_id/deprecations.rb
Normal file
47
lib/gitlab/global_id/deprecations.rb
Normal file
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module GlobalId
|
||||
module Deprecations
|
||||
Deprecation = Struct.new(:old_model_name, :new_model_name, :milestone, keyword_init: true)
|
||||
|
||||
# Contains the deprecations in place.
|
||||
# Example:
|
||||
#
|
||||
# DEPRECATIONS = [
|
||||
# Deprecation.new(old_model_name: 'PrometheusService', new_model_name: 'Integrations::Prometheus', milestone: '14.0')
|
||||
# ].freeze
|
||||
DEPRECATIONS = [
|
||||
# This works around an accidentally released argument named as `"EEIterationID"` in 7000489db.
|
||||
Deprecation.new(old_model_name: 'EEIteration', new_model_name: 'Iteration', milestone: '13.3')
|
||||
].freeze
|
||||
|
||||
# Maps of the DEPRECATIONS Hash for quick access.
|
||||
OLD_NAME_MAP = DEPRECATIONS.index_by(&:old_model_name).freeze
|
||||
NEW_NAME_MAP = DEPRECATIONS.index_by(&:new_model_name).freeze
|
||||
OLD_GRAPHQL_NAME_MAP = DEPRECATIONS.index_by do |d|
|
||||
Types::GlobalIDType.model_name_to_graphql_name(d.old_model_name)
|
||||
end.freeze
|
||||
|
||||
def self.deprecated?(old_model_name)
|
||||
OLD_NAME_MAP.key?(old_model_name)
|
||||
end
|
||||
|
||||
def self.deprecation_for(old_model_name)
|
||||
OLD_NAME_MAP[old_model_name]
|
||||
end
|
||||
|
||||
def self.deprecation_by(new_model_name)
|
||||
NEW_NAME_MAP[new_model_name]
|
||||
end
|
||||
|
||||
# Returns the new `graphql_name` (Type#graphql_name) of a deprecated GID,
|
||||
# or the `graphql_name` argument given if no deprecation applies.
|
||||
def self.apply_to_graphql_name(graphql_name)
|
||||
return graphql_name unless deprecation = OLD_GRAPHQL_NAME_MAP[graphql_name]
|
||||
|
||||
Types::GlobalIDType.model_name_to_graphql_name(deprecation.new_model_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
25
lib/gitlab/patch/global_id.rb
Normal file
25
lib/gitlab/patch/global_id.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# To support GlobalID arguments that present a model with its old "deprecated" name
|
||||
# we alter GlobalID so it will correctly find the record with its new model name.
|
||||
module Gitlab
|
||||
module Patch
|
||||
module GlobalID
|
||||
def initialize(gid, options = {})
|
||||
super
|
||||
|
||||
if deprecation = Gitlab::GlobalId::Deprecations.deprecation_for(model_name)
|
||||
@new_model_name = deprecation.new_model_name
|
||||
end
|
||||
end
|
||||
|
||||
def model_name
|
||||
new_model_name || super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :new_model_name
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6979,9 +6979,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|All data will be deleted and cannot be restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|All installed applications and related resources"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{linkStart}More information%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7687,9 +7684,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Subnets"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|The %{gitlabNamespace} namespace"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11261,6 +11255,18 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Add a group to get started"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Add group"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Add group to table"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Add sub-group"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Add sub-group to table"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Add/remove groups"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11336,6 +11342,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|No filter results."
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|No results…"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Not adopted"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11375,6 +11384,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|There was an error fetching Groups. Please refresh the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|This group has no sub-groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|You cannot remove the group you are currently in."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "1.199.0",
|
||||
"@gitlab/tributejs": "1.0.0",
|
||||
"@gitlab/ui": "29.34.1",
|
||||
"@gitlab/ui": "29.35.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "6.1.3-2",
|
||||
"@rails/ujs": "6.1.3-2",
|
||||
|
|
|
@ -138,10 +138,6 @@ FactoryBot.define do
|
|||
cleanup_status { 1 }
|
||||
end
|
||||
|
||||
trait :cleanup_uninstalling_applications do
|
||||
cleanup_status { 2 }
|
||||
end
|
||||
|
||||
trait :cleanup_removing_project_namespaces do
|
||||
cleanup_status { 3 }
|
||||
end
|
||||
|
|
|
@ -129,8 +129,8 @@ RSpec.describe 'Merge request > User sees versions', :js do
|
|||
)
|
||||
expect(page).to have_content '4 files'
|
||||
|
||||
additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
|
||||
deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
|
||||
additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-addition-line"]').text
|
||||
deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-deletion-line"]').text
|
||||
|
||||
expect(additions_content).to eq '15'
|
||||
expect(deletions_content).to eq '6'
|
||||
|
@ -152,8 +152,8 @@ RSpec.describe 'Merge request > User sees versions', :js do
|
|||
end
|
||||
|
||||
it 'show diff between new and old version' do
|
||||
additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
|
||||
deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
|
||||
additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-addition-line"]').text
|
||||
deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group [data-testid="js-file-deletion-line"]').text
|
||||
|
||||
expect(page).to have_content '4 files'
|
||||
expect(additions_content).to eq '15'
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"path": { "type": "string" }
|
||||
"path": { "type": "string" },
|
||||
"path_with_namespace": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"milestone": {
|
||||
|
|
|
@ -26,7 +26,7 @@ describe('BoardSidebarTimeTracker', () => {
|
|||
store = createStore();
|
||||
store.state.boardItems = {
|
||||
1: {
|
||||
id: 1,
|
||||
iid: 1,
|
||||
timeEstimate: 3600,
|
||||
totalTimeSpent: 1800,
|
||||
humanTimeEstimate: '1h',
|
||||
|
@ -47,13 +47,16 @@ describe('BoardSidebarTimeTracker', () => {
|
|||
createComponent({ provide: { timeTrackingLimitToHours } });
|
||||
|
||||
expect(wrapper.find(IssuableTimeTracker).props()).toEqual({
|
||||
timeEstimate: 3600,
|
||||
timeSpent: 1800,
|
||||
humanTimeEstimate: '1h',
|
||||
humanTimeSpent: '30min',
|
||||
limitToHours: timeTrackingLimitToHours,
|
||||
showCollapsed: false,
|
||||
issuableId: '1',
|
||||
issuableIid: '1',
|
||||
fullPath: '',
|
||||
initialTimeTracking: {
|
||||
timeEstimate: 3600,
|
||||
totalTimeSpent: 1800,
|
||||
humanTimeEstimate: '1h',
|
||||
humanTotalTimeSpent: '30min',
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import DiffStats from '~/diffs/components/diff_stats.vue';
|
||||
import mockDiffFile from '../mock_data/diff_file';
|
||||
|
||||
|
@ -12,13 +14,15 @@ describe('diff_stats', () => {
|
|||
let wrapper;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(DiffStats, {
|
||||
propsData: {
|
||||
addedLines: TEST_ADDED_LINES,
|
||||
removedLines: TEST_REMOVED_LINES,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(DiffStats, {
|
||||
propsData: {
|
||||
addedLines: TEST_ADDED_LINES,
|
||||
removedLines: TEST_REMOVED_LINES,
|
||||
...props,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
describe('diff stats group', () => {
|
||||
|
@ -58,24 +62,24 @@ describe('diff_stats', () => {
|
|||
it("renders the bytes changes instead of line changes when the file isn't diffable", () => {
|
||||
const content = getBytesContainer();
|
||||
|
||||
expect(content.classes('cgreen')).toBe(true);
|
||||
expect(content.classes('gl-text-green-600')).toBe(true);
|
||||
expect(content.text()).toBe('+1.00 KiB (+100%)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('line changes', () => {
|
||||
const findFileLine = (name) => wrapper.find(name);
|
||||
const findFileLine = (name) => wrapper.findByTestId(name);
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('shows the amount of lines added', () => {
|
||||
expect(findFileLine('.js-file-addition-line').text()).toBe(TEST_ADDED_LINES.toString());
|
||||
expect(findFileLine('js-file-addition-line').text()).toBe(TEST_ADDED_LINES.toString());
|
||||
});
|
||||
|
||||
it('shows the amount of lines removed', () => {
|
||||
expect(findFileLine('.js-file-deletion-line').text()).toBe(TEST_REMOVED_LINES.toString());
|
||||
expect(findFileLine('js-file-deletion-line').text()).toBe(TEST_REMOVED_LINES.toString());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -181,7 +181,7 @@ describe('diff_file utilities', () => {
|
|||
{
|
||||
changed: 1024,
|
||||
percent: 100,
|
||||
classes: 'cgreen',
|
||||
classes: 'gl-text-green-600',
|
||||
sign: '+',
|
||||
text: '+1.00 KiB (+100%)',
|
||||
valid: true,
|
||||
|
@ -197,7 +197,7 @@ describe('diff_file utilities', () => {
|
|||
{
|
||||
changed: -1024,
|
||||
percent: -100,
|
||||
classes: 'cred',
|
||||
classes: 'gl-text-red-500',
|
||||
sign: '',
|
||||
text: '-1.00 KiB (-100%)',
|
||||
valid: true,
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
|
||||
import { stubTransition } from 'helpers/stub_transition';
|
||||
import { createMockDirective } from 'helpers/vue_mock_directive';
|
||||
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
|
||||
import SidebarEventHub from '~/sidebar/event_hub';
|
||||
|
||||
import { issuableTimeTrackingResponse } from '../../mock_data';
|
||||
|
||||
describe('Issuable Time Tracker', () => {
|
||||
let wrapper;
|
||||
|
@ -13,16 +17,18 @@ describe('Issuable Time Tracker', () => {
|
|||
const findReportLink = () => findByTestId('reportLink');
|
||||
|
||||
const defaultProps = {
|
||||
timeEstimate: 10_000, // 2h 46m
|
||||
timeSpent: 5_000, // 1h 23m
|
||||
humanTimeEstimate: '2h 46m',
|
||||
humanTimeSpent: '1h 23m',
|
||||
limitToHours: false,
|
||||
issuableId: '1',
|
||||
fullPath: 'gitlab-org/gitlab-test',
|
||||
issuableIid: '1',
|
||||
initialTimeTracking: {
|
||||
...issuableTimeTrackingResponse.data.workspace.issuable,
|
||||
},
|
||||
};
|
||||
|
||||
const mountComponent = ({ props = {}, issuableType = 'issue' } = {}) =>
|
||||
mount(TimeTracker, {
|
||||
const issuableTimeTrackingRefetchSpy = jest.fn();
|
||||
|
||||
const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
|
||||
return mount(TimeTracker, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
directives: { GlTooltip: createMockDirective() },
|
||||
stubs: {
|
||||
|
@ -31,7 +37,19 @@ describe('Issuable Time Tracker', () => {
|
|||
provide: {
|
||||
issuableType,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
issuableTimeTracking: {
|
||||
loading,
|
||||
refetch: issuableTimeTrackingRefetchSpy,
|
||||
query: jest.fn().mockResolvedValue(issuableTimeTrackingResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -48,13 +66,13 @@ describe('Issuable Time Tracker', () => {
|
|||
|
||||
it('should correctly render timeEstimate', () => {
|
||||
expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
|
||||
defaultProps.humanTimeEstimate,
|
||||
defaultProps.initialTimeTracking.humanTimeEstimate,
|
||||
);
|
||||
});
|
||||
|
||||
it('should correctly render time_spent', () => {
|
||||
it('should correctly render totalTimeSpent', () => {
|
||||
expect(findByTestId('timeTrackingComparisonPane').html()).toContain(
|
||||
defaultProps.humanTimeSpent,
|
||||
defaultProps.initialTimeTracking.humanTotalTimeSpent,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -82,10 +100,12 @@ describe('Issuable Time Tracker', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeEstimate: 100_000, // 1d 3h
|
||||
timeSpent: 5_000, // 1h 23m
|
||||
humanTimeEstimate: '1d 3h',
|
||||
humanTimeSpent: '1h 23m',
|
||||
initialTimeTracking: {
|
||||
timeEstimate: 100_000, // 1d 3h
|
||||
totalTimeSpent: 5_000, // 1h 23m
|
||||
humanTimeEstimate: '1d 3h',
|
||||
humanTotalTimeSpent: '1h 23m',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -112,8 +132,11 @@ describe('Issuable Time Tracker', () => {
|
|||
it('should display the remaining meter with the correct background color when over estimate', () => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeEstimate: 10_000, // 2h 46m
|
||||
timeSpent: 20_000_000, // 231 days
|
||||
initialTimeTracking: {
|
||||
...defaultProps.initialTimeTracking,
|
||||
timeEstimate: 10_000, // 2h 46m
|
||||
totalTimeSpent: 20_000_000, // 231 days
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -126,8 +149,11 @@ describe('Issuable Time Tracker', () => {
|
|||
beforeEach(async () => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeEstimate: 100_000, // 1d 3h
|
||||
limitToHours: true,
|
||||
initialTimeTracking: {
|
||||
...defaultProps.initialTimeTracking,
|
||||
timeEstimate: 100_000, // 1d 3h
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -144,10 +170,12 @@ describe('Issuable Time Tracker', () => {
|
|||
beforeEach(async () => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeEstimate: 10_000, // 2h 46m
|
||||
timeSpent: 0,
|
||||
timeEstimateHumanReadable: '2h 46m',
|
||||
timeSpentHumanReadable: '',
|
||||
initialTimeTracking: {
|
||||
timeEstimate: 10_000, // 2h 46m
|
||||
totalTimeSpent: 0,
|
||||
humanTimeEstimate: '2h 46m',
|
||||
humanTotalTimeSpent: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
|
@ -163,10 +191,12 @@ describe('Issuable Time Tracker', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeEstimate: 0,
|
||||
timeSpent: 5_000, // 1h 23m
|
||||
timeEstimateHumanReadable: '2h 46m',
|
||||
timeSpentHumanReadable: '1h 23m',
|
||||
initialTimeTracking: {
|
||||
timeEstimate: 0,
|
||||
totalTimeSpent: 5_000, // 1h 23m
|
||||
humanTimeEstimate: '2h 46m',
|
||||
humanTotalTimeSpent: '1h 23m',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -181,10 +211,12 @@ describe('Issuable Time Tracker', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeEstimate: 0,
|
||||
timeSpent: 0,
|
||||
timeEstimateHumanReadable: '',
|
||||
timeSpentHumanReadable: '',
|
||||
initialTimeTracking: {
|
||||
timeEstimate: 0,
|
||||
totalTimeSpent: 0,
|
||||
humanTimeEstimate: '',
|
||||
humanTotalTimeSpent: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -202,8 +234,11 @@ describe('Issuable Time Tracker', () => {
|
|||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
timeSpent: 0,
|
||||
timeSpentHumanReadable: '',
|
||||
initialTimeTracking: {
|
||||
...defaultProps.initialTimeTracking,
|
||||
totalTimeSpent: 0,
|
||||
humanTotalTimeSpent: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -236,7 +271,16 @@ describe('Issuable Time Tracker', () => {
|
|||
const findCloseHelpButton = () => findByTestId('closeHelpButton');
|
||||
|
||||
beforeEach(async () => {
|
||||
wrapper = mountComponent({ props: { timeEstimate: 0, timeSpent: 0 } });
|
||||
wrapper = mountComponent({
|
||||
props: {
|
||||
initialTimeTracking: {
|
||||
timeEstimate: 0,
|
||||
totalTimeSpent: 0,
|
||||
humanTimeEstimate: '',
|
||||
humanTotalTimeSpent: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
|
@ -265,4 +309,14 @@ describe('Issuable Time Tracker', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event listeners', () => {
|
||||
it('refetches issuableTimeTracking query when eventHub emits `timeTracker:refresh` event', async () => {
|
||||
SidebarEventHub.$emit('timeTracker:refresh');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(issuableTimeTrackingRefetchSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -592,4 +592,21 @@ export const emptyProjectMilestonesResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const issuableTimeTrackingResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
__typename: 'Project',
|
||||
issuable: {
|
||||
__typename: 'Issue',
|
||||
id: 'gid://gitlab/Issue/1',
|
||||
title: 'Commodi incidunt eos eos libero dicta dolores sed.',
|
||||
timeEstimate: 10_000, // 2h 46m
|
||||
totalTimeSpent: 5_000, // 1h 23m
|
||||
humanTimeEstimate: '2h 46m',
|
||||
humanTotalTimeSpent: '1h 23m',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::GlobalIDType do
|
||||
include ::Gitlab::Graphql::Laziness
|
||||
include GraphqlHelpers
|
||||
include GlobalIDDeprecationHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
let(:gid) { project.to_global_id }
|
||||
|
@ -97,6 +101,142 @@ RSpec.describe Types::GlobalIDType do
|
|||
expect { type.coerce_isolated_input(invalid_gid) }
|
||||
.to raise_error(GraphQL::CoercionError, /does not represent an instance of Project/)
|
||||
end
|
||||
|
||||
context 'with a deprecation' do
|
||||
around(:all) do |example|
|
||||
# Unset all previously memoized GlobalIDTypes to allow us to define one
|
||||
# that will use the constants stubbed in the `before` block.
|
||||
previous_id_types = Types::GlobalIDType.instance_variable_get(:@id_types)
|
||||
Types::GlobalIDType.instance_variable_set(:@id_types, {})
|
||||
|
||||
example.run
|
||||
ensure
|
||||
Types::GlobalIDType.instance_variable_set(:@id_types, previous_id_types)
|
||||
end
|
||||
|
||||
before do
|
||||
deprecation = Gitlab::GlobalId::Deprecations::Deprecation.new(old_model_name: 'OldIssue', new_model_name: 'Issue', milestone: '10.0')
|
||||
|
||||
stub_global_id_deprecations(deprecation)
|
||||
end
|
||||
|
||||
let_it_be(:issue) { create(:issue) }
|
||||
|
||||
let!(:type) { ::Types::GlobalIDType[::Issue] }
|
||||
let(:deprecated_gid) { Gitlab::GlobalId.build(model_name: 'OldIssue', id: issue.id) }
|
||||
let(:deprecating_gid) { Gitlab::GlobalId.build(model_name: 'Issue', id: issue.id) }
|
||||
|
||||
it 'appends the description with a deprecation notice for the old Global ID' do
|
||||
expect(type.to_graphql.description).to include('The older format `"gid://gitlab/OldIssue/1"` was deprecated in 10.0')
|
||||
end
|
||||
|
||||
describe 'coercing input against the type (parsing the Global ID string when supplied as an argument)' do
|
||||
subject(:result) { type.coerce_isolated_input(gid.to_s) }
|
||||
|
||||
context 'when passed the deprecated Global ID' do
|
||||
let(:gid) { deprecated_gid }
|
||||
|
||||
it 'changes the model_name to the new model name' do
|
||||
expect(result.model_name).to eq('Issue')
|
||||
end
|
||||
|
||||
it 'changes the model_class to the new model class' do
|
||||
expect(result.model_class).to eq(Issue)
|
||||
end
|
||||
|
||||
it 'can find the correct resource' do
|
||||
expect(result.find).to eq(issue)
|
||||
end
|
||||
|
||||
it 'can find the correct resource loaded through GitlabSchema' do
|
||||
expect(force(GitlabSchema.object_from_id(result, expected_class: Issue))).to eq(issue)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passed the Global ID that is deprecating another' do
|
||||
let(:gid) { deprecating_gid }
|
||||
|
||||
it 'works as normal' do
|
||||
expect(result).to have_attributes(
|
||||
model_class: Issue,
|
||||
model_name: 'Issue',
|
||||
find: issue,
|
||||
to_s: gid.to_s
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'coercing the result against the type (producing the Global ID string when used in a field)' do
|
||||
context 'when passed the deprecated Global ID' do
|
||||
let(:gid) { deprecated_gid }
|
||||
|
||||
it 'works, but does not result in matching the new Global ID', :aggregate_failures do
|
||||
# Note, this would normally never happen in real life as the object being parsed
|
||||
# by the field would not produce the GlobalID of the deprecated model. This test
|
||||
# proves that it is technically possible for the deprecated GlobalID to be
|
||||
# considered parsable for the type, as opposed to raising a `GraphQL::CoercionError`.
|
||||
expect(type.coerce_isolated_result(gid)).not_to eq(issue.to_global_id.to_s)
|
||||
expect(type.coerce_isolated_result(gid)).to eq(gid.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passed the Global ID that is deprecating another' do
|
||||
let(:gid) { deprecating_gid }
|
||||
|
||||
it 'works as normal' do
|
||||
expect(type.coerce_isolated_result(gid)).to eq(issue.to_global_id.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'executing against the schema' do
|
||||
let(:query_result) do
|
||||
context = { current_user: issue.project.owner }
|
||||
variables = { 'id' => gid }
|
||||
|
||||
run_with_clean_state(query, context: context, variables: variables).to_h
|
||||
end
|
||||
|
||||
shared_examples 'a query that works with old and new GIDs' do
|
||||
let(:query) do
|
||||
<<-GQL
|
||||
query($id: #{argument_name}!) {
|
||||
issue(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
GQL
|
||||
end
|
||||
|
||||
subject { query_result.dig('data', 'issue', 'id') }
|
||||
|
||||
context 'when the argument value is the new GID' do
|
||||
let(:gid) { Gitlab::GlobalId.build(model_name: 'Issue', id: issue.id) }
|
||||
|
||||
it { is_expected.to be_present }
|
||||
end
|
||||
|
||||
context 'when the argument value is the old GID' do
|
||||
let(:gid) { Gitlab::GlobalId.build(model_name: 'OldIssue', id: issue.id) }
|
||||
|
||||
it { is_expected.to be_present }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the query signature includes the old type name' do
|
||||
let(:argument_name) { 'OldIssueID' }
|
||||
|
||||
it_behaves_like 'a query that works with old and new GIDs'
|
||||
end
|
||||
|
||||
context 'when the query signature includes the new type name' do
|
||||
let(:argument_name) { 'IssueID' }
|
||||
|
||||
it_behaves_like 'a query that works with old and new GIDs'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'a parameterized type with a namespace' do
|
||||
|
@ -231,4 +371,10 @@ RSpec.describe Types::GlobalIDType do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.model_name_to_graphql_name' do
|
||||
it 'returns a graphql name for the given model name' do
|
||||
expect(described_class.model_name_to_graphql_name('DesignManagement::Design')).to eq('DesignManagementDesignID')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
35
spec/initializers/global_id_spec.rb
Normal file
35
spec/initializers/global_id_spec.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'global_id' do
|
||||
it 'prepends `Gitlab::Patch::GlobalID`' do
|
||||
expect(GlobalID.ancestors).to include(Gitlab::Patch::GlobalID)
|
||||
end
|
||||
|
||||
it 'patches GlobalID to find aliased models when a deprecation exists' do
|
||||
allow(Gitlab::GlobalId::Deprecations).to receive(:deprecation_for).and_call_original
|
||||
allow(Gitlab::GlobalId::Deprecations).to receive(:deprecation_for).with('Issue').and_return(double(new_model_name: 'Project'))
|
||||
project = create(:project)
|
||||
gid_string = Gitlab::GlobalId.build(model_name: Issue.name, id: project.id).to_s
|
||||
|
||||
expect(GlobalID.new(gid_string)).to have_attributes(
|
||||
to_s: gid_string,
|
||||
model_name: 'Project',
|
||||
model_class: Project,
|
||||
find: project
|
||||
)
|
||||
end
|
||||
|
||||
it 'works as normal when no deprecation exists' do
|
||||
issue = create(:issue)
|
||||
gid_string = Gitlab::GlobalId.build(model_name: Issue.name, id: issue.id).to_s
|
||||
|
||||
expect(GlobalID.new(gid_string)).to have_attributes(
|
||||
to_s: gid_string,
|
||||
model_name: 'Issue',
|
||||
model_class: Issue,
|
||||
find: issue
|
||||
)
|
||||
end
|
||||
end
|
|
@ -4,20 +4,17 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
|
||||
let(:db_config) { ActiveRecord::Base.configurations.configs_for(env_name: 'test', name: 'primary').configuration_hash }
|
||||
let(:adapter_class) do
|
||||
Class.new(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
||||
let(:adapter_class) { ActiveRecord::ConnectionAdapters::PostgreSQLAdapter }
|
||||
|
||||
before do
|
||||
adapter_class.type_map_cache.clear
|
||||
end
|
||||
|
||||
describe '#initialize_type_map' do
|
||||
it 'caches loading of types in memory' do
|
||||
initialize_connection.disconnect!
|
||||
recorder_without_cache = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection.disconnect! }
|
||||
|
||||
expect(recorder_without_cache.log).to include(a_string_matching(/FROM pg_type/)).twice
|
||||
|
||||
adapter_class.prepend(described_class)
|
||||
|
||||
initialize_connection.disconnect!
|
||||
recorder_with_cache = ActiveRecord::QueryRecorder.new(skip_schema_queries: false) { initialize_connection.disconnect! }
|
||||
|
||||
expect(recorder_with_cache.count).to be < recorder_without_cache.count
|
||||
|
@ -29,8 +26,6 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
|
|||
end
|
||||
|
||||
it 'only reuses the cache if the connection parameters are exactly the same' do
|
||||
adapter_class.prepend(described_class)
|
||||
|
||||
initialize_connection.disconnect!
|
||||
|
||||
other_config = db_config.dup
|
||||
|
@ -44,8 +39,6 @@ RSpec.describe Gitlab::Database::PostgresqlAdapter::TypeMapCache do
|
|||
|
||||
describe '#reload_type_map' do
|
||||
it 'clears the cache and executes the type map query again' do
|
||||
adapter_class.prepend(described_class)
|
||||
|
||||
initialize_connection.disconnect!
|
||||
|
||||
connection = initialize_connection
|
||||
|
|
46
spec/lib/gitlab/global_id/deprecations_spec.rb
Normal file
46
spec/lib/gitlab/global_id/deprecations_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::GlobalId::Deprecations do
|
||||
include GlobalIDDeprecationHelpers
|
||||
|
||||
let_it_be(:deprecation_1) { described_class::Deprecation.new(old_model_name: 'Foo::Model', new_model_name: 'Bar', milestone: '9.0') }
|
||||
let_it_be(:deprecation_2) { described_class::Deprecation.new(old_model_name: 'Baz', new_model_name: 'Qux::Model', milestone: '10.0') }
|
||||
|
||||
before do
|
||||
stub_global_id_deprecations(deprecation_1, deprecation_2)
|
||||
end
|
||||
|
||||
describe '.deprecated?' do
|
||||
it 'returns a boolean to signal if model name has a deprecation', :aggregate_failures do
|
||||
expect(described_class.deprecated?('Foo::Model')).to eq(true)
|
||||
expect(described_class.deprecated?('Qux::Model')).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.deprecation_for' do
|
||||
it 'returns the deprecation for the model if it exists', :aggregate_failures do
|
||||
expect(described_class.deprecation_for('Foo::Model')).to eq(deprecation_1)
|
||||
expect(described_class.deprecation_for('Qux::Model')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.deprecation_by' do
|
||||
it 'returns the deprecation by the model if it exists', :aggregate_failures do
|
||||
expect(described_class.deprecation_by('Foo::Model')).to be_nil
|
||||
expect(described_class.deprecation_by('Qux::Model')).to eq(deprecation_2)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.apply_to_graphql_name' do
|
||||
it 'returns the corresponding graphql_name of the GID for the new model', :aggregate_failures do
|
||||
expect(described_class.apply_to_graphql_name('FooModelID')).to eq('BarID')
|
||||
expect(described_class.apply_to_graphql_name('BazID')).to eq('QuxModelID')
|
||||
end
|
||||
|
||||
it 'returns the same value if there is no deprecation' do
|
||||
expect(described_class.apply_to_graphql_name('ProjectID')).to eq('ProjectID')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1021,7 +1021,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
|
|||
|
||||
where(:status_name, :cleanup_status) do
|
||||
provider_status | :cleanup_not_started
|
||||
:cleanup_ongoing | :cleanup_uninstalling_applications
|
||||
:cleanup_ongoing | :cleanup_removing_project_namespaces
|
||||
:cleanup_ongoing | :cleanup_removing_service_account
|
||||
:cleanup_errored | :cleanup_errored
|
||||
|
@ -1077,8 +1076,8 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
|
||||
describe '#start_cleanup!' do
|
||||
let(:expected_worker_class) { Clusters::Cleanup::AppWorker }
|
||||
let(:to_state) { :cleanup_uninstalling_applications }
|
||||
let(:expected_worker_class) { Clusters::Cleanup::ProjectNamespaceWorker }
|
||||
let(:to_state) { :cleanup_removing_project_namespaces }
|
||||
|
||||
subject { cluster.start_cleanup! }
|
||||
|
||||
|
@ -1116,25 +1115,13 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
|
||||
describe '#continue_cleanup!' do
|
||||
context 'when cleanup_status is cleanup_uninstalling_applications' do
|
||||
let(:expected_worker_class) { Clusters::Cleanup::ProjectNamespaceWorker }
|
||||
let(:from_state) { :cleanup_uninstalling_applications }
|
||||
let(:to_state) { :cleanup_removing_project_namespaces }
|
||||
let(:expected_worker_class) { Clusters::Cleanup::ServiceAccountWorker }
|
||||
let(:from_state) { :cleanup_removing_project_namespaces }
|
||||
let(:to_state) { :cleanup_removing_service_account }
|
||||
|
||||
subject { cluster.continue_cleanup! }
|
||||
subject { cluster.continue_cleanup! }
|
||||
|
||||
it_behaves_like 'cleanup_status transition'
|
||||
end
|
||||
|
||||
context 'when cleanup_status is cleanup_removing_project_namespaces' do
|
||||
let(:expected_worker_class) { Clusters::Cleanup::ServiceAccountWorker }
|
||||
let(:from_state) { :cleanup_removing_project_namespaces }
|
||||
let(:to_state) { :cleanup_removing_service_account }
|
||||
|
||||
subject { cluster.continue_cleanup! }
|
||||
|
||||
it_behaves_like 'cleanup_status transition'
|
||||
end
|
||||
it_behaves_like 'cleanup_status transition'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe IssueBoardEntity do
|
|||
|
||||
it 'has basic attributes' do
|
||||
expect(subject).to include(:id, :iid, :title, :confidential, :due_date, :project_id, :relative_position,
|
||||
:labels, :assignees, project: hash_including(:id, :path))
|
||||
:labels, :assignees, project: hash_including(:id, :path, :path_with_namespace))
|
||||
end
|
||||
|
||||
it 'has path and endpoints' do
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Clusters::Cleanup::AppService do
|
||||
describe '#execute' do
|
||||
let!(:cluster) { create(:cluster, :project, :cleanup_uninstalling_applications, provider_type: :gcp) }
|
||||
let(:service) { described_class.new(cluster) }
|
||||
let(:logger) { service.send(:logger) }
|
||||
let(:log_meta) do
|
||||
{
|
||||
service: described_class.name,
|
||||
cluster_id: cluster.id,
|
||||
execution_count: 0
|
||||
}
|
||||
end
|
||||
|
||||
subject { service.execute }
|
||||
|
||||
shared_examples 'does not reschedule itself' do
|
||||
it 'does not reschedule itself' do
|
||||
expect(Clusters::Cleanup::AppWorker).not_to receive(:perform_in)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cluster has no applications available or transitioning applications' do
|
||||
it_behaves_like 'does not reschedule itself'
|
||||
|
||||
it 'transitions cluster to cleanup_removing_project_namespaces' do
|
||||
expect { subject }
|
||||
.to change { cluster.reload.cleanup_status_name }
|
||||
.from(:cleanup_uninstalling_applications)
|
||||
.to(:cleanup_removing_project_namespaces)
|
||||
end
|
||||
|
||||
it 'schedules Clusters::Cleanup::ProjectNamespaceWorker' do
|
||||
expect(Clusters::Cleanup::ProjectNamespaceWorker).to receive(:perform_async).with(cluster.id)
|
||||
subject
|
||||
end
|
||||
|
||||
it 'logs all events' do
|
||||
expect(logger).to receive(:info)
|
||||
.with(log_meta.merge(event: :schedule_remove_project_namespaces))
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'when cluster has uninstallable applications' do
|
||||
shared_examples 'reschedules itself' do
|
||||
it 'reschedules itself' do
|
||||
expect(Clusters::Cleanup::AppWorker)
|
||||
.to receive(:perform_in)
|
||||
.with(1.minute, cluster.id, 1)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
context 'has applications with dependencies' do
|
||||
let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
|
||||
let!(:ingress) { create(:clusters_applications_ingress, :installed, cluster: cluster) }
|
||||
let!(:cert_manager) { create(:clusters_applications_cert_manager, :installed, cluster: cluster) }
|
||||
let!(:jupyter) { create(:clusters_applications_jupyter, :installed, cluster: cluster) }
|
||||
|
||||
it_behaves_like 'reschedules itself'
|
||||
|
||||
it 'only uninstalls apps that are not dependencies for other installed apps' do
|
||||
expect(Clusters::Applications::UninstallWorker)
|
||||
.to receive(:perform_async).with(helm.name, helm.id)
|
||||
.and_call_original
|
||||
|
||||
expect(Clusters::Applications::UninstallWorker)
|
||||
.not_to receive(:perform_async).with(ingress.name, ingress.id)
|
||||
|
||||
expect(Clusters::Applications::UninstallWorker)
|
||||
.to receive(:perform_async).with(cert_manager.name, cert_manager.id)
|
||||
.and_call_original
|
||||
|
||||
expect(Clusters::Applications::UninstallWorker)
|
||||
.to receive(:perform_async).with(jupyter.name, jupyter.id)
|
||||
.and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'logs application uninstalls and next execution' do
|
||||
expect(logger).to receive(:info)
|
||||
.with(log_meta.merge(event: :uninstalling_app, application: kind_of(String))).exactly(3).times
|
||||
expect(logger).to receive(:info)
|
||||
.with(log_meta.merge(event: :scheduling_execution, next_execution: 1))
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'cluster is not cleanup_uninstalling_applications' do
|
||||
let!(:cluster) { create(:cluster, :project, provider_type: :gcp) }
|
||||
|
||||
it_behaves_like 'does not reschedule itself'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when applications are still uninstalling/scheduled/depending on others' do
|
||||
let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
|
||||
let!(:ingress) { create(:clusters_applications_ingress, :scheduled, cluster: cluster) }
|
||||
let!(:runner) { create(:clusters_applications_runner, :uninstalling, cluster: cluster) }
|
||||
|
||||
it_behaves_like 'reschedules itself'
|
||||
|
||||
it 'does not call the uninstallation service' do
|
||||
expect(Clusters::Applications::UninstallWorker).not_to receive(:new)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -37,7 +37,7 @@ RSpec.describe Clusters::DestroyService do
|
|||
let(:params) { { cleanup: 'true' } }
|
||||
|
||||
before do
|
||||
allow(Clusters::Cleanup::AppWorker).to receive(:perform_async)
|
||||
allow(Clusters::Cleanup::ProjectNamespaceWorker).to receive(:perform_async)
|
||||
end
|
||||
|
||||
it 'does not destroy cluster' do
|
||||
|
@ -45,10 +45,10 @@ RSpec.describe Clusters::DestroyService do
|
|||
expect(Clusters::Cluster.where(id: cluster.id).exists?).not_to be_falsey
|
||||
end
|
||||
|
||||
it 'transition cluster#cleanup_status from cleanup_not_started to cleanup_uninstalling_applications' do
|
||||
it 'transition cluster#cleanup_status from cleanup_not_started to cleanup_removing_project_namespaces' do
|
||||
expect { subject }.to change { cluster.cleanup_status_name }
|
||||
.from(:cleanup_not_started)
|
||||
.to(:cleanup_uninstalling_applications)
|
||||
.to(:cleanup_removing_project_namespaces)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
13
spec/support/helpers/global_id_deprecation_helpers.rb
Normal file
13
spec/support/helpers/global_id_deprecation_helpers.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module GlobalIDDeprecationHelpers
|
||||
def stub_global_id_deprecations(*deprecations)
|
||||
old_name_map = deprecations.index_by(&:old_model_name)
|
||||
new_name_map = deprecations.index_by(&:new_model_name)
|
||||
old_graphql_name_map = deprecations.index_by { |d| Types::GlobalIDType.model_name_to_graphql_name(d.old_model_name) }
|
||||
|
||||
stub_const('Gitlab::GlobalId::Deprecations::OLD_NAME_MAP', old_name_map)
|
||||
stub_const('Gitlab::GlobalId::Deprecations::NEW_NAME_MAP', new_name_map)
|
||||
stub_const('Gitlab::GlobalId::Deprecations::OLD_GRAPHQL_NAME_MAP', old_graphql_name_map)
|
||||
end
|
||||
end
|
|
@ -1,41 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Clusters::Cleanup::AppWorker do
|
||||
describe '#perform' do
|
||||
subject { worker_instance.perform(cluster.id) }
|
||||
|
||||
let!(:worker_instance) { described_class.new }
|
||||
let!(:cluster) { create(:cluster, :project, :cleanup_uninstalling_applications, provider_type: :gcp) }
|
||||
let!(:logger) { worker_instance.send(:logger) }
|
||||
|
||||
it_behaves_like 'cluster cleanup worker base specs'
|
||||
|
||||
context 'when exceeded the execution limit' do
|
||||
subject { worker_instance.perform(cluster.id, worker_instance.send(:execution_limit)) }
|
||||
|
||||
let(:worker_instance) { described_class.new }
|
||||
let(:logger) { worker_instance.send(:logger) }
|
||||
let!(:helm) { create(:clusters_applications_helm, :installed, cluster: cluster) }
|
||||
let!(:ingress) { create(:clusters_applications_ingress, :scheduled, cluster: cluster) }
|
||||
|
||||
it 'logs the error' do
|
||||
expect(logger).to receive(:error)
|
||||
.with(
|
||||
hash_including(
|
||||
exception: 'ClusterCleanupMethods::ExceededExecutionLimitError',
|
||||
cluster_id: kind_of(Integer),
|
||||
class_name: described_class.name,
|
||||
applications: "helm:installed,ingress:scheduled",
|
||||
cleanup_status: cluster.cleanup_status_name,
|
||||
event: :failed_to_remove_cluster_and_resources,
|
||||
message: "exceeded execution limit of 10 tries"
|
||||
)
|
||||
)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -908,10 +908,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
|
||||
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
|
||||
|
||||
"@gitlab/ui@29.34.1":
|
||||
version "29.34.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.34.1.tgz#1dfd6864c0c7b325745f28ec708d1f187df3acd8"
|
||||
integrity sha512-uNS3KNAzDFELq5SkfN7Yg9F8Pc98kcPVaXVhZGvw8JcjyVdLF4AORUbzFBsAtJXOwkWPH8yfBQY7+RVC9wkjGg==
|
||||
"@gitlab/ui@29.35.0":
|
||||
version "29.35.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-29.35.0.tgz#bb04d1e4f8796134bc406adaa869c1b5b1fdcaf2"
|
||||
integrity sha512-Fso++QXqxZSfIgSmPGlfQC3mdFI6oh/AOL/9cGn4t/3kfxwHd1GCMjUNAFeHsgyIwKIr1hwksipapwuuOIFSCw==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
Loading…
Reference in a new issue