Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-25 09:07:34 +00:00
parent 413a526be6
commit 6121eccf2b
13 changed files with 128 additions and 141 deletions

View file

@ -1,10 +1,8 @@
<script> <script>
import { GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import axios from '~/lib/utils/axios_utils';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { NEW_VERSION_FLAG, ROLLOUT_STRATEGY_ALL_USERS } from '../constants'; import { ROLLOUT_STRATEGY_ALL_USERS } from '../constants';
import { createNewEnvironmentScope } from '../store/helpers';
import FeatureFlagForm from './form.vue'; import FeatureFlagForm from './form.vue';
export default { export default {
@ -13,48 +11,14 @@ export default {
GlAlert, GlAlert,
}, },
mixins: [featureFlagsMixin()], mixins: [featureFlagsMixin()],
inject: {
showUserCallout: {},
userCalloutId: {
default: '',
},
userCalloutsPath: {
default: '',
},
},
data() {
return {
userShouldSeeNewFlagAlert: this.showUserCallout,
};
},
computed: { computed: {
...mapState(['error', 'path']), ...mapState(['error', 'path']),
scopes() {
return [
createNewEnvironmentScope(
{
environmentScope: '*',
active: true,
},
this.glFeatures.featureFlagsPermissions,
),
];
},
version() {
return NEW_VERSION_FLAG;
},
strategies() { strategies() {
return [{ name: ROLLOUT_STRATEGY_ALL_USERS, parameters: {}, scopes: [] }]; return [{ name: ROLLOUT_STRATEGY_ALL_USERS, parameters: {}, scopes: [] }];
}, },
}, },
methods: { methods: {
...mapActions(['createFeatureFlag']), ...mapActions(['createFeatureFlag']),
dismissNewVersionFlagAlert() {
this.userShouldSeeNewFlagAlert = false;
axios.post(this.userCalloutsPath, {
feature_name: this.userCalloutId,
});
},
}, },
}; };
</script> </script>
@ -69,9 +33,7 @@ export default {
<feature-flag-form <feature-flag-form
:cancel-path="path" :cancel-path="path"
:submit-text="s__('FeatureFlags|Create feature flag')" :submit-text="s__('FeatureFlags|Create feature flag')"
:scopes="scopes"
:strategies="strategies" :strategies="strategies"
:version="version"
@handleSubmit="(data) => createFeatureFlag(data)" @handleSubmit="(data) => createFeatureFlag(data)"
/> />
</div> </div>

View file

@ -1,7 +1,6 @@
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { NEW_VERSION_FLAG } from '../../constants'; import { mapStrategiesToRails } from '../helpers';
import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
import * as types from './mutation_types'; import * as types from './mutation_types';
/** /**
@ -17,12 +16,7 @@ export const createFeatureFlag = ({ state, dispatch }, params) => {
dispatch('requestCreateFeatureFlag'); dispatch('requestCreateFeatureFlag');
return axios return axios
.post( .post(state.endpoint, mapStrategiesToRails(params))
state.endpoint,
params.version === NEW_VERSION_FLAG
? mapStrategiesToRails(params)
: mapFromScopesViewModel(params),
)
.then(() => { .then(() => {
dispatch('receiveCreateFeatureFlagSuccess'); dispatch('receiveCreateFeatureFlagSuccess');
visitUrl(state.path); visitUrl(state.path);

View file

@ -1,5 +1,5 @@
<script> <script>
import { GlButton, GlModalDirective } from '@gitlab/ui'; import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
@ -9,8 +9,10 @@ export default {
i18n: { i18n: {
replace: __('Replace'), replace: __('Replace'),
replacePrimaryBtnText: __('Replace file'), replacePrimaryBtnText: __('Replace file'),
delete: __('Delete'),
}, },
components: { components: {
GlButtonGroup,
GlButton, GlButton,
UploadBlobModal, UploadBlobModal,
}, },
@ -48,7 +50,7 @@ export default {
replaceModalId() { replaceModalId() {
return uniqueId('replace-modal'); return uniqueId('replace-modal');
}, },
title() { replaceModalTitle() {
return sprintf(__('Replace %{name}'), { name: this.name }); return sprintf(__('Replace %{name}'), { name: this.name });
}, },
}, },
@ -57,13 +59,16 @@ export default {
<template> <template>
<div class="gl-mr-3"> <div class="gl-mr-3">
<gl-button-group>
<gl-button v-gl-modal="replaceModalId"> <gl-button v-gl-modal="replaceModalId">
{{ $options.i18n.replace }} {{ $options.i18n.replace }}
</gl-button> </gl-button>
<gl-button>{{ $options.i18n.delete }}</gl-button>
</gl-button-group>
<upload-blob-modal <upload-blob-modal
:modal-id="replaceModalId" :modal-id="replaceModalId"
:modal-title="title" :modal-title="replaceModalTitle"
:commit-message="title" :commit-message="replaceModalTitle"
:target-branch="targetBranch || ref" :target-branch="targetBranch || ref"
:original-branch="originalBranch || ref" :original-branch="originalBranch || ref"
:can-push-code="canPushCode" :can-push-code="canPushCode"

View file

@ -7,14 +7,14 @@ import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constant
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql'; import blobInfoQuery from '../queries/blob_info.query.graphql';
import BlobButtonGroup from './blob_button_group.vue';
import BlobEdit from './blob_edit.vue'; import BlobEdit from './blob_edit.vue';
import BlobReplace from './blob_replace.vue';
export default { export default {
components: { components: {
BlobHeader, BlobHeader,
BlobEdit, BlobEdit,
BlobReplace, BlobButtonGroup,
BlobContent, BlobContent,
GlLoadingIcon, GlLoadingIcon,
}, },
@ -132,7 +132,7 @@ export default {
> >
<template #actions> <template #actions>
<blob-edit :edit-path="blobInfo.editBlobPath" :web-ide-path="blobInfo.ideEditPath" /> <blob-edit :edit-path="blobInfo.editBlobPath" :web-ide-path="blobInfo.ideEditPath" />
<blob-replace <blob-button-group
v-if="isLoggedIn" v-if="isLoggedIn"
:path="path" :path="path"
:name="blobInfo.name" :name="blobInfo.name"

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddForeignKeyForEnvironmentIdToEnvironments < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
# `validate: false` option is passed here, because validating the existing rows fails by the orphaned deployments,
# which will be cleaned up in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64588.
# The validation runs for only new records or updates, so that we can at least
# stop creating orphaned rows.
add_concurrent_foreign_key :deployments, :environments, column: :environment_id, on_delete: :cascade, validate: false
end
def down
with_lock_retries do
remove_foreign_key_if_exists :deployments, :environments
end
end
end

View file

@ -0,0 +1 @@
e43889baa57ea2cd0b87ba98819408115955f6a6586b3275cf0a08bd79909c71

View file

@ -25476,6 +25476,9 @@ CREATE TRIGGER trigger_has_external_wiki_on_update AFTER UPDATE ON services FOR
ALTER TABLE ONLY chat_names ALTER TABLE ONLY chat_names
ADD CONSTRAINT fk_00797a2bf9 FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE; ADD CONSTRAINT fk_00797a2bf9 FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE;
ALTER TABLE ONLY deployments
ADD CONSTRAINT fk_009fd21147 FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY epics ALTER TABLE ONLY epics
ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL; ADD CONSTRAINT fk_013c9f36ca FOREIGN KEY (due_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL;

View file

@ -3591,6 +3591,27 @@ Input type: `RunnersRegistrationTokenResetInput`
| <a id="mutationrunnersregistrationtokenreseterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationrunnersregistrationtokenreseterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationrunnersregistrationtokenresettoken"></a>`token` | [`String`](#string) | The runner token after mutation. | | <a id="mutationrunnersregistrationtokenresettoken"></a>`token` | [`String`](#string) | The runner token after mutation. |
### `Mutation.scanExecutionPolicyCommit`
Input type: `ScanExecutionPolicyCommitInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationscanexecutionpolicycommitclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationscanexecutionpolicycommitoperationmode"></a>`operationMode` | [`MutationOperationMode!`](#mutationoperationmode) | Changes the operation mode. |
| <a id="mutationscanexecutionpolicycommitpolicyyaml"></a>`policyYaml` | [`String!`](#string) | YAML snippet of the policy. |
| <a id="mutationscanexecutionpolicycommitprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationscanexecutionpolicycommitbranch"></a>`branch` | [`String`](#string) | Name of the branch to which the policy changes are committed. |
| <a id="mutationscanexecutionpolicycommitclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationscanexecutionpolicycommiterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.terraformStateDelete` ### `Mutation.terraformStateDelete`
Input type: `TerraformStateDeleteInput` Input type: `TerraformStateDeleteInput`

View file

@ -34,7 +34,7 @@ RSpec.describe 'Database schema' do
compliance_management_frameworks: %w[group_id], compliance_management_frameworks: %w[group_id],
commit_user_mentions: %w[commit_id], commit_user_mentions: %w[commit_id],
deploy_keys_projects: %w[deploy_key_id], deploy_keys_projects: %w[deploy_key_id],
deployments: %w[deployable_id environment_id user_id], deployments: %w[deployable_id user_id],
draft_notes: %w[discussion_id commit_id], draft_notes: %w[discussion_id commit_id],
epics: %w[updated_by_id last_edited_by_id state_id], epics: %w[updated_by_id last_edited_by_id state_id],
events: %w[target_id], events: %w[target_id],

View file

@ -4,7 +4,6 @@ import Vuex from 'vuex';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import Form from '~/feature_flags/components/form.vue'; import Form from '~/feature_flags/components/form.vue';
import NewFeatureFlag from '~/feature_flags/components/new_feature_flag.vue'; import NewFeatureFlag from '~/feature_flags/components/new_feature_flag.vue';
import { ROLLOUT_STRATEGY_ALL_USERS, DEFAULT_PERCENT_ROLLOUT } from '~/feature_flags/constants';
import createStore from '~/feature_flags/store/new'; import createStore from '~/feature_flags/store/new';
import { allUsersStrategy } from '../mock_data'; import { allUsersStrategy } from '../mock_data';
@ -71,18 +70,6 @@ describe('New feature flag form', () => {
expect(wrapper.find(Form).exists()).toEqual(true); expect(wrapper.find(Form).exists()).toEqual(true);
}); });
it('should render default * row', () => {
const defaultScope = {
id: expect.any(String),
environmentScope: '*',
active: true,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
rolloutUserIds: '',
};
expect(wrapper.vm.scopes).toEqual([defaultScope]);
});
it('has an all users strategy by default', () => { it('has an all users strategy by default', () => {
const strategies = wrapper.find(Form).props('strategies'); const strategies = wrapper.find(Form).props('strategies');

View file

@ -1,13 +1,8 @@
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants'; import { ROLLOUT_STRATEGY_ALL_USERS, NEW_VERSION_FLAG } from '~/feature_flags/constants';
import { import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
LEGACY_FLAG,
NEW_VERSION_FLAG,
} from '~/feature_flags/constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '~/feature_flags/store/helpers';
import { import {
createFeatureFlag, createFeatureFlag,
requestCreateFeatureFlag, requestCreateFeatureFlag,
@ -24,33 +19,13 @@ describe('Feature flags New Module Actions', () => {
let mockedState; let mockedState;
beforeEach(() => { beforeEach(() => {
mockedState = state({ endpoint: 'feature_flags.json', path: '/feature_flags' }); mockedState = state({ endpoint: '/feature_flags.json', path: '/feature_flags' });
}); });
describe('createFeatureFlag', () => { describe('createFeatureFlag', () => {
let mock; let mock;
const actionParams = {
name: 'name',
description: 'description',
active: true,
version: LEGACY_FLAG,
scopes: [
{
id: 1,
environmentScope: 'environmentScope',
active: true,
canUpdate: true,
protected: true,
shouldBeDestroyed: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
},
],
};
beforeEach(() => { beforeEach(() => {
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
@ -60,9 +35,22 @@ describe('Feature flags New Module Actions', () => {
describe('success', () => { describe('success', () => {
it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', (done) => { it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', (done) => {
const convertedActionParams = mapFromScopesViewModel(actionParams); const actionParams = {
name: 'name',
mock.onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams).replyOnce(200); description: 'description',
active: true,
version: NEW_VERSION_FLAG,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
id: 1,
scopes: [{ id: 1, environmentScope: 'environmentScope', shouldBeDestroyed: false }],
shouldBeDestroyed: false,
},
],
};
mock.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams)).replyOnce(200);
testAction( testAction(
createFeatureFlag, createFeatureFlag,
@ -80,9 +68,11 @@ describe('Feature flags New Module Actions', () => {
done, done,
); );
}); });
});
it('sends strategies for new style feature flags', (done) => { describe('error', () => {
const newVersionFlagParams = { it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', (done) => {
const actionParams = {
name: 'name', name: 'name',
description: 'description', description: 'description',
active: true, active: true,
@ -98,33 +88,7 @@ describe('Feature flags New Module Actions', () => {
], ],
}; };
mock mock
.onPost(`${TEST_HOST}/endpoint.json`, mapStrategiesToRails(newVersionFlagParams)) .onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
.replyOnce(200);
testAction(
createFeatureFlag,
newVersionFlagParams,
mockedState,
[],
[
{
type: 'requestCreateFeatureFlag',
},
{
type: 'receiveCreateFeatureFlagSuccess',
},
],
done,
);
});
});
describe('error', () => {
it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', (done) => {
const convertedActionParams = mapFromScopesViewModel(actionParams);
mock
.onPost(`${TEST_HOST}/endpoint.json`, convertedActionParams)
.replyOnce(500, { message: [] }); .replyOnce(500, { message: [] });
testAction( testAction(

View file

@ -1,5 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import BlobReplace from '~/repository/components/blob_replace.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
const DEFAULT_PROPS = { const DEFAULT_PROPS = {
@ -14,11 +17,11 @@ const DEFAULT_INJECT = {
originalBranch: 'master', originalBranch: 'master',
}; };
describe('BlobReplace component', () => { describe('BlobButtonGroup component', () => {
let wrapper; let wrapper;
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(BlobReplace, { wrapper = shallowMount(BlobButtonGroup, {
propsData: { propsData: {
...DEFAULT_PROPS, ...DEFAULT_PROPS,
...props, ...props,
@ -26,6 +29,9 @@ describe('BlobReplace component', () => {
provide: { provide: {
...DEFAULT_INJECT, ...DEFAULT_INJECT,
}, },
directives: {
GlModal: createMockDirective(),
},
}); });
}; };
@ -34,6 +40,7 @@ describe('BlobReplace component', () => {
}); });
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findReplaceButton = () => wrapper.findAll(GlButton).at(0);
it('renders component', () => { it('renders component', () => {
createComponent(); createComponent();
@ -46,6 +53,28 @@ describe('BlobReplace component', () => {
}); });
}); });
describe('buttons', () => {
beforeEach(() => {
createComponent();
});
it('renders both the replace and delete button', () => {
expect(wrapper.findAll(GlButton)).toHaveLength(2);
});
it('renders the buttons in the correct order', () => {
expect(wrapper.findAll(GlButton).at(0).text()).toBe('Replace');
expect(wrapper.findAll(GlButton).at(1).text()).toBe('Delete');
});
it('triggers the UploadBlobModal from the replace button', () => {
const { value } = getBinding(findReplaceButton().element, 'gl-modal');
const modalId = findUploadBlobModal().props('modalId');
expect(modalId).toEqual(value);
});
});
it('renders UploadBlobModal', () => { it('renders UploadBlobModal', () => {
createComponent(); createComponent();

View file

@ -3,9 +3,9 @@ import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import BlobContent from '~/blob/components/blob_content.vue'; import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue'; import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue'; import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import BlobEdit from '~/repository/components/blob_edit.vue'; import BlobEdit from '~/repository/components/blob_edit.vue';
import BlobReplace from '~/repository/components/blob_replace.vue';
let wrapper; let wrapper;
const simpleMockData = { const simpleMockData = {
@ -80,7 +80,7 @@ describe('Blob content viewer component', () => {
const findBlobHeader = () => wrapper.findComponent(BlobHeader); const findBlobHeader = () => wrapper.findComponent(BlobHeader);
const findBlobEdit = () => wrapper.findComponent(BlobEdit); const findBlobEdit = () => wrapper.findComponent(BlobEdit);
const findBlobContent = () => wrapper.findComponent(BlobContent); const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobReplace = () => wrapper.findComponent(BlobReplace); const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
@ -200,7 +200,7 @@ describe('Blob content viewer component', () => {
}); });
}); });
describe('BlobReplace', () => { describe('BlobButtonGroup', () => {
const { name, path } = simpleMockData; const { name, path } = simpleMockData;
it('renders component', async () => { it('renders component', async () => {
@ -210,13 +210,13 @@ describe('Blob content viewer component', () => {
mockData: { blobInfo: simpleMockData }, mockData: { blobInfo: simpleMockData },
stubs: { stubs: {
BlobContent: true, BlobContent: true,
BlobReplace: true, BlobButtonGroup: true,
}, },
}); });
await nextTick(); await nextTick();
expect(findBlobReplace().props()).toMatchObject({ expect(findBlobButtonGroup().props()).toMatchObject({
name, name,
path, path,
}); });
@ -235,7 +235,7 @@ describe('Blob content viewer component', () => {
await nextTick(); await nextTick();
expect(findBlobReplace().exists()).toBe(false); expect(findBlobButtonGroup().exists()).toBe(false);
}); });
}); });
}); });