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 Vue from 'vue';
|
||||||
import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
|
import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
|
||||||
import apolloProvider from './graphql/provider';
|
import apolloProvider from './graphql/provider';
|
||||||
|
import createRouter from './router';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const el = document.querySelector('#js-cluster-agent-details');
|
const el = document.querySelector('#js-cluster-agent-details');
|
||||||
|
@ -20,6 +21,7 @@ export default () => {
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el,
|
el,
|
||||||
apolloProvider,
|
apolloProvider,
|
||||||
|
router: createRouter(),
|
||||||
provide: {
|
provide: {
|
||||||
activityEmptyStateImage,
|
activityEmptyStateImage,
|
||||||
agentName,
|
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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
toggleLoading: false,
|
providerLoadingId: null,
|
||||||
securityTrainingProviders: [],
|
securityTrainingProviders: [],
|
||||||
hasTouchedConfiguration: false,
|
hasTouchedConfiguration: false,
|
||||||
};
|
};
|
||||||
|
@ -89,37 +89,29 @@ export default {
|
||||||
Sentry.captureException(e);
|
Sentry.captureException(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleProvider(selectedProviderId) {
|
toggleProvider(provider) {
|
||||||
const toggledProviders = this.securityTrainingProviders.map((provider) => ({
|
const { isEnabled } = provider;
|
||||||
...provider,
|
const toggledIsEnabled = !isEnabled;
|
||||||
...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const enabledProviderIds = toggledProviders
|
this.trackProviderToggle(provider.id, toggledIsEnabled);
|
||||||
.filter(({ isEnabled }) => isEnabled)
|
this.storeProvider({ ...provider, isEnabled: toggledIsEnabled });
|
||||||
.map(({ id }) => id);
|
|
||||||
|
|
||||||
const { isEnabled: selectedProviderIsEnabled } = toggledProviders.find(
|
|
||||||
(provider) => provider.id === selectedProviderId,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.trackProviderToggle(selectedProviderId, selectedProviderIsEnabled);
|
|
||||||
this.storeEnabledProviders(enabledProviderIds);
|
|
||||||
},
|
},
|
||||||
async storeEnabledProviders(enabledProviderIds) {
|
async storeProvider({ id, isEnabled, isPrimary }) {
|
||||||
this.toggleLoading = true;
|
this.providerLoadingId = id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
configureSecurityTrainingProviders: { errors = [] },
|
securityTrainingUpdate: { errors = [] },
|
||||||
},
|
},
|
||||||
} = await this.$apollo.mutate({
|
} = await this.$apollo.mutate({
|
||||||
mutation: configureSecurityTrainingProvidersMutation,
|
mutation: configureSecurityTrainingProvidersMutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
enabledProviders: enabledProviderIds,
|
projectPath: this.projectFullPath,
|
||||||
fullPath: this.projectFullPath,
|
providerId: id,
|
||||||
|
isEnabled,
|
||||||
|
isPrimary,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -133,7 +125,7 @@ export default {
|
||||||
} catch {
|
} catch {
|
||||||
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
|
this.errorMessage = this.$options.i18n.configMutationErrorMessage;
|
||||||
} finally {
|
} finally {
|
||||||
this.toggleLoading = false;
|
this.providerLoadingId = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trackProviderToggle(providerId, providerIsEnabled) {
|
trackProviderToggle(providerId, providerIsEnabled) {
|
||||||
|
@ -166,25 +158,21 @@ export default {
|
||||||
</gl-skeleton-loader>
|
</gl-skeleton-loader>
|
||||||
</div>
|
</div>
|
||||||
<ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
|
<ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
|
||||||
<li
|
<li v-for="provider in securityTrainingProviders" :key="provider.id" class="gl-mb-6">
|
||||||
v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
|
|
||||||
:key="id"
|
|
||||||
class="gl-mb-6"
|
|
||||||
>
|
|
||||||
<gl-card>
|
<gl-card>
|
||||||
<div class="gl-display-flex">
|
<div class="gl-display-flex">
|
||||||
<gl-toggle
|
<gl-toggle
|
||||||
:value="isEnabled"
|
:value="provider.isEnabled"
|
||||||
:label="__('Training mode')"
|
:label="__('Training mode')"
|
||||||
label-position="hidden"
|
label-position="hidden"
|
||||||
:is-loading="toggleLoading"
|
:is-loading="providerLoadingId === provider.id"
|
||||||
@change="toggleProvider(id)"
|
@change="toggleProvider(provider)"
|
||||||
/>
|
/>
|
||||||
<div class="gl-ml-5">
|
<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>
|
<p>
|
||||||
{{ description }}
|
{{ provider.description }}
|
||||||
<gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
|
<gl-link :href="provider.url" target="_blank">{{ __('Learn more.') }}</gl-link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
|
mutation updateSecurityTraining($input: SecurityTrainingUpdateInput!) {
|
||||||
configureSecurityTrainingProviders(input: $input) @client {
|
securityTrainingUpdate(input: $input) {
|
||||||
errors
|
errors
|
||||||
securityTrainingProviders {
|
training {
|
||||||
id
|
id
|
||||||
isEnabled
|
isEnabled
|
||||||
|
isPrimary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ query getSecurityTrainingProviders($fullPath: ID!) {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
description
|
description
|
||||||
|
isPrimary
|
||||||
isEnabled
|
isEnabled
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
|
||||||
import SecurityConfigurationApp from './components/app.vue';
|
import SecurityConfigurationApp from './components/app.vue';
|
||||||
import { securityFeatures, complianceFeatures } from './components/constants';
|
import { securityFeatures, complianceFeatures } from './components/constants';
|
||||||
import { augmentFeatures } from './utils';
|
import { augmentFeatures } from './utils';
|
||||||
import tempResolvers from './resolver';
|
|
||||||
|
|
||||||
export const initSecurityConfiguration = (el) => {
|
export const initSecurityConfiguration = (el) => {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
@ -15,7 +14,7 @@ export const initSecurityConfiguration = (el) => {
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
const apolloProvider = new VueApollo({
|
const apolloProvider = new VueApollo({
|
||||||
defaultClient: createDefaultClient(tempResolvers),
|
defaultClient: createDefaultClient(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
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 {
|
.diff-files-changed {
|
||||||
|
background-color: $body-bg;
|
||||||
|
|
||||||
.inline-parallel-buttons {
|
.inline-parallel-buttons {
|
||||||
@include gl-relative;
|
@include gl-relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
|
@ -8,6 +8,9 @@ module Mutations
|
||||||
include Mutations::SpamProtection
|
include Mutations::SpamProtection
|
||||||
include FindsProject
|
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
|
authorize :create_work_item
|
||||||
|
|
||||||
argument :description, GraphQL::Types::String,
|
argument :description, GraphQL::Types::String,
|
||||||
|
@ -29,6 +32,11 @@ module Mutations
|
||||||
|
|
||||||
def resolve(project_path:, **attributes)
|
def resolve(project_path:, **attributes)
|
||||||
project = authorized_find!(project_path)
|
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)
|
params = global_id_compatibility_params(attributes).merge(author_id: current_user.id)
|
||||||
|
|
||||||
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
|
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::Destroy
|
||||||
mount_mutation Mutations::Packages::DestroyFile
|
mount_mutation Mutations::Packages::DestroyFile
|
||||||
mount_mutation Mutations::Echo
|
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::Delete
|
||||||
mount_mutation Mutations::WorkItems::Update
|
mount_mutation Mutations::WorkItems::Update
|
||||||
end
|
end
|
||||||
|
|
|
@ -117,6 +117,7 @@ class Member < ApplicationRecord
|
||||||
# to projects/groups.
|
# to projects/groups.
|
||||||
scope :authorizable, -> do
|
scope :authorizable, -> do
|
||||||
connected_to_user
|
connected_to_user
|
||||||
|
.active_state
|
||||||
.non_request
|
.non_request
|
||||||
.non_minimal_access
|
.non_minimal_access
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
|
|
||||||
.row.gl-mt-3
|
.row.gl-mt-3
|
||||||
.form-group.col-md-9
|
.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
|
= 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 'shared/repository_size_limit_setting_registration_features_cta', form: f
|
||||||
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
|
= 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
|
### Listing all container repositories
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
GET /v2/_catalogue
|
GET /v2/_catalog
|
||||||
```
|
```
|
||||||
|
|
||||||
To list all container repositories on your GitLab instance, admin credentials are required:
|
To list all container repositories on your GitLab instance, admin credentials are required:
|
||||||
|
|
|
@ -5168,7 +5168,7 @@ Input type: `VulnerabilityRevertToDetectedInput`
|
||||||
|
|
||||||
### `Mutation.workItemCreate`
|
### `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`
|
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: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: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: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: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: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. |
|
| [`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:
|
You can configure the following:
|
||||||
|
|
||||||
- Select date range.
|
- 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.
|
- 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.
|
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:
|
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.
|
- **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.
|
- **Deploys:** the number of deployments to production in the date range.
|
||||||
- **Deployment Frequency:** the average number of deployments <sup>1</sup> to production <sup>2</sup>
|
- **Deployment Frequency:** the average number of deployments to production
|
||||||
per day in the date range.
|
per day in the date range.
|
||||||
|
|
||||||
1. To give a more accurate representation of deployments that actually completed successfully,
|
To see deployment metrics, you must have a [production environment configured](../../../ci/environments/index.md#deployment-tier-of-environments).
|
||||||
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
|
NOTE:
|
||||||
keep this slight change in mind.
|
In GitLab 13.9 and later, deployment metrics are calculated based on when the deployment was finished.
|
||||||
1. To see deployment metrics, you must have a
|
In GitLab 13.8 and earlier, deployment metrics are calculated based on when the deployment was created.
|
||||||
[production environment configured](../../../ci/environments/index.md#deployment-tier-of-environments).
|
|
||||||
|
|
||||||
You can learn more about these metrics in our [analytics definitions](../../analytics/index.md).
|
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`.
|
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
|
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
|
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,
|
(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.
|
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
|
## View merge request commits in context
|
||||||
|
|
||||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29274) in GitLab 13.12.
|
> - [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.
|
||||||
> - [Deployed behind a feature flag](../../feature_flags.md), enabled by default.
|
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/320757) in GitLab 14.8.
|
||||||
> - 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).
|
|
||||||
|
|
||||||
WARNING:
|
WARNING:
|
||||||
This feature is in [beta](../../../policy/alpha-beta-support.md#beta-features)
|
This feature is in [beta](../../../policy/alpha-beta-support.md#beta-features)
|
||||||
and is [incomplete](https://gitlab.com/groups/gitlab-org/-/epics/1192).
|
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
|
FLAG:
|
||||||
[risks when enabling features still in development](../../../administration/feature_flags.md#risks-when-enabling-features-still-in-development).
|
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||||
Refer to this feature's version history for more details.
|
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
|
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
|
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**:
|
1. Scroll to **(file-tree)** **Compare** and select **previously merged commits**:
|
||||||
|
|
||||||
![Previously merged commits](img/previously_merged_commits_v14_1.png)
|
![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)
|
def create_test_case(data, test_suite, job)
|
||||||
if data.key?('failure')
|
if data.key?('failure')
|
||||||
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
|
status = ::Gitlab::Ci::Reports::TestCase::STATUS_FAILED
|
||||||
system_output = data['failure']
|
system_output = data['failure'] || data['system_err']
|
||||||
attachment = attachment_path(data['system_out'])
|
attachment = attachment_path(data['system_out'])
|
||||||
elsif data.key?('error')
|
elsif data.key?('error')
|
||||||
status = ::Gitlab::Ci::Reports::TestCase::STATUS_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'])
|
attachment = attachment_path(data['system_out'])
|
||||||
elsif data.key?('skipped')
|
elsif data.key?('skipped')
|
||||||
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED
|
status = ::Gitlab::Ci::Reports::TestCase::STATUS_SKIPPED
|
||||||
|
|
|
@ -48,11 +48,15 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.context_key
|
def self.context_key
|
||||||
"#{self.class.name}_context"
|
@context_key ||= "analyzer_#{self.analyzer_key}_context".to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.suppress_key
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -426,8 +426,23 @@ msgid_plural "%d vulnerabilities dismissed"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
msgid "%d vulnerability updated"
|
msgid "%d vulnerability set to confirmed"
|
||||||
msgid_plural "%d vulnerabilities updated"
|
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[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
@ -17779,7 +17794,7 @@ msgstr ""
|
||||||
msgid "Groups|Group avatar"
|
msgid "Groups|Group avatar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Groups|Group description"
|
msgid "Groups|Group description (optional)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Groups|Group name"
|
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."
|
msgid "Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Groups|Optional group description."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Groups|Remove avatar"
|
msgid "Groups|Remove avatar"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19726,9 +19738,6 @@ msgstr ""
|
||||||
msgid "Integrations|can't exceed %{recipients_limit}"
|
msgid "Integrations|can't exceed %{recipients_limit}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Interactive developer security education."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Interactive mode"
|
msgid "Interactive mode"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -21067,9 +21076,6 @@ msgstr ""
|
||||||
msgid "Ki"
|
msgid "Ki"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Kontra"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Kroki"
|
msgid "Kroki"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -26643,12 +26649,42 @@ msgstr ""
|
||||||
msgid "PipelineStatusTooltip|Pipeline: %{ci_status}"
|
msgid "PipelineStatusTooltip|Pipeline: %{ci_status}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PipelineWizardDefaultCommitMessage|Add %{filename}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PipelineWizardDefaultCommitMessage|Update %{filename}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PipelineWizardInputValidation|This field is required"
|
msgid "PipelineWizardInputValidation|This field is required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "PipelineWizardInputValidation|This value is not valid"
|
msgid "PipelineWizardInputValidation|This value is not valid"
|
||||||
msgstr ""
|
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"
|
msgid "Pipelines"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32024,9 +32060,6 @@ msgstr ""
|
||||||
msgid "Secure token that identifies an external storage request."
|
msgid "Secure token that identifies an external storage request."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "SecureCodeWarrior"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Security"
|
msgid "Security"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32051,9 +32084,6 @@ msgstr ""
|
||||||
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
|
||||||
msgstr ""
|
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."
|
msgid "SecurityApprovals|A merge request approval is required when a security report contains a new vulnerability."
|
||||||
msgstr ""
|
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
|
# 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 [[ $rspec_run_status -ne 0 ]]; then
|
||||||
if [[ "${RETRY_FAILED_TESTS_IN_NEW_PROCESS}" == "true" ]]; 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
|
fi
|
||||||
else
|
else
|
||||||
echosuccess "No examples to retry, congrats!"
|
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
|
# Merge the JUnit report from retry into the first-try report
|
||||||
junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}"
|
junit_merge "${JUNIT_RETRY_FILE}" "${JUNIT_RESULT_FILE}"
|
||||||
|
|
||||||
return $rspec_run_status
|
exit $rspec_run_status
|
||||||
}
|
}
|
||||||
|
|
||||||
function rspec_rerun_previous_failed_tests() {
|
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,
|
dismissUserCalloutErrorResponse,
|
||||||
securityTrainingProviders,
|
securityTrainingProviders,
|
||||||
securityTrainingProvidersResponse,
|
securityTrainingProvidersResponse,
|
||||||
|
updateSecurityTrainingProvidersResponse,
|
||||||
|
updateSecurityTrainingProvidersErrorResponse,
|
||||||
testProjectPath,
|
testProjectPath,
|
||||||
textProviderIds,
|
textProviderIds,
|
||||||
} from '../mock_data';
|
} from '../mock_data';
|
||||||
|
@ -29,18 +31,22 @@ describe('TrainingProviderList component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let apolloProvider;
|
let apolloProvider;
|
||||||
|
|
||||||
const createApolloProvider = ({ resolvers, handlers = [] } = {}) => {
|
const createApolloProvider = ({ handlers = [] } = {}) => {
|
||||||
const defaultHandlers = [
|
const defaultHandlers = [
|
||||||
[
|
[
|
||||||
securityTrainingProvidersQuery,
|
securityTrainingProvidersQuery,
|
||||||
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
|
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
|
// make sure we don't have any duplicate handlers to avoid 'Request handler already defined for query` errors
|
||||||
const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])];
|
const mergedHandlers = [...new Map([...defaultHandlers, ...handlers])];
|
||||||
|
|
||||||
apolloProvider = createMockApollo(mergedHandlers, resolvers);
|
apolloProvider = createMockApollo(mergedHandlers);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = () => {
|
const createComponent = () => {
|
||||||
|
@ -62,7 +68,7 @@ describe('TrainingProviderList component', () => {
|
||||||
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
|
||||||
const findErrorAlert = () => wrapper.findComponent(GlAlert);
|
const findErrorAlert = () => wrapper.findComponent(GlAlert);
|
||||||
|
|
||||||
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change');
|
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', textProviderIds[0]);
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
@ -146,9 +152,9 @@ describe('TrainingProviderList component', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||||
|
|
||||||
await waitForMutationToBeLoaded();
|
await waitForQueryToBeLoaded();
|
||||||
|
|
||||||
toggleFirstProvider();
|
await toggleFirstProvider();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
|
@ -166,7 +172,14 @@ describe('TrainingProviderList component', () => {
|
||||||
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
|
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
mutation: configureSecurityTrainingProvidersMutation,
|
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', () => {
|
describe('when storing training provider configurations', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
createApolloProvider({
|
createApolloProvider({
|
||||||
resolvers: {
|
handlers: [
|
||||||
Mutation: {
|
[
|
||||||
configureSecurityTrainingProviders: () => ({
|
configureSecurityTrainingProvidersMutation,
|
||||||
errors: ['something went wrong!'],
|
jest.fn().mockReturnValue(updateSecurityTrainingProvidersErrorResponse),
|
||||||
securityTrainingProviders: [],
|
],
|
||||||
}),
|
],
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const securityTrainingProviders = [
|
||||||
description: 'Interactive developer security education',
|
description: 'Interactive developer security education',
|
||||||
url: 'https://www.example.org/security/training',
|
url: 'https://www.example.org/security/training',
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
|
isPrimary: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: textProviderIds[1],
|
id: textProviderIds[1],
|
||||||
|
@ -16,6 +17,7 @@ export const securityTrainingProviders = [
|
||||||
description: 'Security training with guide and learning pathways.',
|
description: 'Security training with guide and learning pathways.',
|
||||||
url: 'https://www.vendornametwo.com/',
|
url: 'https://www.vendornametwo.com/',
|
||||||
isEnabled: true,
|
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,
|
protected: true,
|
||||||
instance_runners_enabled: true,
|
instance_runners_enabled: true,
|
||||||
minutes_exceeded: false,
|
minutes_exceeded: false,
|
||||||
tag_ids: [22, 23],
|
tag_ids: match_array([22, 23]),
|
||||||
namespace_traversal_ids: [10]),
|
namespace_traversal_ids: [10]),
|
||||||
an_object_having_attributes(
|
an_object_having_attributes(
|
||||||
build_id: 60,
|
build_id: 60,
|
||||||
|
|
|
@ -99,6 +99,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
|
||||||
'Some failure'
|
'Some failure'
|
||||||
end
|
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
|
context 'and has error' do
|
||||||
let(:testcase_content) { '<error>Some error</error>' }
|
let(:testcase_content) { '<error>Some error</error>' }
|
||||||
|
|
||||||
|
@ -107,6 +120,19 @@ RSpec.describe Gitlab::Ci::Parsers::Test::Junit do
|
||||||
'Some error'
|
'Some error'
|
||||||
end
|
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
|
context 'and has skipped' do
|
||||||
let(:testcase_content) { '<skipped/>' }
|
let(:testcase_content) { '<skipped/>' }
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,22 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
|
||||||
Gitlab::Database::QueryAnalyzer.instance.within { example.run }
|
Gitlab::Database::QueryAnalyzer.instance.within { example.run }
|
||||||
end
|
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:|
|
shared_examples 'successful examples' do |model:|
|
||||||
let(:model) { 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 @invited_member }
|
||||||
it { is_expected.not_to include @requested_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 @member_with_minimal_access }
|
||||||
|
it { is_expected.not_to include awaiting_group_member }
|
||||||
|
it { is_expected.not_to include awaiting_project_member }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.distinct_on_user_with_max_access_level' do
|
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)
|
stub_feature_flags(work_items: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'a mutation that returns top-level errors',
|
it 'does not create the work item and returns an error' do
|
||||||
errors: ["Field 'workItemCreate' doesn't exist on type 'Mutation'", "Variable $workItemCreateInput is declared by anonymous mutation but not used"]
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,10 +71,11 @@ RSpec.describe 'Update a work item' do
|
||||||
stub_feature_flags(work_items: false)
|
stub_feature_flags(work_items: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does nothing and returns and error' do
|
it 'does not update the work item and returns and error' do
|
||||||
expect do
|
expect do
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
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')
|
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
|
||||||
end
|
end
|
||||||
|
|
|
@ -748,6 +748,30 @@ RSpec.describe API::Internal::Base do
|
||||||
end
|
end
|
||||||
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
|
context "custom action" do
|
||||||
let(:access_checker) { double(Gitlab::GitAccess) }
|
let(:access_checker) { double(Gitlab::GitAccess) }
|
||||||
let(:payload) do
|
let(:payload) do
|
||||||
|
|
Loading…
Reference in a new issue