Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5382b5cdc4
commit
5e65d4f6c6
39 changed files with 904 additions and 221 deletions
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
|
||||
import apolloProvider from './graphql/provider';
|
||||
import createRouter from './router';
|
||||
|
||||
export default () => {
|
||||
const el = document.querySelector('#js-cluster-agent-details');
|
||||
|
@ -20,6 +21,7 @@ export default () => {
|
|||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
router: createRouter(),
|
||||
provide: {
|
||||
activityEmptyStateImage,
|
||||
agentName,
|
||||
|
|
22
app/assets/javascripts/clusters/agents/router.js
Normal file
22
app/assets/javascripts/clusters/agents/router.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
// Vue Router requires a component to render if the route matches, but since we're only using it for
|
||||
// querystring handling, we'll create an empty component.
|
||||
const EmptyRouterComponent = {
|
||||
render(createElement) {
|
||||
return createElement('div');
|
||||
},
|
||||
};
|
||||
|
||||
export default () => {
|
||||
// Name and path here don't really matter since we're not rendering anything if the route matches.
|
||||
const routes = [{ path: '/', name: 'cluster_agents', component: EmptyRouterComponent }];
|
||||
return new VueRouter({
|
||||
mode: 'history',
|
||||
base: window.location.pathname,
|
||||
routes,
|
||||
});
|
||||
};
|
224
app/assets/javascripts/pipeline_wizard/components/commit.vue
Normal file
224
app/assets/javascripts/pipeline_wizard/components/commit.vue
Normal file
|
@ -0,0 +1,224 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormTextarea } from '@gitlab/ui';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import createCommitMutation from '../queries/create_commit.graphql';
|
||||
import getFileMetaDataQuery from '../queries/get_file_meta.graphql';
|
||||
import StepNav from './step_nav.vue';
|
||||
|
||||
export const i18n = {
|
||||
updateFileHeading: s__('PipelineWizard|Commit changes to your file'),
|
||||
createFileHeading: s__('PipelineWizard|Commit your new file'),
|
||||
fieldRequiredFeedback: __('This field is required'),
|
||||
commitMessageLabel: s__('PipelineWizard|Commit Message'),
|
||||
branchSelectorLabel: s__('PipelineWizard|Commit file to Branch'),
|
||||
defaultUpdateCommitMessage: s__('PipelineWizardDefaultCommitMessage|Update %{filename}'),
|
||||
defaultCreateCommitMessage: s__('PipelineWizardDefaultCommitMessage|Add %{filename}'),
|
||||
commitButtonLabel: s__('PipelineWizard|Commit'),
|
||||
commitSuccessMessage: s__('PipelineWizard|The file has been committed.'),
|
||||
errors: {
|
||||
loadError: s__(
|
||||
'PipelineWizard|There was a problem while checking whether your file already exists in the specified branch.',
|
||||
),
|
||||
commitError: s__('PipelineWizard|There was a problem committing the changes.'),
|
||||
},
|
||||
};
|
||||
|
||||
const COMMIT_ACTION = {
|
||||
CREATE: 'CREATE',
|
||||
UPDATE: 'UPDATE',
|
||||
};
|
||||
|
||||
export default {
|
||||
i18n,
|
||||
name: 'PipelineWizardCommitStep',
|
||||
components: {
|
||||
RefSelector,
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormTextarea,
|
||||
StepNav,
|
||||
},
|
||||
props: {
|
||||
prev: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
projectPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultBranch: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileContent: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
filename: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
branch: this.defaultBranch,
|
||||
loading: false,
|
||||
loadError: null,
|
||||
commitError: null,
|
||||
message: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fileExistsInRepo() {
|
||||
return this.project?.repository?.blobs.nodes.length > 0;
|
||||
},
|
||||
commitAction() {
|
||||
return this.fileExistsInRepo ? COMMIT_ACTION.UPDATE : COMMIT_ACTION.CREATE;
|
||||
},
|
||||
defaultMessage() {
|
||||
return sprintf(
|
||||
this.fileExistsInRepo
|
||||
? this.$options.i18n.defaultUpdateCommitMessage
|
||||
: this.$options.i18n.defaultCreateCommitMessage,
|
||||
{ filename: this.filename },
|
||||
);
|
||||
},
|
||||
isCommitButtonEnabled() {
|
||||
return this.fileExistsCheckInProgress;
|
||||
},
|
||||
fileExistsCheckInProgress() {
|
||||
return this.$apollo.queries.project.loading;
|
||||
},
|
||||
mutationPayload() {
|
||||
return {
|
||||
mutation: createCommitMutation,
|
||||
variables: {
|
||||
input: {
|
||||
projectPath: this.projectPath,
|
||||
branch: this.branch,
|
||||
message: this.message || this.defaultMessage,
|
||||
actions: [
|
||||
{
|
||||
action: this.commitAction,
|
||||
filePath: `/${this.filename}`,
|
||||
content: this.fileContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
project: {
|
||||
query: getFileMetaDataQuery,
|
||||
variables() {
|
||||
this.loadError = null;
|
||||
return {
|
||||
fullPath: this.projectPath,
|
||||
filePath: this.filename,
|
||||
ref: this.branch,
|
||||
};
|
||||
},
|
||||
error() {
|
||||
this.loadError = this.$options.i18n.errors.loadError;
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async commit() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate(this.mutationPayload);
|
||||
const hasError = Boolean(data.commitCreate.errors?.length);
|
||||
if (hasError) {
|
||||
this.commitError = this.$options.i18n.errors.commitError;
|
||||
} else {
|
||||
this.handleCommitSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
this.commitError = this.$options.i18n.errors.commitError;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
handleCommitSuccess() {
|
||||
this.$toast.show(this.$options.i18n.commitSuccessMessage);
|
||||
this.$emit('done');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h4 v-if="fileExistsInRepo" key="create-heading">
|
||||
{{ $options.i18n.updateFileHeading }}
|
||||
</h4>
|
||||
<h4 v-else key="update-heading">
|
||||
{{ $options.i18n.createFileHeading }}
|
||||
</h4>
|
||||
<gl-alert
|
||||
v-if="!!loadError"
|
||||
:dismissible="false"
|
||||
class="gl-mb-5"
|
||||
data-testid="load-error"
|
||||
variant="danger"
|
||||
>
|
||||
{{ loadError }}
|
||||
</gl-alert>
|
||||
<gl-form class="gl-max-w-48">
|
||||
<gl-form-group
|
||||
:invalid-feedback="$options.i18n.fieldRequiredFeedback"
|
||||
:label="$options.i18n.commitMessageLabel"
|
||||
data-testid="commit_message_group"
|
||||
label-for="commit_message"
|
||||
>
|
||||
<gl-form-textarea
|
||||
id="commit_message"
|
||||
v-model="message"
|
||||
:placeholder="defaultMessage"
|
||||
data-testid="commit_message"
|
||||
size="md"
|
||||
@input="(v) => $emit('update:message', v)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
:invalid-feedback="$options.i18n.fieldRequiredFeedback"
|
||||
:label="$options.i18n.branchSelectorLabel"
|
||||
data-testid="branch_selector_group"
|
||||
label-for="branch"
|
||||
>
|
||||
<ref-selector id="branch" v-model="branch" data-testid="branch" :project-id="projectPath" />
|
||||
</gl-form-group>
|
||||
<gl-alert
|
||||
v-if="!!commitError"
|
||||
:dismissible="false"
|
||||
class="gl-mb-5"
|
||||
data-testid="commit-error"
|
||||
variant="danger"
|
||||
>
|
||||
{{ commitError }}
|
||||
</gl-alert>
|
||||
<step-nav show-back-button v-bind="$props" @back="$emit('go-back')">
|
||||
<template #after>
|
||||
<gl-button
|
||||
:disabled="isCommitButtonEnabled"
|
||||
:loading="fileExistsCheckInProgress || loading"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
@click="commit"
|
||||
>
|
||||
{{ $options.i18n.commitButtonLabel }}
|
||||
</gl-button>
|
||||
</template>
|
||||
</step-nav>
|
||||
</gl-form>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
mutation CreateCommit($input: CommitCreateInput!) {
|
||||
commitCreate(input: $input) {
|
||||
commit {
|
||||
id
|
||||
}
|
||||
content
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
query GetFileMetadata($fullPath: ID!, $filePath: String!, $ref: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
repository {
|
||||
blobs(paths: [$filePath], ref: $ref) {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,7 +49,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
errorMessage: '',
|
||||
toggleLoading: false,
|
||||
providerLoadingId: null,
|
||||
securityTrainingProviders: [],
|
||||
hasTouchedConfiguration: false,
|
||||
};
|
||||
|
@ -89,37 +89,29 @@ export default {
|
|||
Sentry.captureException(e);
|
||||
}
|
||||
},
|
||||
toggleProvider(selectedProviderId) {
|
||||
const toggledProviders = this.securityTrainingProviders.map((provider) => ({
|
||||
...provider,
|
||||
...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
|
||||
}));
|
||||
toggleProvider(provider) {
|
||||
const { isEnabled } = provider;
|
||||
const toggledIsEnabled = !isEnabled;
|
||||
|
||||
const enabledProviderIds = toggledProviders
|
||||
.filter(({ isEnabled }) => isEnabled)
|
||||
.map(({ id }) => id);
|
||||
|
||||
const { isEnabled: selectedProviderIsEnabled } = toggledProviders.find(
|
||||
(provider) => provider.id === selectedProviderId,
|
||||
);
|
||||
|
||||
this.trackProviderToggle(selectedProviderId, selectedProviderIsEnabled);
|
||||
this.storeEnabledProviders(enabledProviderIds);
|
||||
this.trackProviderToggle(provider.id, toggledIsEnabled);
|
||||
this.storeProvider({ ...provider, isEnabled: toggledIsEnabled });
|
||||
},
|
||||
async storeEnabledProviders(enabledProviderIds) {
|
||||
this.toggleLoading = true;
|
||||
async storeProvider({ id, isEnabled, isPrimary }) {
|
||||
this.providerLoadingId = id;
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
configureSecurityTrainingProviders: { errors = [] },
|
||||
securityTrainingUpdate: { errors = [] },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: configureSecurityTrainingProvidersMutation,
|
||||
variables: {
|
||||
input: {
|
||||
enabledProviders: enabledProviderIds,
|
||||
fullPath: this.projectFullPath,
|
||||
projectPath: this.projectFullPath,
|
||||
providerId: id,
|
||||
isEnabled,
|
||||
isPrimary,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -133,7 +125,7 @@ export default {
|
|||
} catch {
|
||||
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
|
||||
} finally {
|
||||
this.toggleLoading = false;
|
||||
this.providerLoadingId = null;
|
||||
}
|
||||
},
|
||||
trackProviderToggle(providerId, providerIsEnabled) {
|
||||
|
@ -166,25 +158,21 @@ export default {
|
|||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
|
||||
<li
|
||||
v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
|
||||
:key="id"
|
||||
class="gl-mb-6"
|
||||
>
|
||||
<li v-for="provider in securityTrainingProviders" :key="provider.id" class="gl-mb-6">
|
||||
<gl-card>
|
||||
<div class="gl-display-flex">
|
||||
<gl-toggle
|
||||
:value="isEnabled"
|
||||
:value="provider.isEnabled"
|
||||
:label="__('Training mode')"
|
||||
label-position="hidden"
|
||||
:is-loading="toggleLoading"
|
||||
@change="toggleProvider(id)"
|
||||
:is-loading="providerLoadingId === provider.id"
|
||||
@change="toggleProvider(provider)"
|
||||
/>
|
||||
<div class="gl-ml-5">
|
||||
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
|
||||
<h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ provider.name }}</h3>
|
||||
<p>
|
||||
{{ description }}
|
||||
<gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
|
||||
{{ provider.description }}
|
||||
<gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
|
||||
configureSecurityTrainingProviders(input: $input) @client {
|
||||
mutation updateSecurityTraining($input: SecurityTrainingUpdateInput!) {
|
||||
securityTrainingUpdate(input: $input) {
|
||||
errors
|
||||
securityTrainingProviders {
|
||||
training {
|
||||
id
|
||||
isEnabled
|
||||
isPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ query getSecurityTrainingProviders($fullPath: ID!) {
|
|||
name
|
||||
id
|
||||
description
|
||||
isPrimary
|
||||
isEnabled
|
||||
url
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
|
|||
import SecurityConfigurationApp from './components/app.vue';
|
||||
import { securityFeatures, complianceFeatures } from './components/constants';
|
||||
import { augmentFeatures } from './utils';
|
||||
import tempResolvers from './resolver';
|
||||
|
||||
export const initSecurityConfiguration = (el) => {
|
||||
if (!el) {
|
||||
|
@ -15,7 +14,7 @@ export const initSecurityConfiguration = (el) => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(tempResolvers),
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import produce from 'immer';
|
||||
import { __ } from '~/locale';
|
||||
import securityTrainingProvidersQuery from './graphql/security_training_providers.query.graphql';
|
||||
|
||||
// Note: this is behind a feature flag and only a placeholder
|
||||
// until the actual GraphQL fields have been added
|
||||
// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
|
||||
export default {
|
||||
Query: {
|
||||
securityTrainingProviders() {
|
||||
return [
|
||||
{
|
||||
__typename: 'SecurityTrainingProvider',
|
||||
id: 101,
|
||||
name: __('Kontra'),
|
||||
description: __('Interactive developer security education.'),
|
||||
url: 'https://application.security/',
|
||||
isEnabled: false,
|
||||
},
|
||||
{
|
||||
__typename: 'SecurityTrainingProvider',
|
||||
id: 102,
|
||||
name: __('SecureCodeWarrior'),
|
||||
description: __('Security training with guide and learning pathways.'),
|
||||
url: 'https://www.securecodewarrior.com/',
|
||||
isEnabled: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
configureSecurityTrainingProviders: (
|
||||
_,
|
||||
{ input: { enabledProviders, primaryProvider, fullPath } },
|
||||
{ cache },
|
||||
) => {
|
||||
const sourceData = cache.readQuery({
|
||||
query: securityTrainingProvidersQuery,
|
||||
variables: {
|
||||
fullPath,
|
||||
},
|
||||
});
|
||||
|
||||
const data = produce(sourceData.project, (draftData) => {
|
||||
/* eslint-disable no-param-reassign */
|
||||
draftData.securityTrainingProviders.forEach((provider) => {
|
||||
provider.isPrimary = provider.id === primaryProvider;
|
||||
provider.isEnabled =
|
||||
provider.id === primaryProvider || enabledProviders.includes(provider.id);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
__typename: 'configureSecurityTrainingProvidersPayload',
|
||||
securityTrainingProviders: data.securityTrainingProviders,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
|
@ -859,6 +859,8 @@ table.code {
|
|||
}
|
||||
|
||||
.diff-files-changed {
|
||||
background-color: $body-bg;
|
||||
|
||||
.inline-parallel-buttons {
|
||||
@include gl-relative;
|
||||
z-index: 1;
|
||||
|
|
|
@ -8,6 +8,9 @@ module Mutations
|
|||
include Mutations::SpamProtection
|
||||
include FindsProject
|
||||
|
||||
description "Creates a work item." \
|
||||
" Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice."
|
||||
|
||||
authorize :create_work_item
|
||||
|
||||
argument :description, GraphQL::Types::String,
|
||||
|
@ -29,6 +32,11 @@ module Mutations
|
|||
|
||||
def resolve(project_path:, **attributes)
|
||||
project = authorized_find!(project_path)
|
||||
|
||||
unless Feature.enabled?(:work_items, project)
|
||||
return { errors: ['`work_items` feature flag disabled for this project'] }
|
||||
end
|
||||
|
||||
params = global_id_compatibility_params(attributes).merge(author_id: current_user.id)
|
||||
|
||||
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
|
||||
|
|
|
@ -125,7 +125,7 @@ module Types
|
|||
mount_mutation Mutations::Packages::Destroy
|
||||
mount_mutation Mutations::Packages::DestroyFile
|
||||
mount_mutation Mutations::Echo
|
||||
mount_mutation Mutations::WorkItems::Create, feature_flag: :work_items
|
||||
mount_mutation Mutations::WorkItems::Create
|
||||
mount_mutation Mutations::WorkItems::Delete
|
||||
mount_mutation Mutations::WorkItems::Update
|
||||
end
|
||||
|
|
|
@ -117,6 +117,7 @@ class Member < ApplicationRecord
|
|||
# to projects/groups.
|
||||
scope :authorizable, -> do
|
||||
connected_to_user
|
||||
.active_state
|
||||
.non_request
|
||||
.non_minimal_access
|
||||
end
|
||||
|
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
.row.gl-mt-3
|
||||
.form-group.col-md-9
|
||||
= f.label :description, s_('Groups|Group description'), class: 'label-bold'
|
||||
= f.label :description, s_('Groups|Group description (optional)'), class: 'label-bold'
|
||||
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
|
||||
.form-text.text-muted= s_('Groups|Optional group description.')
|
||||
|
||||
= render 'shared/repository_size_limit_setting_registration_features_cta', form: f
|
||||
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: vulnerability_report_pagination
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79834
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351975
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::threat insights
|
||||
default_enabled: false
|
|
@ -441,7 +441,7 @@ These are different from project or personal access tokens in the GitLab applica
|
|||
### Listing all container repositories
|
||||
|
||||
```plaintext
|
||||
GET /v2/_catalogue
|
||||
GET /v2/_catalog
|
||||
```
|
||||
|
||||
To list all container repositories on your GitLab instance, admin credentials are required:
|
||||
|
|
|
@ -5168,7 +5168,7 @@ Input type: `VulnerabilityRevertToDetectedInput`
|
|||
|
||||
### `Mutation.workItemCreate`
|
||||
|
||||
Available only when feature flag `work_items` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
|
||||
Creates a work item. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
|
||||
|
||||
Input type: `WorkItemCreateInput`
|
||||
|
||||
|
|
|
@ -473,7 +473,7 @@ The following are some available Rake tasks:
|
|||
| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Enables Elasticsearch indexing and run `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:pause_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Pauses Elasticsearch indexing. Changes are still tracked. Useful for cluster/index migrations. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:resume_indexing`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Resumes Elasticsearch indexing. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects and queues Sidekiq jobs to index them in the background. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects, and queues Sidekiq jobs to index them in the background. It can only be used after the index is created. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. Note that this command results in a complete wipe of the index, and it should be used with caution. |
|
||||
| [`sudo gitlab-rake gitlab:elastic:create_empty_index`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Generates empty indexes (the default index and a separate issues index) and assigns an alias for each on the Elasticsearch side only if it doesn't already exist. |
|
||||
|
|
|
@ -85,8 +85,10 @@ When you enable the roadmap settings sidebar, you can use it to refine epics sho
|
|||
You can configure the following:
|
||||
|
||||
- Select date range.
|
||||
- Turn milestones on or off and select whether to show all, group, sub-group, or project milestones.
|
||||
- Show all, open, or closed epics.
|
||||
- Turn progress tracking on or off and select whether it uses issue weights or counts.
|
||||
- Turn progress tracking for child issues on or off and select whether
|
||||
to use issue weights or counts.
|
||||
|
||||
The progress tracking setting is not saved in user preferences but is saved or shared using URL parameters.
|
||||
|
||||
|
|
|
@ -117,16 +117,15 @@ production environment for all merge requests deployed in the given time period.
|
|||
The "Recent Activity" metrics near the top of the page are measured as follows:
|
||||
|
||||
- **New Issues:** the number of issues created in the date range.
|
||||
- **Deploys:** the number of deployments <sup>1</sup> to production <sup>2</sup> in the date range.
|
||||
- **Deployment Frequency:** the average number of deployments <sup>1</sup> to production <sup>2</sup>
|
||||
- **Deploys:** the number of deployments to production in the date range.
|
||||
- **Deployment Frequency:** the average number of deployments to production
|
||||
per day in the date range.
|
||||
|
||||
1. To give a more accurate representation of deployments that actually completed successfully,
|
||||
the calculation for these two metrics changed in GitLab 13.9 from using the time a deployment was
|
||||
created to the time a deployment finished. If you were referencing this metric prior to 13.9, please
|
||||
keep this slight change in mind.
|
||||
1. To see deployment metrics, you must have a
|
||||
[production environment configured](../../../ci/environments/index.md#deployment-tier-of-environments).
|
||||
To see deployment metrics, you must have a [production environment configured](../../../ci/environments/index.md#deployment-tier-of-environments).
|
||||
|
||||
NOTE:
|
||||
In GitLab 13.9 and later, deployment metrics are calculated based on when the deployment was finished.
|
||||
In GitLab 13.8 and earlier, deployment metrics are calculated based on when the deployment was created.
|
||||
|
||||
You can learn more about these metrics in our [analytics definitions](../../analytics/index.md).
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ When issues/pull requests are being imported, the Bitbucket importer tries to fi
|
|||
the Bitbucket author/assignee in the GitLab database using the Bitbucket `nickname`.
|
||||
For this to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
|
||||
and **associated their Bitbucket account**. Their `nickname` must also match their Bitbucket
|
||||
`username.`. If the user is not found in the GitLab database, the project creator
|
||||
`username`. If the user is not found in the GitLab database, the project creator
|
||||
(most of the times the current user that started the import process) is set as the author,
|
||||
but a reference on the issue about the original Bitbucket author is kept.
|
||||
|
||||
|
|
|
@ -29,21 +29,17 @@ To seamlessly navigate among commits in a merge request:
|
|||
|
||||
## View merge request commits in context
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29274) in GitLab 13.12.
|
||||
> - [Deployed behind a feature flag](../../feature_flags.md), enabled by default.
|
||||
> - Disabled on GitLab.com.
|
||||
> - Not recommended for production use.
|
||||
> - To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-viewing-merge-request-commits-in-context).
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29274) in GitLab 13.12 [with a flag](../../../administration/feature_flags.md) named `context_commits`. Enabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/320757) in GitLab 14.8.
|
||||
|
||||
WARNING:
|
||||
This feature is in [beta](../../../policy/alpha-beta-support.md#beta-features)
|
||||
and is [incomplete](https://gitlab.com/groups/gitlab-org/-/epics/1192).
|
||||
Previously merged commits can be added, but they can't be removed due to
|
||||
[this bug](https://gitlab.com/gitlab-org/gitlab/-/issues/325538).
|
||||
|
||||
This in-development feature might not be available for your use. There can be
|
||||
[risks when enabling features still in development](../../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
|
||||
Refer to this feature's version history for more details.
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||
ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `context_commits`.
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
When reviewing a merge request, it helps to have more context about the changes
|
||||
made. That includes unchanged lines in unchanged files, and previous commits
|
||||
|
@ -66,22 +62,3 @@ To view the changes done on those previously merged commits:
|
|||
1. Scroll to **(file-tree)** **Compare** and select **previously merged commits**:
|
||||
|
||||
![Previously merged commits](img/previously_merged_commits_v14_1.png)
|
||||
|
||||
### Enable or disable viewing merge request commits in context **(FREE SELF)**
|
||||
|
||||
Viewing merge request commits in context is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:context_commits)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:context_commits)
|
||||
```
|
||||
|
|
|
@ -64,11 +64,11 @@ module Gitlab
|
|||
def create_test_case(data, test_suite, job)
|
||||
if data.key?('failure')
|
||||
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
|
||||
system_output = data['failure']
|
||||
system_output = data['failure'] || data['system_err']
|
||||
attachment = attachment_path(data['system_out'])
|
||||
elsif data.key?('error')
|
||||
status = ::Gitlab::Ci::Reports::TestCase::STATUS_ERROR
|
||||
system_output = data['error']
|
||||
system_output = data['error'] || data['system_err']
|
||||
attachment = attachment_path(data['system_out'])
|
||||
elsif data.key?('skipped')
|
||||
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED
|
||||
|
|
|
@ -48,11 +48,15 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.context_key
|
||||
"#{self.class.name}_context"
|
||||
@context_key ||= "analyzer_#{self.analyzer_key}_context".to_sym
|
||||
end
|
||||
|
||||
def self.suppress_key
|
||||
"#{self.class.name}_suppressed"
|
||||
@suppress_key ||= "analyzer_#{self.analyzer_key}_suppressed".to_sym
|
||||
end
|
||||
|
||||
def self.analyzer_key
|
||||
@analyzer_key ||= self.name.demodulize.underscore.to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -426,8 +426,23 @@ msgid_plural "%d vulnerabilities dismissed"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d vulnerability updated"
|
||||
msgid_plural "%d vulnerabilities updated"
|
||||
msgid "%d vulnerability set to confirmed"
|
||||
msgid_plural "%d vulnerabilities set to confirmed"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d vulnerability set to dismissed"
|
||||
msgid_plural "%d vulnerabilities set to dismissed"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d vulnerability set to needs triage"
|
||||
msgid_plural "%d vulnerabilities set to needs triage"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d vulnerability set to resolved"
|
||||
msgid_plural "%d vulnerabilities set to resolved"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
|
@ -17779,7 +17794,7 @@ msgstr ""
|
|||
msgid "Groups|Group avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "Groups|Group description"
|
||||
msgid "Groups|Group description (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Groups|Group name"
|
||||
|
@ -17797,9 +17812,6 @@ msgstr ""
|
|||
msgid "Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses."
|
||||
msgstr ""
|
||||
|
||||
msgid "Groups|Optional group description."
|
||||
msgstr ""
|
||||
|
||||
msgid "Groups|Remove avatar"
|
||||
msgstr ""
|
||||
|
||||
|
@ -19726,9 +19738,6 @@ msgstr ""
|
|||
msgid "Integrations|can't exceed %{recipients_limit}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Interactive developer security education."
|
||||
msgstr ""
|
||||
|
||||
msgid "Interactive mode"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21067,9 +21076,6 @@ msgstr ""
|
|||
msgid "Ki"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kontra"
|
||||
msgstr ""
|
||||
|
||||
msgid "Kroki"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26643,12 +26649,42 @@ msgstr ""
|
|||
msgid "PipelineStatusTooltip|Pipeline: %{ci_status}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizardDefaultCommitMessage|Add %{filename}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizardDefaultCommitMessage|Update %{filename}"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizardInputValidation|This field is required"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizardInputValidation|This value is not valid"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|Commit"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|Commit Message"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|Commit changes to your file"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|Commit file to Branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|Commit your new file"
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|The file has been committed."
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|There was a problem committing the changes."
|
||||
msgstr ""
|
||||
|
||||
msgid "PipelineWizard|There was a problem while checking whether your file already exists in the specified branch."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32024,9 +32060,6 @@ msgstr ""
|
|||
msgid "Secure token that identifies an external storage request."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecureCodeWarrior"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32051,9 +32084,6 @@ msgstr ""
|
|||
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
||||
msgstr ""
|
||||
|
||||
msgid "Security training with guide and learning pathways."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -279,7 +279,8 @@ function rspec_paralellized_job() {
|
|||
# Experiment to retry failed examples in a new RSpec process: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1148
|
||||
if [[ $rspec_run_status -ne 0 ]]; then
|
||||
if [[ "${RETRY_FAILED_TESTS_IN_NEW_PROCESS}" == "true" ]]; then
|
||||
$rspec_run_status=$(retry_failed_rspec_examples)
|
||||
retry_failed_rspec_examples
|
||||
rspec_run_status=$?
|
||||
fi
|
||||
else
|
||||
echosuccess "No examples to retry, congrats!"
|
||||
|
@ -310,7 +311,7 @@ function retry_failed_rspec_examples() {
|
|||
# Merge the JUnit report from retry into the first-try report
|
||||
junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}"
|
||||
|
||||
return $rspec_run_status
|
||||
exit $rspec_run_status
|
||||
}
|
||||
|
||||
function rspec_rerun_previous_failed_tests() {
|
||||
|
|
282
spec/frontend/pipeline_wizard/components/commit_spec.js
Normal file
282
spec/frontend/pipeline_wizard/components/commit_spec.js
Normal file
|
@ -0,0 +1,282 @@
|
|||
import { GlButton, GlFormGroup } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { mountExtended } from 'jest/__helpers__/vue_test_utils_helper';
|
||||
import CommitStep, { i18n } from '~/pipeline_wizard/components/commit.vue';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import createCommitMutation from '~/pipeline_wizard/queries/create_commit.graphql';
|
||||
import getFileMetadataQuery from '~/pipeline_wizard/queries/get_file_meta.graphql';
|
||||
import RefSelector from '~/ref/components/ref_selector.vue';
|
||||
import flushPromises from 'helpers/flush_promises';
|
||||
import {
|
||||
createCommitMutationErrorResult,
|
||||
createCommitMutationResult,
|
||||
fileQueryErrorResult,
|
||||
fileQueryResult,
|
||||
fileQueryEmptyResult,
|
||||
} from '../mock/query_responses';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const COMMIT_MESSAGE_ADD_FILE = s__('PipelineWizardDefaultCommitMessage|Add %{filename}');
|
||||
const COMMIT_MESSAGE_UPDATE_FILE = s__('PipelineWizardDefaultCommitMessage|Update %{filename}');
|
||||
|
||||
describe('Pipeline Wizard - Commit Page', () => {
|
||||
const createCommitMutationHandler = jest.fn();
|
||||
const $toast = {
|
||||
show: jest.fn(),
|
||||
};
|
||||
|
||||
let wrapper;
|
||||
|
||||
const getMockApollo = (scenario = {}) => {
|
||||
return createMockApollo([
|
||||
[
|
||||
createCommitMutation,
|
||||
createCommitMutationHandler.mockResolvedValue(
|
||||
scenario.commitHasError ? createCommitMutationErrorResult : createCommitMutationResult,
|
||||
),
|
||||
],
|
||||
[
|
||||
getFileMetadataQuery,
|
||||
(vars) => {
|
||||
if (scenario.fileResultByRef) return scenario.fileResultByRef[vars.ref];
|
||||
if (scenario.hasError) return fileQueryErrorResult;
|
||||
return scenario.fileExists ? fileQueryResult : fileQueryEmptyResult;
|
||||
},
|
||||
],
|
||||
]);
|
||||
};
|
||||
const createComponent = (props = {}, mockApollo = getMockApollo()) => {
|
||||
wrapper = mountExtended(CommitStep, {
|
||||
apolloProvider: mockApollo,
|
||||
propsData: {
|
||||
projectPath: 'some/path',
|
||||
defaultBranch: 'main',
|
||||
filename: 'newFile.yml',
|
||||
...props,
|
||||
},
|
||||
mocks: { $toast },
|
||||
stubs: {
|
||||
RefSelector: true,
|
||||
GlFormGroup,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function getButtonWithLabel(label) {
|
||||
return wrapper.findAllComponents(GlButton).filter((n) => n.text().match(label));
|
||||
}
|
||||
|
||||
describe('ui setup', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('shows a commit message input with the correct label', () => {
|
||||
expect(wrapper.findByTestId('commit_message').exists()).toBe(true);
|
||||
expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel);
|
||||
});
|
||||
|
||||
it('shows a branch selector with the correct label', () => {
|
||||
expect(wrapper.findByTestId('branch').exists()).toBe(true);
|
||||
expect(wrapper.find('label[for="branch"]').text()).toBe(i18n.branchSelectorLabel);
|
||||
});
|
||||
|
||||
it('shows a commit button', () => {
|
||||
expect(getButtonWithLabel(i18n.commitButtonLabel).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows a back button', () => {
|
||||
expect(getButtonWithLabel(__('Back')).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show a next button', () => {
|
||||
expect(getButtonWithLabel(__('Next')).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading the remote file', () => {
|
||||
const projectPath = 'foo/bar';
|
||||
const filename = 'foo.yml';
|
||||
|
||||
it('does not show a load error if call is successful', async () => {
|
||||
createComponent({ projectPath, filename });
|
||||
await flushPromises();
|
||||
expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
|
||||
});
|
||||
|
||||
it('shows a load error if call returns an unexpected error', async () => {
|
||||
const branch = 'foo';
|
||||
createComponent(
|
||||
{ defaultBranch: branch, projectPath, filename },
|
||||
createMockApollo([[getFileMetadataQuery, () => fileQueryErrorResult]]),
|
||||
);
|
||||
await flushPromises();
|
||||
expect(wrapper.findByTestId('load-error').exists()).toBe(true);
|
||||
expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit result handling', () => {
|
||||
describe('successful commit', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
await flushPromises();
|
||||
await getButtonWithLabel(__('Commit')).trigger('click');
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it('will not show an error', async () => {
|
||||
expect(wrapper.findByTestId('commit-error').exists()).not.toBe(true);
|
||||
});
|
||||
|
||||
it('will show a toast message', () => {
|
||||
expect($toast.show).toHaveBeenCalledWith(
|
||||
s__('PipelineWizard|The file has been committed.'),
|
||||
);
|
||||
});
|
||||
|
||||
it('emits a done event', () => {
|
||||
expect(wrapper.emitted().done.length).toBe(1);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed commit', () => {
|
||||
beforeEach(async () => {
|
||||
createComponent({}, getMockApollo({ commitHasError: true }));
|
||||
await flushPromises();
|
||||
await getButtonWithLabel(__('Commit')).trigger('click');
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it('will show an error', async () => {
|
||||
expect(wrapper.findByTestId('commit-error').exists()).toBe(true);
|
||||
expect(wrapper.findByTestId('commit-error').text()).toBe(i18n.errors.commitError);
|
||||
});
|
||||
|
||||
it('will not show a toast message', () => {
|
||||
expect($toast.show).not.toHaveBeenCalledWith(i18n.commitSuccessMessage);
|
||||
});
|
||||
|
||||
it('will not emit a done event', () => {
|
||||
expect(wrapper.emitted().done?.length).toBeFalsy();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('modelling different input combinations', () => {
|
||||
const projectPath = 'some/path';
|
||||
const defaultBranch = 'foo';
|
||||
const fileContent = 'foo: bar';
|
||||
|
||||
describe.each`
|
||||
filename | fileExistsOnDefaultBranch | fileExistsOnInputtedBranch | fileLoadError | commitMessageInputValue | branchInputValue | expectedCommitBranch | expectedCommitMessage | expectedAction
|
||||
${'foo.yml'} | ${false} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'CREATE'}
|
||||
${'foo.yml'} | ${true} | ${undefined} | ${false} | ${'foo'} | ${undefined} | ${defaultBranch} | ${'foo'} | ${'UPDATE'}
|
||||
${'foo.yml'} | ${false} | ${true} | ${false} | ${'foo'} | ${'dev'} | ${'dev'} | ${'foo'} | ${'UPDATE'}
|
||||
${'foo.yml'} | ${false} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_ADD_FILE} | ${'CREATE'}
|
||||
${'foo.yml'} | ${true} | ${undefined} | ${false} | ${null} | ${undefined} | ${defaultBranch} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'}
|
||||
${'foo.yml'} | ${false} | ${true} | ${false} | ${null} | ${'dev'} | ${'dev'} | ${COMMIT_MESSAGE_UPDATE_FILE} | ${'UPDATE'}
|
||||
`(
|
||||
'Test with fileExistsOnDefaultBranch=$fileExistsOnDefaultBranch, fileExistsOnInputtedBranch=$fileExistsOnInputtedBranch, commitMessageInputValue=$commitMessageInputValue, branchInputValue=$branchInputValue, commitReturnsError=$commitReturnsError',
|
||||
({
|
||||
filename,
|
||||
fileExistsOnDefaultBranch,
|
||||
fileExistsOnInputtedBranch,
|
||||
commitMessageInputValue,
|
||||
branchInputValue,
|
||||
expectedCommitBranch,
|
||||
expectedCommitMessage,
|
||||
expectedAction,
|
||||
}) => {
|
||||
let consoleSpy;
|
||||
|
||||
beforeAll(async () => {
|
||||
createComponent(
|
||||
{
|
||||
filename,
|
||||
defaultBranch,
|
||||
projectPath,
|
||||
fileContent,
|
||||
},
|
||||
getMockApollo({
|
||||
fileResultByRef: {
|
||||
[defaultBranch]: fileExistsOnDefaultBranch ? fileQueryResult : fileQueryEmptyResult,
|
||||
[branchInputValue]: fileExistsOnInputtedBranch
|
||||
? fileQueryResult
|
||||
: fileQueryEmptyResult,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
consoleSpy = jest.spyOn(console, 'error');
|
||||
|
||||
await wrapper
|
||||
.findByTestId('commit_message')
|
||||
.get('textarea')
|
||||
.setValue(commitMessageInputValue);
|
||||
|
||||
if (branchInputValue) {
|
||||
await wrapper.getComponent(RefSelector).vm.$emit('input', branchInputValue);
|
||||
}
|
||||
await Vue.nextTick();
|
||||
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('sets up without error', async () => {
|
||||
expect(consoleSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show a load error', async () => {
|
||||
expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
|
||||
});
|
||||
|
||||
it('sends the expected commit mutation', async () => {
|
||||
await getButtonWithLabel(__('Commit')).trigger('click');
|
||||
|
||||
expect(createCommitMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
actions: [
|
||||
{
|
||||
action: expectedAction,
|
||||
content: fileContent,
|
||||
filePath: `/${filename}`,
|
||||
},
|
||||
],
|
||||
branch: expectedCommitBranch,
|
||||
message: sprintf(expectedCommitMessage, { filename }),
|
||||
projectPath,
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
62
spec/frontend/pipeline_wizard/mock/query_responses.js
Normal file
62
spec/frontend/pipeline_wizard/mock/query_responses.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
export const createCommitMutationResult = {
|
||||
data: {
|
||||
commitCreate: {
|
||||
commit: {
|
||||
id: '82a9df1',
|
||||
},
|
||||
content: 'foo: bar',
|
||||
errors: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const createCommitMutationErrorResult = {
|
||||
data: {
|
||||
commitCreate: {
|
||||
commit: null,
|
||||
content: null,
|
||||
errors: ['Some Error Message'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const fileQueryResult = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
repository: {
|
||||
blobs: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'gid://gitlab/Blob/9ff96777b315cd37188f7194d8382c718cb2933c',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const fileQueryEmptyResult = {
|
||||
data: {
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/2',
|
||||
repository: {
|
||||
blobs: {
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const fileQueryErrorResult = {
|
||||
data: {
|
||||
foo: 'bar',
|
||||
project: {
|
||||
id: null,
|
||||
repository: null,
|
||||
},
|
||||
},
|
||||
errors: [{ message: 'GraphQL Error' }],
|
||||
};
|
|
@ -19,6 +19,8 @@ import {
|
|||
dismissUserCalloutErrorResponse,
|
||||
securityTrainingProviders,
|
||||
securityTrainingProvidersResponse,
|
||||
updateSecurityTrainingProvidersResponse,
|
||||
updateSecurityTrainingProvidersErrorResponse,
|
||||
testProjectPath,
|
||||
textProviderIds,
|
||||
} from '../mock_data';
|
||||
|
@ -29,18 +31,22 @@ describe('TrainingProviderList component', () => {
|
|||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
const createApolloProvider = ({ resolvers, handlers = [] } = {}) => {
|
||||
const createApolloProvider = ({ handlers = [] } = {}) => {
|
||||
const defaultHandlers = [
|
||||
[
|
||||
securityTrainingProvidersQuery,
|
||||
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
|
||||
],
|
||||
[
|
||||
configureSecurityTrainingProvidersMutation,
|
||||
jest.fn().mockResolvedValue(updateSecurityTrainingProvidersResponse),
|
||||
],
|
||||
];
|
||||
|
||||
// make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors
|
||||
const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])];
|
||||
|
||||
apolloProvider = createMockApollo(mergedHandlers, resolvers);
|
||||
apolloProvider = createMockApollo(mergedHandlers);
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
|
@ -62,7 +68,7 @@ describe('TrainingProviderList component', () => {
|
|||
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||
const findErrorAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
|
||||
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -146,9 +152,9 @@ describe('TrainingProviderList component', () => {
|
|||
beforeEach(async () => {
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||
|
||||
await waitForMutationToBeLoaded();
|
||||
await waitForQueryToBeLoaded();
|
||||
|
||||
toggleFirstProvider();
|
||||
await toggleFirstProvider();
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -166,7 +172,14 @@ describe('TrainingProviderList component', () => {
|
|||
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: configureSecurityTrainingProvidersMutation,
|
||||
variables: { input: { enabledProviders: textProviderIds, fullPath: testProjectPath } },
|
||||
variables: {
|
||||
input: {
|
||||
providerId: textProviderIds[0],
|
||||
isEnabled: true,
|
||||
isPrimary: false,
|
||||
projectPath: testProjectPath,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -264,14 +277,12 @@ describe('TrainingProviderList component', () => {
|
|||
describe('when storing training provider configurations', () => {
|
||||
beforeEach(async () => {
|
||||
createApolloProvider({
|
||||
resolvers: {
|
||||
Mutation: {
|
||||
configureSecurityTrainingProviders: () => ({
|
||||
errors: ['something went wrong!'],
|
||||
securityTrainingProviders: [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
handlers: [
|
||||
[
|
||||
configureSecurityTrainingProvidersMutation,
|
||||
jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse),
|
||||
],
|
||||
],
|
||||
});
|
||||
createComponent();
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ export const securityTrainingProviders = [
|
|||
description: 'Interactive developer security education',
|
||||
url: 'https://www.example.org/security/training',
|
||||
isEnabled: false,
|
||||
isPrimary: false,
|
||||
},
|
||||
{
|
||||
id: textProviderIds[1],
|
||||
|
@ -16,6 +17,7 @@ export const securityTrainingProviders = [
|
|||
description: 'Security training with guide and learning pathways.',
|
||||
url: 'https://www.vendornametwo.com/',
|
||||
isEnabled: true,
|
||||
isPrimary: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -51,3 +53,26 @@ export const dismissUserCalloutErrorResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const updateSecurityTrainingProvidersResponse = {
|
||||
data: {
|
||||
securityTrainingUpdate: {
|
||||
errors: [],
|
||||
training: {
|
||||
id: 101,
|
||||
name: 'Acme',
|
||||
isEnabled: true,
|
||||
isPrimary: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const updateSecurityTrainingProvidersErrorResponse = {
|
||||
data: {
|
||||
securityTrainingUpdate: {
|
||||
errors: ['something went wrong!'],
|
||||
training: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -98,7 +98,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration,
|
|||
protected: true,
|
||||
instance_runners_enabled: true,
|
||||
minutes_exceeded: false,
|
||||
tag_ids: [22, 23],
|
||||
tag_ids: match_array([22, 23]),
|
||||
namespace_traversal_ids: [10]),
|
||||
an_object_having_attributes(
|
||||
build_id: 60,
|
||||
|
|
|
@ -99,6 +99,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
|
|||
'Some failure'
|
||||
end
|
||||
|
||||
context 'and has failure with no message but has system-err' do
|
||||
let(:testcase_content) do
|
||||
<<-EOF.strip_heredoc
|
||||
<failure></failure>
|
||||
<system-err>Some failure</system-err>
|
||||
EOF
|
||||
end
|
||||
|
||||
it_behaves_like '<testcase> XML parser',
|
||||
::Gitlab::Ci::Reports::TestCase::STATUS_FAILED,
|
||||
'Some failure'
|
||||
end
|
||||
|
||||
context 'and has error' do
|
||||
let(:testcase_content) { '<error>Some error</error>' }
|
||||
|
||||
|
@ -107,6 +120,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
|
|||
'Some error'
|
||||
end
|
||||
|
||||
context 'and has error with no message but has system-err' do
|
||||
let(:testcase_content) do
|
||||
<<-EOF.strip_heredoc
|
||||
<error></error>
|
||||
<system-err>Some error</system-err>
|
||||
EOF
|
||||
end
|
||||
|
||||
it_behaves_like '<testcase> XML parser',
|
||||
::Gitlab::Ci::Reports::TestCase::STATUS_ERROR,
|
||||
'Some error'
|
||||
end
|
||||
|
||||
context 'and has skipped' do
|
||||
let(:testcase_content) { '<skipped/>' }
|
||||
|
||||
|
|
|
@ -14,6 +14,22 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
|
|||
Gitlab::Database::QueryAnalyzer.instance.within { example.run }
|
||||
end
|
||||
|
||||
describe 'context and suppress key names' do
|
||||
describe '.context_key' do
|
||||
it 'contains class name' do
|
||||
expect(described_class.context_key)
|
||||
.to eq 'analyzer_prevent_cross_database_modification_context'.to_sym
|
||||
end
|
||||
end
|
||||
|
||||
describe '.suppress_key' do
|
||||
it 'contains class name' do
|
||||
expect(described_class.suppress_key)
|
||||
.to eq 'analyzer_prevent_cross_database_modification_suppressed'.to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'successful examples' do |model:|
|
||||
let(:model) { model }
|
||||
|
||||
|
|
|
@ -513,6 +513,8 @@ RSpec.describe Member do
|
|||
it { is_expected.not_to include @invited_member }
|
||||
it { is_expected.not_to include @requested_member }
|
||||
it { is_expected.not_to include @member_with_minimal_access }
|
||||
it { is_expected.not_to include awaiting_group_member }
|
||||
it { is_expected.not_to include awaiting_project_member }
|
||||
end
|
||||
|
||||
describe '.distinct_on_user_with_max_access_level' do
|
||||
|
|
|
@ -68,8 +68,13 @@ RSpec.describe 'Create a work item' do
|
|||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'a mutation that returns top-level errors',
|
||||
errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
|
||||
it 'does not create the work item and returns an error' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end.to not_change(WorkItem, :count)
|
||||
|
||||
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,10 +71,11 @@ RSpec.describe 'Update a work item' do
|
|||
stub_feature_flags(work_items: false)
|
||||
end
|
||||
|
||||
it 'does nothing and returns and error' do
|
||||
it 'does not update the work item and returns and error' do
|
||||
expect do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
end.to not_change(WorkItem, :count)
|
||||
work_item.reload
|
||||
end.to not_change(work_item, :title)
|
||||
|
||||
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||
end
|
||||
|
|
|
@ -748,6 +748,30 @@ RSpec.describe API::Internal::Base do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with a pending membership' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
before_all do
|
||||
create(:project_member, :awaiting, :developer, source: project, user: user)
|
||||
end
|
||||
|
||||
it 'returns not found for git pull' do
|
||||
pull(key, project)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response["status"]).to be_falsey
|
||||
expect(user.reload.last_activity_on).to be_nil
|
||||
end
|
||||
|
||||
it 'returns not found for git push' do
|
||||
push(key, project)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response["status"]).to be_falsey
|
||||
expect(user.reload.last_activity_on).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "custom action" do
|
||||
let(:access_checker) { double(Gitlab::GitAccess) }
|
||||
let(:payload) do
|
||||
|
|
Loading…
Reference in a new issue